Public: anyone can read this; the crew can change it.
A run page's tests bar draws §5's bar and badge, and .tests-bar, .tests-words leave the markup #4288
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4201-a-run-page-s-tests-bar-draws-5-s-bar-and → main 3 commits fork 1 thread · 1 unresolved
6 files
changed +22
−16
docs/DESIGN.md +2 −2
| @@ -154,7 +154,7 @@ specification. | ||
| 154 | 154 | | **stat pill** `pill()` | `.pill` | `data-tone=accent\|danger` | tabular | a parenthesised count | |
| 155 | 155 | | **status dot** `dot()` | `.dot` | `data-tone=success\|danger\|attn\|merged`; `data-series=0…7` (`seriesDot()`) is a chart series' swatch, in its `--series-N` colour | — | `●` in the tone | |
| 156 | 156 | | **sha** `sha()` | `.sha` | — | first 7 chars | the same 7 chars, dim | |
| 157 | | **progress bar** `progress()` (`pages/milestones.ts`) | `progress` (element rule in `style/components.ts`; every `<progress>` on the site except `.tests-bar`, the run page's own bar) | — | native `<progress max value>`, 4px, full width, an `aria-label` saying what is done; no width rule per value | `▓▓▓░░ 3 of 5` | | |
| 157 | | **progress bar** `progress()` (`pages/milestones.ts`) | `progress` (element rule in `style/components.ts`; every `<progress>` on the site) | — | native `<progress max value>`, 4px, full width, an `aria-label` saying what is done; no width rule per value | `▓▓▓░░ 3 of 5` | | |
| 158 | 158 | | **meter** `meter()` | `meter` | `data-tone=attn` when the share is a limit reached | native `<meter min=0 max=100>`, a whole percentage, an `aria-label` saying what it is a share of; no width rule per value, so no inline style | `▓▓▓░░ 62%` | |
| 159 | 159 | | **kbd** `kbd()` | `kbd` | — | — | a reverse-video key name | |
| 160 | 160 | | **card** `card()` / `listCard()` | `.card` (`.card-head`, `.card-links`, `.card-body`, `.card-foot`) | `data-tone=success\|danger\|merged\|attn` | — | a box with a title bar | |
| @@ -397,7 +397,7 @@ gone, so this list only shrinks. | ||
| 397 | 397 | - **`style/content.ts`, code view** — the source view and the syntax-highlight token classes `highlight-html.ts` builds as `tok-${kind}`: `.blob-view` `.ln` `.tok-attribute` `.tok-comment` `.tok-constant` `.tok-decorator` `.tok-deleted` `.tok-emphasis` `.tok-function` `.tok-heading` `.tok-inserted` `.tok-keyword` `.tok-link` `.tok-meta` `.tok-number` `.tok-operator` `.tok-plain` `.tok-property` `.tok-punctuation` `.tok-regex` `.tok-string` `.tok-tag` `.tok-type` `.tok-variable` |
| 398 | 398 | - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r` |
| 399 | 399 | - **`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-hidden` `.rc-resolve` `.rc-resolved` `.wd` |
| 400 | - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-jump` `.run-live` `.run-steer` `.tests-bar` `.tests-words` `.txt` `.who` | |
| 400 | - **`style/content.ts`, apps, runs, and the crew** — run pages, their logs and checks (a pull's look is §5's **look**): `.ln-forge` `.ln-pulse` `.ln-say` `.ln-tool` `.ln-you` `.log-tail-row` `.pinned-failures` `.pinned-slot` `.run-jump` `.run-live` `.run-steer` `.txt` `.who` | |
| 401 | 401 | - **`style/content.ts`, search results** — search results, the inbox's ask cards: `.ask-acts` `.ask-card` `.ask-group` `.ask-kind` `.ask-meta` `.ask-more-acts` `.ask-pick` `.ask-pick-hit` `.ask-short` `.ask-waiting` `.live-toggle` `.needs-you` |
| 402 | 402 | - **`pages/board.ts`** — the board itself is §5's **board**; the spend tables `spend.ts` draws wear §5's `.table` and `.stack`, no class of their own |
| 403 | 403 | - **`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` |
docs/design/areas/runs.md added +3
| @@ -0,0 +1,3 @@ | ||
| 1 | # Runs | |
| 2 | ||
| 3 | - `.check-words` — the run page's live tests line under the step list: reserves one line of height (1.4em) so the pin slot and log never move as the words arrive. |
src/web/studio/run.ts +3 −3
| @@ -309,11 +309,11 @@ export async function runView( | ||
| 309 | 309 | ), |
| 310 | 310 | )} |
| 311 | 311 | ${known |
| 312 | ? html`<progress class="tests-bar" aria-label="Test files started" max="${row.of ?? 0}" value="${row.started ?? 0}"></progress>` | |
| 312 | ? html`<progress aria-label="Test files started" max="${row.of ?? 0}" value="${row.started ?? 0}"></progress>` | |
| 313 | 313 | : open |
| 314 | ? html`<progress class="tests-bar" aria-label="Test files started"></progress>` | |
| 314 | ? html`<progress aria-label="Test files started"></progress>` | |
| 315 | 315 | : ""} |
| 316 | <p class="tests-words small muted">${[words, runningWords].filter((w) => w !== "").join(" · ")}</p> | |
| 316 | <p class="check-words small muted break">${[words, runningWords].filter((w) => w !== "").join(" · ")}</p> | |
| 317 | 317 | </div> |
| 318 | 318 | <div class="pinned-slot" id="check-pins"${row.failures.length > 0 ? raw(' role="region" tabindex="0" aria-label="Failed tests"') : ""}> |
| 319 | 319 | ${when( |
src/web/style/components.ts +4 −4
| @@ -238,10 +238,10 @@ a.label:hover { color: color-mix(in srgb, var(--label) 90%, var(--fg)); } | ||
| 238 | 238 | .pill[data-tone="accent"] { background: color-mix(in srgb, var(--accent) 14%, transparent); color: var(--accent); } |
| 239 | 239 | .pill[data-tone="danger"] { background: color-mix(in srgb, var(--danger) 14%, transparent); color: var(--danger); } |
| 240 | 240 | |
| 241 | progress:not(.tests-bar) { display: block; width: 100%; height: 4px; margin: 6px 0; appearance: none; border: 0; border-radius: var(--radius-sm); background: var(--border); overflow: hidden; } | |
| 242 | progress:not(.tests-bar)::-webkit-progress-bar { background: var(--border); } | |
| 243 | progress:not(.tests-bar)::-webkit-progress-value { background: var(--success); } | |
| 244 | progress:not(.tests-bar)::-moz-progress-bar { background: var(--success); } | |
| 241 | progress { display: block; width: 100%; height: 4px; margin: 6px 0; appearance: none; border: 0; border-radius: var(--radius-sm); background: var(--border); overflow: hidden; } | |
| 242 | progress::-webkit-progress-bar { background: var(--border); } | |
| 243 | progress::-webkit-progress-value { background: var(--success); } | |
| 244 | progress::-moz-progress-bar { background: var(--success); } | |
| 245 | 245 | |
| 246 | 246 | .dot { display: inline-block; width: 8px; height: 8px; border-radius: 999px; background: var(--faint); flex: none; } |
| 247 | 247 | .dot[data-tone="success"] { background: var(--success); } |
src/web/style/content.ts +1 −2
| @@ -766,8 +766,7 @@ form.inline { display: inline; margin-left: auto; } | ||
| 766 | 766 | .checks-line .ok { display: inline-flex; align-items: center; gap: 4px; color: var(--success); } |
| 767 | 767 | .checks-line .bad { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); } |
| 768 | 768 | .checks-line .now { display: inline-flex; align-items: center; gap: 4px; color: var(--attn); } |
| 769 | .tests-bar { display: block; width: 100%; height: 8px; margin-top: 8px; } | |
| 770 | .tests-words { margin: 4px 0 0; min-height: 1.4em; } | |
| 769 | .check-words { margin: 4px 0 0; min-height: 1.4em; } | |
| 771 | 770 | /* Reserved only while the run is open: pins arriving never move the log. */ |
| 772 | 771 | .pinned-slot { max-height: 9em; overflow-y: auto; } |
| 773 | 772 | [data-check-progress][data-state="queued"] .pinned-slot, [data-check-progress][data-state="running"] .pinned-slot { height: 9em; } |
test/studio-runs.test.ts +9 −5
| @@ -840,14 +840,14 @@ describe("working: a run's own page in the studio", () => { | ||
| 840 | 840 | ); |
| 841 | 841 | expect(stepsLine(app.config.dataDir, { state: "running", run }, false)).toBe(""); |
| 842 | 842 | const page = await (await get(`/alice/engine/runs/${run.id}`, writerCookie)).text(); |
| 843 | expect(page).toContain('<progress class="tests-bar" aria-label="Test files started" max="9" value="2"'); | |
| 843 | expect(page).toContain('<progress aria-label="Test files started" max="9" value="2"'); | |
| 844 | 844 | expect(page).toContain("2 of 9 files started · 4 passed, 1 failed · running test/b.test.ts"); |
| 845 | 845 | expect(page).toContain("<strong>a breaks</strong>"); |
| 846 | 846 | expect(page).toContain("error: <b>boom</b>"); |
| 847 | 847 | // The pins sit after the live region closes. |
| 848 | 848 | const live = page.indexOf('role="status"'); |
| 849 | 849 | expect(live).toBeGreaterThan(-1); |
| 850 | expect(page.indexOf("pinned-failures")).toBeGreaterThan(page.indexOf("tests-words")); | |
| 850 | expect(page.indexOf("pinned-failures")).toBeGreaterThan(page.indexOf("Test files")); | |
| 851 | 851 | expect(page.slice(live, page.indexOf("pinned-slot"))).not.toContain("a breaks"); |
| 852 | 852 | // The replaced element (#check-steps) closes before the pin slot, which is |
| 853 | 853 | // patched in place so focus and scroll survive a refetch. |
| @@ -859,15 +859,19 @@ describe("working: a run's own page in the studio", () => { | ||
| 859 | 859 | // Nothing logged yet: the bar and the line are still there, empty. |
| 860 | 860 | const fresh = mk([]); |
| 861 | 861 | const quiet = await (await get(`/alice/engine/runs/${fresh.id}`, writerCookie)).text(); |
| 862 | expect(quiet).toContain('<progress class="tests-bar" aria-label="Test files started"></progress>'); | |
| 862 | expect(quiet).toContain('<progress aria-label="Test files started"></progress>'); | |
| 863 | 863 | const block = quiet.slice(quiet.indexOf('id="check-steps"'), quiet.indexOf('id="run-live"')); |
| 864 | 864 | expect(block).not.toContain("value="); |
| 865 | 865 | expect(block).not.toContain("tabindex"); |
| 866 | 866 | expect(block).not.toContain('role="region"'); |
| 867 | 867 | // With pins, the slot is focusable and labelled; the live region holds no pin. |
| 868 | 868 | expect(page).toContain('class="pinned-slot" id="check-pins" role="region" tabindex="0" aria-label="Failed tests"'); |
| 869 | expect(page.slice(live, page.indexOf(`id="check-pins"`, live))).toContain("tests-words"); | |
| 870 | expect(quiet).toContain('class="tests-words'); | |
| 869 | expect(page.slice(live, page.indexOf(`id="check-pins"`, live))).toContain("2 of 9 files started"); | |
| 870 | expect(quiet).toContain('<p class="check-words small muted break"></p>'); // empty, but still rendered | |
| 871 | for (const old of ["tests-bar", "tests-words"]) { | |
| 872 | expect(page).not.toContain(old); | |
| 873 | expect(quiet).not.toContain(old); | |
| 874 | } | |
| 871 | 875 | // A finished run with no known count shows no bar at all. |
| 872 | 876 | const done = mk(["$ bun install"]); |
| 873 | 877 | finishRun(app.db, done.id, { state: "failed", summary: "x" }); |
new above
offline
This line used to reserve 1.4em of height. Without it an empty <p> collapses and the page shifts when the words arrive. Put the reservation back with a token-based rule.