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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3698-ui-sweep-the-audit-s-defects-on-boards-s → main 1 commit fork
13 files
changed +121
−44
src/web/layout.ts +2 −3
| @@ -205,8 +205,7 @@ function userMenu( | ||
| 205 | 205 | { label: "Explore", href: "/explore", icon: "search" }, |
| 206 | 206 | { label: "Map", href: "/map", icon: "map" }, |
| 207 | 207 | { 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. | |
| 210 | 209 | { label: "New app", href: "/apps/new", icon: "rocket" }, |
| 211 | 210 | { label: "New repository", href: "/new", icon: "repo" }, |
| 212 | 211 | { label: "Migrate", href: "/migrate", icon: "download" }, |
| @@ -450,7 +449,7 @@ function topbar( | ||
| 450 | 449 | ${searchBar()} |
| 451 | 450 | ${user |
| 452 | 451 | ? 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( | |
| 454 | 453 | ctx, |
| 455 | 454 | user, |
| 456 | 455 | 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 | ||
| 102 | 102 | const onReviewer = requestedSays(requested.reviewer, "reviewer", now); |
| 103 | 103 | const listed = asked(url); |
| 104 | 104 | 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> | |
| 108 | 109 | ${conflicted === null ? "" : html`<p id="conflicted">${conflicted}</p>`} |
| 109 | 110 | ${onBuilder === null ? "" : html`<p id="requested-builder">${onBuilder}</p>`} |
| 110 | 111 | ${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 | ||
| 127 | 128 | <p class="muted small"> |
| 128 | 129 | Limits are set on <a href="/settings/capacity">Capacity</a>. How the forge stands against its threats is on |
| 129 | 130 | <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 | )} | |
| 131 | 134 | ${card( |
| 132 | 135 | html`<div class="table-scroll"> |
| 133 | 136 | <table class="table" aria-label="Pipeline stages"> |
| @@ -154,7 +157,7 @@ export function flowPanel(app: App, reader: Reader, url: URL, base: string): Htm | ||
| 154 | 157 | <div class="muted small">${STAGE_HOLDS[s.key]}</div> |
| 155 | 158 | </th> |
| 156 | 159 | <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> | |
| 158 | 161 | <td class="mono">${waited(s, now) ?? "—"}</td> |
| 159 | 162 | <td class="small">${s.capacity ?? "—"}</td> |
| 160 | 163 | <td>${outCell(base, out.get(s.key)!)}</td> |
src/web/pages/map.ts +6 −6
| @@ -1079,12 +1079,12 @@ export const mapCss = /* css */ ` | ||
| 1079 | 1079 | .map-node[data-state="deferred"] .map-card { fill: none; stroke-dasharray: 4 3; } |
| 1080 | 1080 | .map-node[data-state="deferred"] .map-title { fill: var(--muted-fg); } |
| 1081 | 1081 | .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; } | |
| 1083 | 1083 | .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); } | |
| 1088 | 1088 | .map-node[data-state="ready"] .map-dot { fill: var(--success); } |
| 1089 | 1089 | .map-node[data-state="working"] .map-dot { fill: var(--accent); } |
| 1090 | 1090 | .map-node[data-state="working"] .map-word { fill: var(--accent); } |
| @@ -1100,7 +1100,7 @@ export const mapCss = /* css */ ` | ||
| 1100 | 1100 | .map-edge-crit { stroke: var(--accent); } |
| 1101 | 1101 | .map-edge-done { opacity: .45; } |
| 1102 | 1102 | .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; } | |
| 1104 | 1104 | |
| 1105 | 1105 | /* ---- the line under a drawing ---- */ |
| 1106 | 1106 | .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 { | ||
| 76 | 76 | return hours.length === 0 |
| 77 | 77 | ? empty({ icon: "activity", title: "No samples yet", note: "The forge samples its memory every minute; the first hour appears shortly." }) |
| 78 | 78 | : 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"> | |
| 80 | 80 | <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> |
| 81 | 81 | <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>`; | |
| 83 | 83 | } |
| 84 | 84 | |
| 85 | 85 | /** |
src/web/pages/thread.ts +2 −2
| @@ -984,7 +984,7 @@ export function dependenciesCard( | ||
| 984 | 984 | (i.closeReason === "dropped" || i.closeReason === "split") |
| 985 | 985 | ? html`<form method="post" action="${action}" class="inline"> |
| 986 | 986 | <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"> | |
| 988 | 988 | Still holds |
| 989 | 989 | </button> |
| 990 | 990 | </form>` |
| @@ -993,7 +993,7 @@ export function dependenciesCard( | ||
| 993 | 993 | ? html`<form method="post" action="${action}" class="inline"> |
| 994 | 994 | <input type="hidden" name="unlink" value="${i.number}" /> |
| 995 | 995 | <button |
| 996 | class="btn btn-ghost btn-xs" | |
| 996 | class="btn" data-variant="ghost" data-size="sm" | |
| 997 | 997 | type="submit" |
| 998 | 998 | aria-label="No longer waits for #${i.number}" |
| 999 | 999 | > |
src/web/studio/asks_page.ts +2 −2
| @@ -255,7 +255,7 @@ function mutableRow(ctx: Ctx, r: AskRow<Decision>, picked: ReadonlySet<string>): | ||
| 255 | 255 | <input type="hidden" name="repo" value="${r.repo.id}" /> |
| 256 | 256 | <button |
| 257 | 257 | type="submit" |
| 258 | class="btn btn-sm btn-ghost" | |
| 258 | class="btn" data-variant="ghost" data-size="sm" | |
| 259 | 259 | title="Stop its asks reaching your home, inbox and phone" |
| 260 | 260 | >Mute ${r.repo.ownerName}/${r.repo.name}</button |
| 261 | 261 | > |
| @@ -408,7 +408,7 @@ export function needsYouSection( | ||
| 408 | 408 | <span class="muted small" title="${silenced.map((r) => `${r.ownerName}/${r.name}`).join(", ")}" |
| 409 | 409 | >${silenced.length} muted ${silenced.length === 1 ? "repository still asks" : "repositories still ask"}</span |
| 410 | 410 | > |
| 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> | |
| 412 | 412 | </form>`; |
| 413 | 413 | const aside = putAsideWaves(ctx.app.db, user); |
| 414 | 414 | const asideNote = |
src/web/studio/explore.ts +1 −1
| @@ -116,7 +116,7 @@ export async function explorePage(ctx: Ctx): Promise<Response> { | ||
| 116 | 116 | })), |
| 117 | 117 | )} |
| 118 | 118 | <a |
| 119 | class="btn btn-sm" | |
| 119 | class="btn" data-size="sm" | |
| 120 | 120 | href="${href({ ...query, archived: !query.archived })}" |
| 121 | 121 | aria-pressed="${query.archived ? "true" : "false"}" |
| 122 | 122 | >Show archived</a |
src/web/studio/feed.ts +10 −4
| @@ -80,8 +80,8 @@ export function feedCard( | ||
| 80 | 80 | ${btn(target ? "Steer" : "Tell", { size: "sm", variant: "primary" })} |
| 81 | 81 | </form> |
| 82 | 82 | <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>`} | |
| 85 | 85 | </span> |
| 86 | 86 | </details>`; |
| 87 | 87 | return card(body, { |
| @@ -219,7 +219,7 @@ function chipsBar(user: User, ctx: Ctx, items: readonly FeedItem[]): Html { | ||
| 219 | 219 | const chip = (kind: string, value: string, label: string, shown: boolean): Html => |
| 220 | 220 | html`<button |
| 221 | 221 | type="button" |
| 222 | class="btn btn-sm btn-ghost feed-chip" | |
| 222 | class="btn feed-chip" data-variant="ghost" data-size="sm" | |
| 223 | 223 | data-chip="${kind}" |
| 224 | 224 | data-value="${value}" |
| 225 | 225 | aria-pressed="${String(shown)}" |
| @@ -297,9 +297,12 @@ ${outboxJs} | ||
| 297 | 297 | var n = outbox ? outbox.size() : 0; |
| 298 | 298 | if (n > 0) parts.push(n + (n === 1 ? ' act is' : ' acts are') + ' waiting to send.'); |
| 299 | 299 | if (problem) parts.push(problem); |
| 300 | if (said) parts.unshift(said); | |
| 300 | 301 | note.textContent = parts.join(' '); |
| 301 | 302 | note.hidden = parts.length === 0; |
| 302 | 303 | } |
| 304 | var said = ''; | |
| 305 | function say(t) { said = t; paint(); setTimeout(function () { if (said === t) { said = ''; paint(); } }, 4000); } | |
| 303 | 306 | function flush() { |
| 304 | 307 | if (!outbox) return; |
| 305 | 308 | outbox.flush().then(paint); |
| @@ -321,7 +324,10 @@ ${outboxJs} | ||
| 321 | 324 | if (next) next.focus(); |
| 322 | 325 | } |
| 323 | 326 | // 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 | } | |
| 325 | 331 | // A chip flips at once; the saved filter then redraws the region in place |
| 326 | 332 | // (the same fragment the live region pulls), keeping scroll and focus. A |
| 327 | 333 | // 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 { | ||
| 125 | 125 | )} |
| 126 | 126 | ${when( |
| 127 | 127 | 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> | |
| 129 | 129 | <span class="home-pick">${btn("Archive", { size: "sm" })}</span>`, |
| 130 | 130 | )} |
| 131 | 131 | </div>`; |
src/web/style/base.ts +1
| @@ -26,6 +26,7 @@ export const base = /* css */ ` | ||
| 26 | 26 | --radius-sm: 6px; /* controls: buttons, inputs, chips, kbd */ |
| 27 | 27 | --radius: 8px; /* surfaces: cards, panels, code blocks */ |
| 28 | 28 | --radius-lg: 12px; /* overlays: dialogs, palette, menus */ |
| 29 | --fs-min: 12px; /* type floor: no meaningful text is smaller */ | |
| 29 | 30 | --topbar-h: 56px; |
| 30 | 31 | --page: 1120px; |
| 31 | 32 | --measure: 72ch; |
src/web/style/components.ts +15 −12
| @@ -45,6 +45,9 @@ export const components = /* css */ ` | ||
| 45 | 45 | .btn[data-variant="outline"] { background: transparent; box-shadow: none; } |
| 46 | 46 | .btn[data-variant="ghost"] { background: transparent; border-color: transparent; box-shadow: none; color: var(--muted-fg); } |
| 47 | 47 | .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); } | |
| 48 | 51 | .btn[data-variant="danger"] { color: var(--danger); } |
| 49 | 52 | .btn[data-variant="danger"]:hover { |
| 50 | 53 | border-color: color-mix(in srgb, var(--danger) 45%, var(--border)); |
| @@ -153,7 +156,7 @@ fieldset.field > .check + .check { margin-top: 8px; } | ||
| 153 | 156 | .badge { |
| 154 | 157 | display: inline-flex; align-items: center; gap: 4px; padding: 1px 8px; |
| 155 | 158 | 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); | |
| 157 | 160 | font-variant-numeric: tabular-nums; white-space: nowrap; |
| 158 | 161 | } |
| 159 | 162 | .badge[data-variant="secondary"] { background: var(--muted); border-color: transparent; } |
| @@ -192,7 +195,7 @@ fieldset.field > .check + .check { margin-top: 8px; } | ||
| 192 | 195 | .label { |
| 193 | 196 | --label: var(--muted-fg); |
| 194 | 197 | 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; | |
| 196 | 199 | color: color-mix(in srgb, var(--label) 78%, var(--fg)); |
| 197 | 200 | background: color-mix(in srgb, var(--label) 14%, transparent); |
| 198 | 201 | 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)); } | ||
| 204 | 207 | display: inline-flex; align-items: center; justify-content: center; |
| 205 | 208 | min-width: 20px; padding: 0 6px; border-radius: 999px; |
| 206 | 209 | 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; | |
| 208 | 211 | font-variant-numeric: tabular-nums; |
| 209 | 212 | } |
| 210 | 213 | .pill[data-tone="accent"] { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); } |
| @@ -255,7 +258,7 @@ kbd { | ||
| 255 | 258 | min-width: 20px; height: 20px; padding: 0 5px; |
| 256 | 259 | border: 1px solid var(--border); border-bottom-width: 2px; |
| 257 | 260 | 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); | |
| 259 | 262 | } |
| 260 | 263 | |
| 261 | 264 | .sha { |
| @@ -274,7 +277,7 @@ a.sha:hover { color: var(--accent); } | ||
| 274 | 277 | background: color-mix(in srgb, hsl(var(--hue, 220) 62% 52%) 22%, var(--card)); |
| 275 | 278 | color: color-mix(in srgb, hsl(var(--hue, 220) 62% 42%) 88%, var(--fg)); |
| 276 | 279 | 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; | |
| 278 | 281 | user-select: none; |
| 279 | 282 | } |
| 280 | 283 | .avatar[data-size="md"] { width: 28px; height: 28px; font-size: 12.5px; } |
| @@ -284,7 +287,7 @@ a.sha:hover { color: var(--accent); } | ||
| 284 | 287 | .byline a:hover { color: var(--accent); } |
| 285 | 288 | .name-mark { |
| 286 | 289 | 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; | |
| 288 | 291 | text-transform: uppercase; white-space: nowrap; vertical-align: 1px; |
| 289 | 292 | color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); |
| 290 | 293 | } |
| @@ -363,7 +366,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 363 | 366 | font-variant-numeric: tabular-nums; |
| 364 | 367 | } |
| 365 | 368 | .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; } | |
| 367 | 370 | .row-meta .byline-name { margin-left: 4px; } |
| 368 | 371 | .row-lead { color: var(--faint); display: inline-flex; flex: none; } |
| 369 | 372 | .row-trail { display: flex; align-items: center; gap: 8px; flex: none; } |
| @@ -457,7 +460,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 457 | 460 | |
| 458 | 461 | /* ---- tabs (navigation links, never role=tab) ---- */ |
| 459 | 462 | .tabs { |
| 460 | display: flex; gap: 2px; align-items: stretch; | |
| 463 | min-width: 0; max-width: 100%; display: flex; gap: 2px; align-items: stretch; | |
| 461 | 464 | border-bottom: 1px solid var(--border); |
| 462 | 465 | overflow-x: auto; scrollbar-width: none; |
| 463 | 466 | scroll-snap-type: x proximity; |
| @@ -523,7 +526,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 523 | 526 | @keyframes pop { from { opacity: 0; transform: translateY(-4px); } } |
| 524 | 527 | .menu-title { |
| 525 | 528 | 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; | |
| 527 | 530 | text-transform: uppercase; color: var(--faint); |
| 528 | 531 | } |
| 529 | 532 | .menu-item { |
| @@ -635,7 +638,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 635 | 638 | .palette-group + .palette-group { margin-top: 4px; } |
| 636 | 639 | .palette-heading { |
| 637 | 640 | 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; | |
| 639 | 642 | text-transform: uppercase; color: var(--faint); |
| 640 | 643 | } |
| 641 | 644 | .palette-item { |
| @@ -770,9 +773,9 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 770 | 773 | .card.empty { border-style: dashed; box-shadow: none; } |
| 771 | 774 | |
| 772 | 775 | /* ---- 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; } | |
| 774 | 777 | .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; | |
| 776 | 779 | border-bottom: 1px solid var(--border); |
| 777 | 780 | } |
| 778 | 781 | .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 { | ||
| 69 | 69 | .where-place .ico { color: var(--faint); flex: none; } |
| 70 | 70 | /* A thumb needs more than a 12px word to land on. */ |
| 71 | 71 | @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; } | |
| 73 | 73 | .where-note { color: var(--faint); } |
| 74 | 74 | /* Accent is interaction everywhere else in the product; here it is the one |
| 75 | 75 | 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); } | ||
| 312 | 312 | /* A fork's relationship to what it follows: one sentence, one action. */ |
| 313 | 313 | .following { margin-top: 12px; } |
| 314 | 314 | .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); } | |
| 316 | 316 | .following-eyebrow a { color: var(--fg); text-transform: none; letter-spacing: 0; font-family: var(--font); font-size: 13px; font-weight: 600; } |
| 317 | 317 | .following-sentence { margin: 8px 0 0; font-size: 15px; font-weight: 600; letter-spacing: -0.01em; text-wrap: balance; } |
| 318 | 318 | .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); } | ||
| 387 | 387 | .bt-bar::-moz-meter-bar { background: var(--accent); } |
| 388 | 388 | .bt-bar:-moz-meter-sub-optimum::-moz-meter-bar { background: var(--accent); } |
| 389 | 389 | .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; } | |
| 391 | 393 | .bt-procs { font-size: 12.5px; } |
| 392 | 394 | .bt-procs th, .bt-procs td { padding: 2px 8px; white-space: nowrap; } |
| 393 | 395 | .bt-group th, .bt-group td { background: var(--muted); font-weight: 600; } |
| @@ -435,7 +437,7 @@ form.inline { display: inline; margin-left: auto; } | ||
| 435 | 437 | } |
| 436 | 438 | .lineage-children .row { padding-left: 12px; } |
| 437 | 439 | .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; | |
| 439 | 441 | letter-spacing: 0.04em; color: var(--accent); |
| 440 | 442 | } |
| 441 | 443 | .lineage .row[aria-current="page"] { background: var(--muted); } |
| @@ -830,7 +832,7 @@ form.inline { display: inline; margin-left: auto; } | ||
| 830 | 832 | .check-progress[data-state="queued"] .pinned-slot, .check-progress[data-state="running"] .pinned-slot { height: 9em; } |
| 831 | 833 | .pinned-failures { list-style: none; margin: 8px 0 0; padding: 0; display: grid; gap: 6px; } |
| 832 | 834 | .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); } | |
| 834 | 836 | @media (max-width: 480px) { .check-steps { grid-template-columns: minmax(0, 1fr); } } |
| 835 | 837 | .check-steps .step-state { color: var(--faint); } |
| 836 | 838 | .check-steps [data-state="ok"] .step-state { color: var(--success); } |
| @@ -1038,7 +1040,7 @@ form.inline { display: inline; margin-left: auto; } | ||
| 1038 | 1040 | .finder { width: min(720px, calc(100vw - 24px)); } |
| 1039 | 1041 | @media (max-width: 719px) { .finder { width: 100%; max-width: 100%; } } |
| 1040 | 1042 | .finder .palette-item .cat { |
| 1041 | font-family: var(--mono); font-size: 11.5px; | |
| 1043 | font-family: var(--mono); font-size: var(--fs-min); | |
| 1042 | 1044 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 45%; |
| 1043 | 1045 | } |
| 1044 | 1046 | .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 | }); |
new above
offline