Public: anyone can read this; the crew can change it.
Design: /flow's outflow sparklines use the design system, and flow-bar/flow-out leave §5d #3823
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/3614-design-flow-s-outflow-sparklines-use-the → main 1 commit fork
4 files
changed +8
−15
docs/DESIGN.md −1
| @@ -394,7 +394,6 @@ gone, so this list only shrinks. | ||
| 394 | 394 | - **`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` |
| 395 | 395 | - **`style/content.ts`, search results** — search results, the inbox's ask cards, the client list and the claim link: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.claim-link-box` `.forges-sent` `.live-toggle` `.needs-you` `.no-hits` |
| 396 | 396 | - **`pages/board.ts`** — the spend tables `spend.ts` draws, first on the board (the board itself is §5's **board**): `.spend-table` `.spend-totals` |
| 397 | - **`pages/flow-panel.ts`** — the pipeline's outflow sparklines, on /flow and /boards: `.flow-bar` `.flow-out` | |
| 398 | 397 | - **`pages/map.ts`** — the map of what waits for what: `.map-ask` `.map-breath` `.map-canvas` `.map-card` `.map-counts` `.map-dot` `.map-edge` `.map-edge-crit` `.map-edge-done` `.map-empty` `.map-list` `.map-main` `.map-meta` `.map-node` `.map-pillars` `.map-rule` `.map-svg` `.map-title` `.map-tool` `.map-tools` `.map-who` `.map-who-i` `.map-who-n` `.map-word` `.map-wrap` |
| 399 | 398 | - **`studio/feed_style.ts`** — the home feed's cards, its composer and the apps rail: `.composer` `.composer-bar` `.fcard-form` `.fcard-reason` `.fcard-reply` `.fcard-text` `.fcard-unblock` `.fcard-who` `.fcard-why` `.feed` `.feed-head` `.kpi` `.kpi-note` `.kpi-spark` `.kpi-value` `.kpis` `.home-app` `.home-apps` `.home-apps-verbs` `.home-pick` `.home-rail` `.home-tabs` |
| 400 | 399 |
src/web/pages/boards-view.ts +1 −2
| @@ -19,7 +19,7 @@ import { notFoundPage } from "../chrome.ts"; | ||
| 19 | 19 | import { html, raw, when, type Html } from "../html.ts"; |
| 20 | 20 | import { fragment, page } from "../layout.ts"; |
| 21 | 21 | import { btn, card, empty } from "../ui.ts"; |
| 22 | import { flowPanel, flowPanelCss } from "./flow-panel.ts"; | |
| 22 | import { flowPanel } from "./flow-panel.ts"; | |
| 23 | 23 | import { isPinned, listPins } from "../../boards/pins.ts"; |
| 24 | 24 | import { cellsOf, chartBox, paramsOf, queryFields, readParams, runFor } from "./boards.ts"; |
| 25 | 25 | |
| @@ -105,7 +105,6 @@ export async function boardsList(ctx: Ctx): Promise<Response> { | ||
| 105 | 105 | body: html`<div class="page-head"><h1>Boards</h1> |
| 106 | 106 | ${btn("Explore the data", { href: "/boards/data", variant: "ghost" })}</div> |
| 107 | 107 | ${card(make)} ${body} ${flowPanel(ctx.app, { kind: "owner" }, ctx.url, "/boards")}`, |
| 108 | styleCss: flowPanelCss, | |
| 109 | 108 | }); |
| 110 | 109 | } |
| 111 | 110 |
src/web/pages/flow-panel.ts +3 −10
| @@ -37,13 +37,6 @@ import { html, type Html } from "../html.ts"; | ||
| 37 | 37 | import { heldCard } from "./boards-held.ts"; |
| 38 | 38 | import { badge, card } from "../ui.ts"; |
| 39 | 39 | |
| 40 | export const flowPanelCss = /* css */ ` | |
| 41 | .flow-out { display: inline-flex; align-items: center; gap: 8px; font-variant-numeric: tabular-nums; } | |
| 42 | .flow-out svg { width: 72px; height: 16px; } | |
| 43 | .flow-bar { fill: var(--muted-fg); } | |
| 44 | .flow-bar[data-empty] { fill: var(--muted); } | |
| 45 | `; | |
| 46 | ||
| 47 | 40 | /** What `?out=` and `?hour=` ask to list, checked: an unknown stage or hour lists nothing. */ |
| 48 | 41 | function asked(url: URL): { key: PipelineKey; hour: number | null } | null { |
| 49 | 42 | const key = stageOf(url.searchParams.get("out") ?? ""); |
| @@ -69,15 +62,15 @@ function sparkline(t: StageThroughput): Html { | ||
| 69 | 62 | const bars = t.hours.map((n, hour) => { |
| 70 | 63 | const x = (HOURS - 1 - hour) * 3; |
| 71 | 64 | const h = n === 0 ? 1 : Math.max(2, Math.round((n / max) * 16)); |
| 72 | return html`<rect class="flow-bar" ${n === 0 ? html`data-empty` : null} x="${String(x)}" y="${String(16 - h)}" width="2" height="${String(h)}" />`; | |
| 65 | return html`<rect ${n === 0 ? html`fill-opacity="0.3"` : null} x="${String(x)}" y="${String(16 - h)}" width="2" height="${String(h)}" />`; | |
| 73 | 66 | }); |
| 74 | return html`<svg viewBox="0 0 72 16" aria-hidden="true" focusable="false">${bars}</svg>`; | |
| 67 | return html`<svg class="muted" viewBox="0 0 72 16" width="72" height="16" fill="currentColor" aria-hidden="true" focusable="false">${bars}</svg>`; | |
| 75 | 68 | } |
| 76 | 69 | |
| 77 | 70 | function outCell(base: string, t: StageThroughput): Html { |
| 78 | 71 | const day = t.items.length; |
| 79 | 72 | const passed = t.items.filter((i) => i.outcome === "passed").length; |
| 80 | return html`<span class="flow-out" data-out="${OUTFLOW[t.key].out}"> | |
| 73 | return html`<span class="bar num" data-out="${OUTFLOW[t.key].out}"> | |
| 81 | 74 | <a class="mono" href="${outHref(base, t.key, 0)}" data-hour="0">${String(t.hours[0] ?? 0)}</a> |
| 82 | 75 | ${sparkline(t)} |
| 83 | 76 | <a class="muted small" href="${outHref(base, t.key, null)}" data-day="${String(day)}">${String(day)} in 24 h</a> |
test/flow-throughput.test.ts +4 −2
| @@ -190,13 +190,15 @@ describe("/flow: actual throughput per stage", () => { | ||
| 190 | 190 | expect(page).toMatch(/data-out="landed">\s*<a class="mono" href="\/boards\?out=landed&hour=0#out" data-hour="0">2<\/a>/); |
| 191 | 191 | expect(page).toContain('data-day="6">6 in 24 h'); |
| 192 | 192 | expect(page).toContain("2 passed · 1 failed"); |
| 193 | expect((page.match(/class="flow-bar"/g) ?? []).length).toBe(PIPELINE.length * 24); | |
| 193 | expect(page).not.toContain("flow-bar"); | |
| 194 | expect(page).not.toContain("flow-out"); | |
| 195 | expect((page.match(/<rect /g) ?? []).length).toBeGreaterThanOrEqual(PIPELINE.length * 24); | |
| 194 | 196 | }); |
| 195 | 197 | |
| 196 | 198 | test("each sparkline is a picture only: hidden from assistive tech, no links or tab stops of its own", async () => { |
| 197 | 199 | const page = await (await get("/boards")).text(); |
| 198 | 200 | const svgs = page.match(/<svg[\s\S]*?<\/svg>/g) ?? []; |
| 199 | const sparks = svgs.filter((s) => s.includes('class="flow-bar"')); | |
| 201 | const sparks = svgs.filter((s) => s.includes('viewBox="0 0 72 16"')); | |
| 200 | 202 | expect(sparks).toHaveLength(PIPELINE.length); |
| 201 | 203 | for (const svg of sparks) { |
| 202 | 204 | expect(svg).toContain('aria-hidden="true"'); |
new above
offline