Public: anyone can read this; the crew can change it.

Design: the looks page is built from the design system, and its look-* classes leave §5d #3221

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

mason/forge:mason/3198-design-the-looks-page-is-built-from-the → main 1 commit fork

  • Merged. mason/3198-design-the-looks-page-is-built-from-the was merged into main as ddc5abc.

Review

Sign in to review this change.
5 files changed +60 −61
docs/DESIGN.md +2 −1
@@ -205,6 +205,7 @@ specification.
205205 | **run log** | `pre.run-log` | `data-run` while the run is going | the log as written, wrapped so a phone never scrolls sideways; a script appends the run's lines from the page's one `/events` stream and follows the bottom until the reader scrolls up; the page never reloads itself | a scrolling pane that follows the tail |
206206 | **assignee card** | `.card` with an `.edit-row` form | — | who is on it, then one select and one button for a writer. People first, the crew last and marked `(agent)`, because assigning an agent is what starts it working and the choice is not the same kind of choice | a picker with people, then agents |
207207 | **inbox** (`asks_page.ts`) | one list above the feed | — | _On you_: what asks you and what is assigned to you, one row each, each with what it waits for (`needsYouSection`), and one count in the top bar. Absent when empty: a card saying nothing is a zero drawn | the same two panes on the dashboard |
208 | **look** (`pages/looks.ts`) | `.look` in a `.looks` strip | `.look[data-slot=phone\|desktop\|thumb\|now]` sizes the frame; `.looks[data-compare]` sets two sides (before · main, then after · the pull) beside each other | a picture always carries its own `width`/`height`, so nothing shifts when it loads; a phone keeps 200px, a desktop takes the rest up to its own size, and under 640px a strip stacks; an attached picture is a `figure` in the strip with its line under it | the picture in the terminal (§6·0c) |
208209 | **board** (`pages/board.ts`) | `.board` (`.board-grid`, `.board-region`) | `data-wall` (across the room: no keys, no pointer, larger), `data-focus=working\|landed\|decided\|today` | the one whole-screen layout: four live regions fill a second monitor ≥1100px and each list scrolls on its own, one column below; inside, only §5 parts (list card rows, badges, meters, `kbd`); a swapped region's heading wears `data-fresh` for a moment | four panes, one focused by its number |
209210
210211 Two things are deliberately **not** in the vocabulary: skeleton loaders (the
@@ -387,7 +388,7 @@ gone, so this list only shrinks.
387388 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
388389 - **`style/content.ts`, diff viewer** — the diff table: its gutters, line kinds, file head and inline review comments: `.add` `.del` `.dh` `.diff` `.diff-body` `.diff-head` `.diff-note` `.diff-path` `.diff-stat` `.diff-stub` `.diff-summary` `.diff-table` `.dl` `.dl-review` `.dn` `.dn-comment` `.nonl` `.rc-body` `.rc-form` `.rc-head` `.rc-outdated` `.rc-resolved-line` `.wd`
389390 - **`style/content.ts`, review verdicts** — the body under a verdict: `.review-body`
390 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs, checks and look screenshots (a pull's look sets the before side, main's, sits first, then the pull's after side: a `.look-compare` of two `.look-side`): `.app-url` `.check-progress` `.check-steps` `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.look` `.look-attached` `.look-attached-phone` `.look-attachments` `.look-card` `.look-compare` `.look-desktop` `.look-now` `.look-of` `.look-pair` `.look-phone` `.look-side` `.look-thumb` `.pinned-failures` `.pinned-slot` `.run-actions` `.run-jump` `.run-live` `.run-steer` `.run-summary` `.step-state` `.tests-bar` `.tests-words` `.txt` `.who`
391 - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.app-url` `.check-progress` `.check-steps` `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-actions` `.run-jump` `.run-live` `.run-steer` `.run-summary` `.step-state` `.tests-bar` `.tests-words` `.txt` `.who`
391392 - **`style/content.ts`, the code browser: explorer beside content** — the explorer column's own parts: `.code-main` `.code-split` `.ex-empty` `.ex-find` `.ex-head` `.ex-list` `.ex-name` `.ex-ref` `.ex-twist` `.explorer-toggle`
392393 - **`style/content.ts`, the finder** — the finder's footnote: `.finder-note`
393394 - **`style/content.ts`, the editor** — the editor's parts beyond the §5 row: `.editor-mirror` `.editor-path` `.editor-preview` `.editor-stack` `.editor-tab` `.mirrored` `.ml`
src/web/pages/looks.ts +31 −29
@@ -161,7 +161,7 @@ export function lookCard(
161161 html`<p class="muted small">
162162 No picture yet. One is taken when the preview comes up green.
163163 </p>`,
164 { head, extra: "look-card" },
164 { head },
165165 );
166166 }
167167 // Home first, then the rest in the order the manifest is likeliest to
@@ -182,7 +182,7 @@ export function lookCard(
182182 ): Html => {
183183 const url = lookUrl(base, look);
184184 const scheme = look.theme === "dark" ? " in the dark" : "";
185 return html`<a class="look look-${kind}" href="${url}">
185 return html`<a class="look" data-slot="${kind}" href="${url}">
186186 <img
187187 src="${url}"
188188 width="${LOOK_SIZES[kind].width}"
@@ -202,18 +202,18 @@ export function lookCard(
202202 (l) => l.commitOid === headOid && l.error !== null,
203203 );
204204 const label = when(labelled, () =>
205 html`<p class="muted small look-page">${page}</p>`,
205 html`<p class="muted small">${page}</p>`,
206206 );
207207 const shown = phone ?? desktop;
208208 if (shown === undefined)
209 return html`<div class="look-group">
209 return html`<section class="stack stack-tight">
210210 ${label}
211211 <p class="muted small">
212212 ${failed === undefined
213213 ? "No picture yet. One is taken when the preview comes up green."
214214 : html`No picture of ${sha(headOid)}: ${failed.error}.`}
215215 </p>
216 </div>`;
216 </section>`;
217217 const moved = shown.commitOid !== headOid;
218218 const mainOfPage = mainLooks.filter((l) => l.page === page);
219219 const beforePhone = pictured(mainOfPage, "phone");
@@ -231,26 +231,26 @@ export function lookCard(
231231 );
232232 const darkPhone = pictured(darkOfPage, "phone");
233233 const darkDesktop = pictured(darkOfPage, "desktop");
234 return html`<div class="look-group">
234 return html`<section class="stack stack-tight">
235235 ${label}
236 <div class="look-compare">
237 <div class="look-side look-before">
236 <div class="looks" data-compare>
237 <div>
238238 <p class="muted small">Before · main</p>
239239 ${beforeShown.length === 0
240 ? html`<p class="muted small look-nobefore">
240 ? html`<p class="muted small">
241241 Main has no look of ${page} yet.
242242 </p>`
243 : html`<div class="look-pair">
243 : html`<div class="looks">
244244 ${beforeShown.map((l) => picture(l, l.kind, page, "main"))}
245245 </div>
246 <span class="muted small look-of"
247 >at ${sha(beforeShown[0]!.commitOid, `${base}/commit/${beforeShown[0]!.commitOid}`)} ·
248 ${time(beforeShown[0]!.createdAt)}</span
249 >`}
246 <p class="muted small">
247 at ${sha(beforeShown[0]!.commitOid, `${base}/commit/${beforeShown[0]!.commitOid}`)} ·
248 ${time(beforeShown[0]!.createdAt)}
249 </p>`}
250250 </div>
251 <div class="look-side look-after">
251 <div>
252252 <p class="muted small">After · #${pull.number}</p>
253 <div class="look-pair">
253 <div class="looks">
254254 ${when(phone, (l) => picture(l, "phone", page))}
255255 ${when(desktop, (l) => picture(l, "desktop", page))}
256256 </div>
@@ -259,14 +259,14 @@ export function lookCard(
259259 ${when(darkPhone ?? darkDesktop, () =>
260260 html`<details class="quiet-details">
261261 <summary class="muted small">In the dark</summary>
262 <div class="look-pair">
262 <div class="looks">
263263 ${when(darkPhone, (l) => picture(l, "phone", page))}
264264 ${when(darkDesktop, (l) => picture(l, "desktop", page))}
265265 </div>
266266 </details>`,
267267 )}
268 <span class="muted small look-of"
269 >at ${sha(shown.commitOid, `${base}/commit/${shown.commitOid}`)} ·
268 <p class="muted small">
269 at ${sha(shown.commitOid, `${base}/commit/${shown.commitOid}`)} ·
270270 ${time(shown.createdAt)}${when(
271271 moved,
272272 () =>
@@ -274,16 +274,16 @@ export function lookCard(
274274 since${failed === undefined
275275 ? html`; a new picture follows its deploy`
276276 : html`; no picture of ${sha(headOid)}: ${failed.error}`}`,
277 )}</span
278 >
279 </div>`;
277 )}
278 </p>
279 </section>`;
280280 };
281281 // What people attached sits in the same strip, each with its line and
282282 // who put it there. Their size is their own: the picture's, not a window's.
283283 const person = (look: Look): Html => {
284284 const url = lookUrl(base, look);
285285 const size = attachedSize(ctx, look);
286 return html`<figure class="look-attached look-attached-${look.kind}">
286 return html`<figure data-slot="${look.kind}">
287287 <a class="look" href="${url}">
288288 <img
289289 src="${url}"
@@ -299,10 +299,12 @@ export function lookCard(
299299 </figure>`;
300300 };
301301 return card(
302 html`${pages.map(group)}${when(attached.length > 0, () =>
303 html`<div class="look-pair look-attachments">${attached.map(person)}</div>`,
304 )}`,
305 { head, extra: "look-card" },
302 html`<div class="stack">
303 ${pages.map(group)}${when(attached.length > 0, () =>
304 html`<div class="looks">${attached.map(person)}</div>`,
305 )}
306 </div>`,
307 { head },
306308 );
307309 }
308310
@@ -346,7 +348,7 @@ export function lookNow(ctx: Ctx, repo: Repo, record: AppRecord): Html {
346348 const base = repoBase(repo);
347349 const size = LOOK_SIZES[look.kind];
348350 return html`<a
349 class="look-now"
351 class="look" data-slot="now"
350352 href="${base}/app/production/open"
351353 title="How it looks now, at ${look.commitOid.slice(0, 7)}"
352354 >
@@ -361,7 +363,7 @@ export function lookNow(ctx: Ctx, repo: Repo, record: AppRecord): Html {
361363
362364 /** A small picture as a thumbnail that opens the pull. */
363365 export function lookThumb(href: string, src: string): Html {
364 return html`<a class="look-thumb" href="${href}"
366 return html`<a class="look" data-slot="thumb" href="${href}"
365367 ><img src="${src}" width="30" height="40" alt="" loading="lazy"
366368 /></a>`;
367369 }
src/web/style/components.ts +22
@@ -294,6 +294,28 @@ a.sha:hover { color: var(--accent); }
294294 .card[data-tone="merged"] { border-color: color-mix(in srgb, var(--merged) 40%, var(--border)); }
295295 .card[data-tone="attn"] { border-color: color-mix(in srgb, var(--attn) 40%, var(--border)); }
296296
297 /* ---- look: a picture the forge took, in a hairline frame ----
298 A strip sets a phone beside a desktop: the phone keeps a phone's
299 narrowness, the desktop takes what is left, never more than its own size;
300 under 640px they stack. data-compare sets two sides (before, after) beside
301 each other instead. A thumb shows the top of the picture; now is
302 production small. */
303 .look { display: block; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--muted); }
304 .look img { display: block; width: 100%; height: auto; }
305 .look[data-slot="thumb"] { flex: none; width: 30px; height: 40px; border-radius: 4px; }
306 .look[data-slot="thumb"] img { height: 100%; object-fit: cover; object-position: top; }
307 .look[data-slot="now"] { flex: none; width: 168px; }
308 .looks { display: flex; gap: 14px; align-items: flex-start; flex-wrap: wrap; }
309 .looks > [data-slot="phone"] { flex: 0 0 200px; }
310 .looks > [data-slot="desktop"] { flex: 1 1 320px; min-width: 0; max-width: 1280px; }
311 .looks > figure { margin: 0; display: flex; flex-direction: column; gap: 6px; }
312 .looks[data-compare] { gap: 20px; }
313 .looks[data-compare] > * { flex: 1 1 320px; min-width: 0; display: flex; flex-direction: column; gap: 6px; }
314 @media (max-width: 640px) {
315 .looks:not([data-compare]) { flex-direction: column; }
316 .looks > [data-slot="phone"] { flex-basis: auto; width: 200px; }
317 }
318
297319 /* ---- list rows ---- */
298320 .list > li + li { border-top: 1px solid var(--border); }
299321 .row {
src/web/style/content.ts −26
@@ -864,32 +864,6 @@ form.inline { display: inline; margin-left: auto; }
864864 .env-card .card-head { gap: 10px; }
865865 .env-facts { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; font-size: 13px; color: var(--muted-fg); }
866866 .env-error { color: var(--danger); font-size: 13px; margin-top: 6px; overflow-wrap: anywhere; text-wrap: pretty; }
867 /* The look: a phone beside a desktop, each in a hairline frame. The phone
868 keeps a phone's narrowness; the desktop takes what is left, never more
869 than its own size. A thumbnail shows the top of the phone picture; the
870 studio head shows production small. */
871 .look-card .card-head { gap: 10px; flex-wrap: wrap; }
872 .look-card .look-of { display: inline-flex; align-items: center; gap: 4px; flex-wrap: wrap; }
873 .look-pair { display: flex; gap: 14px; align-items: flex-start; }
874 .look { display: block; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--muted); }
875 .look img { display: block; width: 100%; height: auto; }
876 .look-compare { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
877 .look-side { flex: 1 1 320px; min-width: 0; }
878 .look-side > p { margin: 0 0 6px; }
879 .look-side .look-pair { flex-wrap: wrap; }
880 .look-attachments { flex-wrap: wrap; margin-top: 10px; }
881 .look-attached { margin: 0; display: flex; flex-direction: column; gap: 6px; flex: 1 1 200px; min-width: 0; max-width: 1280px; }
882 .look-attached-phone { flex: 0 0 200px; }
883 .look-phone { flex: 0 0 200px; }
884 .look-desktop { flex: 1 1 320px; min-width: 0; max-width: 1280px; }
885 @media (max-width: 640px) {
886 .look-pair { flex-direction: column; }
887 .look-phone { flex-basis: auto; width: 200px; }
888 }
889 .look-thumb { display: block; flex: none; width: 30px; height: 40px; border: 1px solid var(--border); border-radius: 4px; overflow: hidden; background: var(--muted); }
890 .look-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: top; }
891 .look-now { display: block; flex: none; width: 168px; border: 1px solid var(--border); border-radius: var(--radius-sm); overflow: hidden; background: var(--muted); }
892 .look-now img { display: block; width: 100%; height: auto; }
893867 /* The checks line inside the merge box, in the reviews chip's voice. */
894868 .checks-line {
895869 display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
test/looks.test.ts +5 −5
@@ -670,7 +670,7 @@ describe("the look", () => {
670670 const production = looksForEnv(app.db, record.id, "production").find(
671671 (l) => l.kind === "thumb",
672672 )!;
673 expect(page).toContain(`class="look-now"`);
673 expect(page).toContain(`class="look" data-slot="now"`);
674674 expect(page).toContain(
675675 `src="/alice/hello/looks/production/${production.commitOid}/thumb.png?v=`,
676676 );
@@ -761,8 +761,8 @@ describe("the look", () => {
761761 const page = await (
762762 await web(`/alice/hello/pulls/${opened.value.number}`, aliceCookie)
763763 ).text();
764 expect(page).toContain(`<p class="muted small look-page">/</p>`);
765 expect(page).toContain(`<p class="muted small look-page">/board</p>`);
764 expect(page).toContain(`<p class="muted small">/</p>`);
765 expect(page).toContain(`<p class="muted small">/board</p>`);
766766 expect(page).toContain(
767767 `src="/alice/hello/looks/${env}/${commitOid}/board-phone.png?v=`,
768768 );
@@ -805,7 +805,7 @@ describe("the look", () => {
805805 await web(`/alice/hello/pulls/${opened.value.number}`, aliceCookie)
806806 ).text();
807807 expect(darkPage).toContain(
808 `<a class="look look-phone" href="/alice/hello/looks/${env}/${commitOid}/phone-dark.png?v=`,
808 `<a class="look" data-slot="phone" href="/alice/hello/looks/${env}/${commitOid}/phone-dark.png?v=`,
809809 );
810810 expect(darkPage).toContain(
811811 `src="/alice/hello/looks/${env}/${commitOid}/board-desktop-dark.png?v=`,
@@ -1120,7 +1120,7 @@ describe("the look", () => {
11201120 await web(`/alice/hello/pulls/${pull.number}`, aliceCookie)
11211121 ).text();
11221122 const group = (page: string) =>
1123 text.split(`<p class="muted small look-page">${page}</p>`)[1]!.split("look-group")[0]!;
1123 text.split(`<p class="muted small">${page}</p>`)[1]!.split("<section")[0]!;
11241124 const map = group("/map");
11251125 expect(map).toContain(`/looks/production/${mainOid}/map-phone.png?v=`);
11261126 expect(map).toContain(`/looks/${env}/${oid}/map-phone.png?v=`);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs