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

UI sweep: the audit's defects on /boards, /system, the header and settings, and a 12 px type floor #3712

Merged opened this in answer to

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

Merged

scout/forge:scout/3698-ui-sweep-the-audit-s-defects-on-boards-s → main 1 commit fork

  • Merged. scout/3698-ui-sweep-the-audit-s-defects-on-boards-s was merged into main as 54682d7.

Review

Sign in to review this change.
13 files changed +121 −44
src/web/layout.ts +2 −3
@@ -205,8 +205,7 @@ function userMenu(
205205 { label: "Explore", href: "/explore", icon: "search" },
206206 { label: "Map", href: "/map", icon: "map" },
207207 { separator: true, label: "" },
208 // The "+" menu is hidden on a phone to keep the topbar inside the
209 // screen; creating things stays one tap away here.
208 // The "+" menu shows on every width; these repeat it for the user menu.
210209 { label: "New app", href: "/apps/new", icon: "rocket" },
211210 { label: "New repository", href: "/new", icon: "repo" },
212211 { label: "Migrate", href: "/migrate", icon: "download" },
@@ -450,7 +449,7 @@ function topbar(
450449 ${searchBar()}
451450 ${user
452451 ? html`<a class="small" href="/asks" data-shortcut="g q">Asks</a
453 ><span class="hide-sm">${newMenu(ctx)}</span>${userMenu(
452 >${newMenu(ctx)}${userMenu(
454453 ctx,
455454 user,
456455 theme,
src/web/pages/flow-panel.ts +8 −5
@@ -102,9 +102,10 @@ export function flowPanel(app: App, reader: Reader, url: URL, base: string): Htm
102102 const onReviewer = requestedSays(requested.reviewer, "reviewer", now);
103103 const listed = asked(url);
104104 const shown = listed === null ? undefined : out.get(listed.key);
105 return html`<p id="bottleneck" data-stage="${worst?.key ?? ""}">
106 <strong>${verdict(stages, now)}</strong>
107 </p>
105 return html`${card(
106 html`<p id="bottleneck" data-stage="${worst?.key ?? ""}">
107 <strong>${verdict(stages, now)}</strong>
108 </p>
108109 ${conflicted === null ? "" : html`<p id="conflicted">${conflicted}</p>`}
109110 ${onBuilder === null ? "" : html`<p id="requested-builder">${onBuilder}</p>`}
110111 ${onReviewer === null ? "" : html`<p id="requested-reviewer">${onReviewer}</p>`}
@@ -127,7 +128,9 @@ export function flowPanel(app: App, reader: Reader, url: URL, base: string): Htm
127128 <p class="muted small">
128129 Limits are set on <a href="/settings/capacity">Capacity</a>. How the forge stands against its threats is on
129130 <a href="/resilience">Resilience</a>; where an outside agent can help is on <a href="/contribute">Contribute</a>.
130 </p>
131 </p>`,
132 { id: "flow-summary" },
133 )}
131134 ${card(
132135 html`<div class="table-scroll">
133136 <table class="table" aria-label="Pipeline stages">
@@ -154,7 +157,7 @@ export function flowPanel(app: App, reader: Reader, url: URL, base: string): Htm
154157 <div class="muted small">${STAGE_HOLDS[s.key]}</div>
155158 </th>
156159 <td class="mono">${String(s.waiting)}</td>
157 <td class="mono">${String(s.active)}</td>
160 <td class="mono">${s.key === "live" ? "—" : String(s.active)}</td>
158161 <td class="mono">${waited(s, now) ?? "—"}</td>
159162 <td class="small">${s.capacity ?? "—"}</td>
160163 <td>${outCell(base, out.get(s.key)!)}</td>
src/web/pages/map.ts +6 −6
@@ -1079,12 +1079,12 @@ export const mapCss = /* css */ `
10791079 .map-node[data-state="deferred"] .map-card { fill: none; stroke-dasharray: 4 3; }
10801080 .map-node[data-state="deferred"] .map-title { fill: var(--muted-fg); }
10811081 .map-node[data-state="blocked"] .map-title { fill: var(--muted-fg); }
1082 .map-meta { font-size: 11.5px; fill: var(--faint); font-variant-numeric: tabular-nums; }
1082 .map-meta { font-size: var(--fs-min); fill: var(--faint); font-variant-numeric: tabular-nums; }
10831083 .map-who circle { fill: var(--muted); stroke: var(--border); }
1084 .map-who-i { font-size: 11.5px; font-weight: 650; text-transform: uppercase; fill: var(--fg); }
1085 .map-who-n { font-size: 11.5px; fill: var(--muted-fg); }
1086 .map-word { font-size: 11.5px; font-weight: 550; fill: var(--muted-fg); }
1087 .map-word .map-dot { font-size: 11.5px; fill: var(--faint); }
1084 .map-who-i { font-size: var(--fs-min); font-weight: 650; text-transform: uppercase; fill: var(--fg); }
1085 .map-who-n { font-size: var(--fs-min); fill: var(--muted-fg); }
1086 .map-word { font-size: var(--fs-min); font-weight: 550; fill: var(--muted-fg); }
1087 .map-word .map-dot { font-size: var(--fs-min); fill: var(--faint); }
10881088 .map-node[data-state="ready"] .map-dot { fill: var(--success); }
10891089 .map-node[data-state="working"] .map-dot { fill: var(--accent); }
10901090 .map-node[data-state="working"] .map-word { fill: var(--accent); }
@@ -1100,7 +1100,7 @@ export const mapCss = /* css */ `
11001100 .map-edge-crit { stroke: var(--accent); }
11011101 .map-edge-done { opacity: .45; }
11021102 .map-rule line { stroke: var(--border); stroke-width: 1; }
1103 .map-rule text { font-size: 11.5px; fill: var(--faint); letter-spacing: .02em; }
1103 .map-rule text { font-size: var(--fs-min); fill: var(--faint); letter-spacing: .02em; }
11041104
11051105 /* ---- the line under a drawing ---- */
11061106 .map-counts { display: flex; flex-wrap: wrap; gap: 4px 14px; margin: 8px 2px 0; font-size: 12.5px; color: var(--muted-fg); }
src/web/pages/system.ts +2 −2
@@ -76,10 +76,10 @@ function memorySection(ctx: Ctx): Html {
7676 return hours.length === 0
7777 ? empty({ icon: "activity", title: "No samples yet", note: "The forge samples its memory every minute; the first hour appears shortly." })
7878 : html`<div class="sys-pair">${series("RSS (MiB, hourly mean)", (h) => h.rssMean)}${series("JS heap (MiB, hourly mean)", (h) => h.heapMean)}</div>
79 <table class="table" aria-label="Forge memory by hour">
79 <div class="table-scroll"><table class="table" aria-label="Forge memory by hour">
8080 <thead><tr><th scope="col">Hour (UTC)</th><th scope="col" class="num">RSS mean</th><th scope="col" class="num">RSS max</th><th scope="col" class="num">Heap mean</th><th scope="col" class="num">Heap max</th></tr></thead>
8181 <tbody>${hours.map((h) => html`<tr><td class="mono">${hhmm(h.at)}</td><td class="num mono">${mib(h.rssMean)}</td><td class="num mono">${mib(h.rssMax)}</td><td class="num mono">${mib(h.heapMean)}</td><td class="num mono">${mib(h.heapMax)}</td></tr>`)}</tbody>
82 </table>`;
82 </table></div>`;
8383 }
8484
8585 /**
src/web/pages/thread.ts +2 −2
@@ -984,7 +984,7 @@ export function dependenciesCard(
984984 (i.closeReason === "dropped" || i.closeReason === "split")
985985 ? html`<form method="post" action="${action}" class="inline">
986986 <input type="hidden" name="confirm" value="${i.number}" />
987 <button class="btn btn-ghost btn-xs" type="submit">
987 <button class="btn" data-variant="ghost" data-size="sm" type="submit">
988988 Still holds
989989 </button>
990990 </form>`
@@ -993,7 +993,7 @@ export function dependenciesCard(
993993 ? html`<form method="post" action="${action}" class="inline">
994994 <input type="hidden" name="unlink" value="${i.number}" />
995995 <button
996 class="btn btn-ghost btn-xs"
996 class="btn" data-variant="ghost" data-size="sm"
997997 type="submit"
998998 aria-label="No longer waits for #${i.number}"
999999 >
src/web/studio/asks_page.ts +2 −2
@@ -255,7 +255,7 @@ function mutableRow(ctx: Ctx, r: AskRow<Decision>, picked: ReadonlySet<string>):
255255 <input type="hidden" name="repo" value="${r.repo.id}" />
256256 <button
257257 type="submit"
258 class="btn btn-sm btn-ghost"
258 class="btn" data-variant="ghost" data-size="sm"
259259 title="Stop its asks reaching your home, inbox and phone"
260260 >Mute ${r.repo.ownerName}/${r.repo.name}</button
261261 >
@@ -408,7 +408,7 @@ export function needsYouSection(
408408 <span class="muted small" title="${silenced.map((r) => `${r.ownerName}/${r.name}`).join(", ")}"
409409 >${silenced.length} muted ${silenced.length === 1 ? "repository still asks" : "repositories still ask"}</span
410410 >
411 <button type="submit" class="btn btn-sm btn-ghost">Listen again</button>
411 <button type="submit" class="btn" data-variant="ghost" data-size="sm">Listen again</button>
412412 </form>`;
413413 const aside = putAsideWaves(ctx.app.db, user);
414414 const asideNote =
src/web/studio/explore.ts +1 −1
@@ -116,7 +116,7 @@ export async function explorePage(ctx: Ctx): Promise<Response> {
116116 })),
117117 )}
118118 <a
119 class="btn btn-sm"
119 class="btn" data-size="sm"
120120 href="${href({ ...query, archived: !query.archived })}"
121121 aria-pressed="${query.archived ? "true" : "false"}"
122122 >Show archived</a
src/web/studio/feed.ts +10 −4
@@ -80,8 +80,8 @@ export function feedCard(
8080 ${btn(target ? "Steer" : "Tell", { size: "sm", variant: "primary" })}
8181 </form>
8282 <span class="bar">
83 <button type="button" class="btn btn-sm btn-ghost" data-feed="dismissed" title="Dismiss (x)">Dismiss</button>
84 ${item.kind === "ask" || item.kind === "milestone" || item.kind === "choose" ? null : html`<button type="button" class="btn btn-sm btn-ghost" data-feed="muted" title="Mute ${item.app}">Mute app</button>`}
83 <button type="button" class="btn" data-variant="ghost" data-size="sm" data-feed="dismissed" title="Dismiss (x)">Dismiss</button>
84 ${item.kind === "ask" || item.kind === "milestone" || item.kind === "choose" ? null : html`<button type="button" class="btn" data-variant="ghost" data-size="sm" data-feed="muted" title="Mute ${item.app}">Mute app</button>`}
8585 </span>
8686 </details>`;
8787 return card(body, {
@@ -219,7 +219,7 @@ function chipsBar(user: User, ctx: Ctx, items: readonly FeedItem[]): Html {
219219 const chip = (kind: string, value: string, label: string, shown: boolean): Html =>
220220 html`<button
221221 type="button"
222 class="btn btn-sm btn-ghost feed-chip"
222 class="btn feed-chip" data-variant="ghost" data-size="sm"
223223 data-chip="${kind}"
224224 data-value="${value}"
225225 aria-pressed="${String(shown)}"
@@ -297,9 +297,12 @@ ${outboxJs}
297297 var n = outbox ? outbox.size() : 0;
298298 if (n > 0) parts.push(n + (n === 1 ? ' act is' : ' acts are') + ' waiting to send.');
299299 if (problem) parts.push(problem);
300 if (said) parts.unshift(said);
300301 note.textContent = parts.join(' ');
301302 note.hidden = parts.length === 0;
302303 }
304 var said = '';
305 function say(t) { said = t; paint(); setTimeout(function () { if (said === t) { said = ''; paint(); } }, 4000); }
303306 function flush() {
304307 if (!outbox) return;
305308 outbox.flush().then(paint);
@@ -321,7 +324,10 @@ ${outboxJs}
321324 if (next) next.focus();
322325 }
323326 // Acting is instant: the card goes, the act is kept and sent.
324 function act(card, kind) { if (send(card, kind)) gone(card); }
327 function act(card, kind) {
328 var app = card.getAttribute('data-app');
329 if (send(card, kind)) { gone(card); say((kind === 'muted' ? 'Muted ' + app + '.' : 'Dismissed.') ); }
330 }
325331 // A chip flips at once; the saved filter then redraws the region in place
326332 // (the same fragment the live region pulls), keeping scroll and focus. A
327333 // refusal puts the chip back and says why in the note.
src/web/studio/home.ts +1 −1
@@ -125,7 +125,7 @@ function yourApps(ctx: Ctx, user: User): Html {
125125 )}
126126 ${when(
127127 user.isAdmin && rows.length > 0,
128 () => html`<label class="btn btn-sm" for="apps-select">Select</label>
128 () => html`<label class="btn" data-size="sm" for="apps-select">Select</label>
129129 <span class="home-pick">${btn("Archive", { size: "sm" })}</span>`,
130130 )}
131131 </div>`;
src/web/style/base.ts +1
@@ -26,6 +26,7 @@ export const base = /* css */ `
2626 --radius-sm: 6px; /* controls: buttons, inputs, chips, kbd */
2727 --radius: 8px; /* surfaces: cards, panels, code blocks */
2828 --radius-lg: 12px; /* overlays: dialogs, palette, menus */
29 --fs-min: 12px; /* type floor: no meaningful text is smaller */
2930 --topbar-h: 56px;
3031 --page: 1120px;
3132 --measure: 72ch;
src/web/style/components.ts +15 −12
@@ -45,6 +45,9 @@ export const components = /* css */ `
4545 .btn[data-variant="outline"] { background: transparent; box-shadow: none; }
4646 .btn[data-variant="ghost"] { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted-fg); }
4747 .btn[data-variant="ghost"]:hover { background: var(--muted); color: var(--fg); border-color: transparent; }
48 /* A toggle shows its state: the chip, the sort button. */
49 .btn[aria-pressed="true"] { background: var(--muted); border-color: var(--border-strong); color: var(--fg); font-weight: 550; }
50 .btn[aria-pressed="false"] { color: var(--muted-fg); }
4851 .btn[data-variant="danger"] { color: var(--danger); }
4952 .btn[data-variant="danger"]:hover {
5053 border-color: color-mix(in srgb, var(--danger) 45%, var(--border));
@@ -153,7 +156,7 @@ fieldset.field > .check + .check { margin-top: 8px; }
153156 .badge {
154157 display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px;
155158 border: 1px solid var(--border); border-radius: 999px;
156 font-size: 11.5px; font-weight: 500; line-height: 18px; color: var(--muted-fg);
159 font-size: var(--fs-min); font-weight: 500; line-height: 18px; color: var(--muted-fg);
157160 font-variant-numeric: tabular-nums; white-space: nowrap;
158161 }
159162 .badge[data-variant="secondary"] { background: var(--muted); border-color: transparent; }
@@ -192,7 +195,7 @@ fieldset.field > .check + .check { margin-top: 8px; }
192195 .label {
193196 --label: var(--muted-fg);
194197 display: inline-flex; align-items: center; padding: 0 8px;
195 border-radius: 999px; font-size: 11.5px; font-weight: 500; line-height: 18px;
198 border-radius: 999px; font-size: var(--fs-min); font-weight: 500; line-height: 18px;
196199 color: color-mix(in srgb, var(--label) 78%, var(--fg));
197200 background: color-mix(in srgb, var(--label) 14%, transparent);
198201 border: 1px solid color-mix(in srgb, var(--label) 32%, transparent);
@@ -204,7 +207,7 @@ a.label:hover { color: color-mix(in srgb, var(--label) 90%, var(--fg)); }
204207 display: inline-flex; align-items: center; justify-content: center;
205208 min-width: 20px; padding: 0 6px; border-radius: 999px;
206209 background: var(--muted); color: var(--muted-fg);
207 font-size: 11.5px; font-weight: 550; line-height: 18px;
210 font-size: var(--fs-min); font-weight: 550; line-height: 18px;
208211 font-variant-numeric: tabular-nums;
209212 }
210213 .pill[data-tone="accent"] { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); }
@@ -255,7 +258,7 @@ kbd {
255258 min-width: 20px; height: 20px; padding: 0 5px;
256259 border: 1px solid var(--border); border-bottom-width: 2px;
257260 border-radius: var(--radius-sm); background: var(--muted);
258 font-family: var(--mono); font-size: 11.5px; font-weight: 500; color: var(--muted-fg);
261 font-family: var(--mono); font-size: var(--fs-min); font-weight: 500; color: var(--muted-fg);
259262 }
260263
261264 .sha {
@@ -274,7 +277,7 @@ a.sha:hover { color: var(--accent); }
274277 background: color-mix(in srgb, hsl(var(--hue, 220) 62% 52%) 22%, var(--card));
275278 color: color-mix(in srgb, hsl(var(--hue, 220) 62% 42%) 88%, var(--fg));
276279 border: 1px solid color-mix(in srgb, hsl(var(--hue, 220) 62% 52%) 30%, transparent);
277 font-size: 9.5px; font-weight: 650; letter-spacing: 0; text-transform: uppercase;
280 font-size: var(--fs-min); font-weight: 650; letter-spacing: 0; text-transform: uppercase;
278281 user-select: none;
279282 }
280283 .avatar[data-size="md"] { width: 28px; height: 28px; font-size: 12.5px; }
@@ -284,7 +287,7 @@ a.sha:hover { color: var(--accent); }
284287 .byline a:hover { color: var(--accent); }
285288 .name-mark {
286289 display: inline-block; padding: 0 5px; border-radius: 4px;
287 font-size: 11.5px; font-weight: 600; line-height: 15px; letter-spacing: 0.04em;
290 font-size: var(--fs-min); font-weight: 600; line-height: 15px; letter-spacing: 0.04em;
288291 text-transform: uppercase; white-space: nowrap; vertical-align: 1px;
289292 color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent);
290293 }
@@ -363,7 +366,7 @@ a.row:hover .row-title { color: var(--accent); }
363366 font-variant-numeric: tabular-nums;
364367 }
365368 .row-meta .byline { display: inline; }
366 .row-meta .avatar { width: 16px; height: 16px; font-size: 8.5px; vertical-align: -0.28em; }
369 .row-meta .avatar { width: 16px; height: 16px; font-size: var(--fs-min); vertical-align: -0.28em; }
367370 .row-meta .byline-name { margin-left: 4px; }
368371 .row-lead { color: var(--faint); display: inline-flex; flex: none; }
369372 .row-trail { display: flex; align-items: center; gap: 8px; flex: none; }
@@ -457,7 +460,7 @@ a.row:hover .row-title { color: var(--accent); }
457460
458461 /* ---- tabs (navigation links, never role=tab) ---- */
459462 .tabs {
460 display: flex; gap: 2px; align-items: stretch;
463 min-width: 0; max-width: 100%; display: flex; gap: 2px; align-items: stretch;
461464 border-bottom: 1px solid var(--border);
462465 overflow-x: auto; scrollbar-width: none;
463466 scroll-snap-type: x proximity;
@@ -523,7 +526,7 @@ a.row:hover .row-title { color: var(--accent); }
523526 @keyframes pop { from { opacity: 0; transform: translateY(-4px); } }
524527 .menu-title {
525528 display: block; padding: 6px 10px 4px;
526 font-size: 11.5px; font-weight: 600; letter-spacing: .08em;
529 font-size: var(--fs-min); font-weight: 600; letter-spacing: .08em;
527530 text-transform: uppercase; color: var(--faint);
528531 }
529532 .menu-item {
@@ -635,7 +638,7 @@ a.row:hover .row-title { color: var(--accent); }
635638 .palette-group + .palette-group { margin-top: 4px; }
636639 .palette-heading {
637640 padding: 8px 10px 4px;
638 font-size: 11.5px; font-weight: 600; letter-spacing: .08em;
641 font-size: var(--fs-min); font-weight: 600; letter-spacing: .08em;
639642 text-transform: uppercase; color: var(--faint);
640643 }
641644 .palette-item {
@@ -770,9 +773,9 @@ a.row:hover .row-title { color: var(--accent); }
770773 .card.empty { border-style: dashed; box-shadow: none; }
771774
772775 /* ---- settings shell ---- */
773 .settings { display: grid; gap: 20px; margin-top: 16px; }
776 .settings { display: grid; grid-template-columns: minmax(0, 1fr); gap: 20px; margin-top: 16px; }
774777 .settings-nav {
775 display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none;
778 min-width: 0; display: flex; gap: 2px; overflow-x: auto; scrollbar-width: none;
776779 border-bottom: 1px solid var(--border);
777780 }
778781 .settings-nav::-webkit-scrollbar { display: none; }
src/web/style/content.ts +8 −6
@@ -69,7 +69,7 @@ a.where-place:hover, button.where-place:hover {
6969 .where-place .ico { color: var(--faint); flex: none; }
7070 /* A thumb needs more than a 12px word to land on. */
7171 @media (pointer: coarse) { .where-place { min-height: 32px; padding: 4px 8px; } }
72 .where-dot { font-size: 11.5px; line-height: 1; }
72 .where-dot { font-size: var(--fs-min); line-height: 1; }
7373 .where-note { color: var(--faint); }
7474 /* Accent is interaction everywhere else in the product; here it is the one
7575 place a page is *reading from*, which is the page's own subject. */
@@ -312,7 +312,7 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
312312 /* A fork's relationship to what it follows: one sentence, one action. */
313313 .following { margin-top: 12px; }
314314 .following-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
315 .following-eyebrow { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: 11.5px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-fg); }
315 .following-eyebrow { display: inline-flex; align-items: center; gap: 6px; font-family: var(--mono); font-size: var(--fs-min); font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted-fg); }
316316 .following-eyebrow a { color: var(--fg); text-transform: none; letter-spacing: 0; font-family: var(--font); font-size: 13px; font-weight: 600; }
317317 .following-sentence { margin: 8px 0 0; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; text-wrap: balance; }
318318 .following-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
@@ -387,7 +387,9 @@ a.origin-chip:hover { border-color: var(--border-strong); color: var(--fg); }
387387 .bt-bar::-moz-meter-bar { background: var(--accent); }
388388 .bt-bar:-moz-meter-sub-optimum::-moz-meter-bar { background: var(--accent); }
389389 .bt-bar:-moz-meter-sub-sub-optimum::-moz-meter-bar { background: var(--danger); }
390 .table-wrap { overflow-x: auto; }
390 .table-wrap { overflow-x: auto; max-width: 100%; }
391 #summary { display: flex; flex-wrap: wrap; gap: 6px; }
392 #summary .pill { max-width: 100%; white-space: normal; }
391393 .bt-procs { font-size: 12.5px; }
392394 .bt-procs th, .bt-procs td { padding: 2px 8px; white-space: nowrap; }
393395 .bt-group th, .bt-group td { background: var(--muted); font-weight: 600; }
@@ -435,7 +437,7 @@ form.inline { display: inline; margin-left: auto; }
435437 }
436438 .lineage-children .row { padding-left: 12px; }
437439 .lineage-here {
438 margin-left: 6px; font-family: var(--mono); font-size: 11.5px; font-weight: 500;
440 margin-left: 6px; font-family: var(--mono); font-size: var(--fs-min); font-weight: 500;
439441 letter-spacing: 0.04em; color: var(--accent);
440442 }
441443 .lineage .row[aria-current="page"] { background: var(--muted); }
@@ -830,7 +832,7 @@ form.inline { display: inline; margin-left: auto; }
830832 .check-progress[data-state="queued"] .pinned-slot, .check-progress[data-state="running"] .pinned-slot { height: 9em; }
831833 .pinned-failures { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; }
832834 .pinned-failures li { padding: 6px 12px; border: 1px solid var(--danger); border-radius: var(--radius); }
833 .pinned-failures pre { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: 0.85em; }
835 .pinned-failures pre { margin: 4px 0 0; white-space: pre-wrap; overflow-wrap: anywhere; font-size: var(--fs-min); }
834836 @media (max-width: 480px) { .check-steps { grid-template-columns: minmax(0, 1fr); } }
835837 .check-steps .step-state { color: var(--faint); }
836838 .check-steps [data-state="ok"] .step-state { color: var(--success); }
@@ -1038,7 +1040,7 @@ form.inline { display: inline; margin-left: auto; }
10381040 .finder { width: min(720px, calc(100vw - 24px)); }
10391041 @media (max-width: 719px) { .finder { width: 100%; max-width: 100%; } }
10401042 .finder .palette-item .cat {
1041 font-family: var(--mono); font-size: 11.5px;
1043 font-family: var(--mono); font-size: var(--fs-min);
10421044 overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 45%;
10431045 }
10441046 .finder .palette-item .grow { font-family: var(--mono); font-size: 12.5px; }
test/ui-sweep.test.ts added +63
@@ -0,0 +1,63 @@
1 /**
2 * The UI sweep (#3698): the type floor is a token, the phone never loses the
3 * create menu, the settings strip and the system tables stay in their boxes,
4 * and the Live row does not call its 24 h count "active". Source-level: the
5 * sandbox has no browser, so the pixel checks at 1440/1024/390 stay with the
6 * audit.
7 */
8 import { describe, expect, test } from "bun:test";
9 import { readFileSync } from "node:fs";
10 import { feedJs } from "../src/web/studio/feed.ts";
11 import { css } from "../src/web/style.ts";
12 import { mapCss } from "../src/web/pages/map.ts";
13
14 const read = (p: string): string => readFileSync(new URL(`../${p}`, import.meta.url), "utf8");
15
16 describe("type floor", () => {
17 test("no declared font-size is under 12px", () => {
18 for (const sheet of [css, mapCss]) {
19 for (const m of sheet.matchAll(/font-size:\s*([0-9.]+)px/g)) expect(Number(m[1])).toBeGreaterThanOrEqual(12);
20 expect(sheet).not.toMatch(/font-size:\s*0?\.[0-9]+(em|rem)/);
21 }
22 });
23 test("the floor is one token", () => {
24 expect(css).toContain("--fs-min: 12px");
25 });
26 });
27
28 describe("layout", () => {
29 test("the + menu is not hidden on a phone", () => {
30 expect(read("src/web/layout.ts")).not.toContain('<span class="hide-sm">${newMenu(ctx)}');
31 });
32 test("settings grid and tab strips may shrink below their content", () => {
33 expect(css).toMatch(/\.settings \{[^}]*grid-template-columns: minmax\(0, 1fr\)/);
34 expect(css).toMatch(/\.settings-nav \{\s*min-width: 0/);
35 expect(css).toMatch(/\.tabs \{\s*min-width: 0/);
36 });
37 test("the system page's pills wrap and its tables scroll inside a box", () => {
38 expect(css).toContain("#summary { display: flex; flex-wrap: wrap");
39 expect(read("src/web/pages/system.ts")).toContain('<div class="table-scroll"><table class="table" aria-label="Forge memory by hour">');
40 });
41 test("a pressed button looks pressed", () => {
42 expect(css).toContain('.btn[aria-pressed="true"]');
43 });
44 });
45
46 describe("flow panel", () => {
47 const src = read("src/web/pages/flow-panel.ts");
48 test("the summary sits in a card, and Live shows no 'active' count", () => {
49 expect(src).toContain('id: "flow-summary"');
50 expect(src).toContain('s.key === "live" ? "—"');
51 });
52 });
53
54 describe("home feed", () => {
55 test("acting says what it did", () => {
56 expect(feedJs).toContain("Dismissed.");
57 expect(feedJs).toContain("'Muted '");
58 });
59 test("no button uses a class the stylesheet lacks", () => {
60 for (const f of ["src/web/studio/feed.ts", "src/web/studio/asks_page.ts", "src/web/studio/home.ts", "src/web/pages/thread.ts"])
61 expect(read(f)).not.toMatch(/btn-(sm|ghost|xs)/);
62 });
63 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs