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

The chart frame becomes a §5 component: .chart-box, .chart and .big are named in §5's table and leave §5d #4331

Merged opened this

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

Merged

mason/forge:mason/4282-the-chart-frame-becomes-a-5-component-ch → main 8 commits fork 1 thread · 1 unresolved

  • Merged. mason/4282-the-chart-frame-becomes-a-5-component-ch was merged into main as 2e75254.

Review

Sign in to review this change.
8 files changed +50 −13
docs/DESIGN.md +2 −1
@@ -211,6 +211,7 @@ specification.
211211 | **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) |
212212 | **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; above the panels only a collapsed one-line `#notes` (`<details>`: closed by default, an open list stays open across live swaps), the strips below them; 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 |
213213 | **stacked bar chart** `windowSvg()` (`fleet-graph.ts`) | `.plots`, `.plot` (`.plot-name`), `.plot-legend` | bars wear `fg-sN`, the `--series-N` token | server-drawn SVG with `width`/`height` set, so it holds its box before the stylesheet; gridlines `--border`, figures `--muted-fg` in mono, panel names `--faint`; `role="img"` with an `aria-label` totalling it, a `<title>` per bar, and a table of the same numbers beside it | a table of the same numbers |
214 | **chart** `chartBox()` (`ui.ts`) | `.chart-box` wrapping `drawChart`'s svg `.chart` (`.big` for the headline figure) | `.is-table` for a table panel | one query or series drawn on the server: the svg carries its own `width`/`height` and the box holds its space, so nothing shifts; `role="img"` with an `aria-label` of its title. Reach for it wherever a page draws a `boards/chart.ts` chart — `/system`'s memory pair and every board panel — and size it from the page (`.bp-*`, `.sys-pair`, `.bgrid`), never inside the frame | the same numbers as a table |
214215
215216 Two things are deliberately **not** in the vocabulary: skeleton loaders (the
216217 server renders whole pages — there is nothing to skeletonise) and a custom
@@ -394,7 +395,7 @@ gone, so this list only shrinks.
394395 A `.field` directly inside a `.bar` drops its bottom margin and the bar aligns to the end, so a labelled input and its button share a baseline
395396 - **`style/components.ts`** — parts of the §5 components that the table does not spell out — the palette's list, the menu's items, a dialog's head and foot, comments, the theme picker — and a few small helpers beside them: `.cat` `.clone` `.clone-text` `.comment` `.comment-body` `.comment-form` `.dash` `.dialog-body` `.dialog-foot` `.dialog-head` `.empty-title` `.error-note` `.field-row` `.form-actions` `.hex` `.ico` `.ico-loader` `.list` `.live-status` `.menu-item` `.menu-panel` `.menu-sep` `.menu-title` `.palette-empty` `.palette-foot` `.palette-group` `.palette-heading` `.palette-input` `.palette-item` `.palette-list` `.passkey` `.passkey-lead` `.passkey-note` `.passkey-off` `.passkey-on` `.preview` `.sep` `.split` `.sprite` `.swatch` `.table-scroll` `.tell-here` `.theme-apply` `.theme-option` `.tick` `.timeline` `.tl-dot` `.toast-body` `.topbar-search`
396397 - **`style/components.ts`, settings rows** — a list whose rows are settings (limits, capacity): the `<ul class="list setting-list">` lets title and meta wrap instead of ellipsizing, and a row's controls take their own line under 640px. `.row-fields` is the form in that row's trail (input, Save, Default): always visible, never hover-gated, wrapping under the text before it would clip. Use both for any page whose rows show a name, a current value, and a control to change it; use `.row-actions` only for secondary actions (delete, wake, pin, revoke). Flags, telemetry, cloud, people, crew roles and subscription headroom/budget use `.row-fields`. Classes: `.row-fields` `.setting-list`
397 - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the merge box, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.app-shelf` `.app-tile` `.app-tile-body` `.app-tile-desc` `.app-tile-look` `.app-tile-name` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.big` `.bmanage` `.boards-result` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.chart` `.chart-box` `.commit-body` `.commit-head` `.domain-records` `.explore-filters` `.explore-line` `.explore-main` `.explore-name` `.explore-row` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.front-pin-any` `.front-weights` `.frozen` `.frozen-form` `.frozen-sentence` `.headroom` `.issue-head` `.issue-meta` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-link` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.terminal-line` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep`
398 - **`style/content.ts`, page furniture** — what frames a page's content: page and repo heads, the merge box, the front page and its tiles, explore, backups, boards, the fleet chart's series colours, lineage: `.app-shelf` `.app-tile` `.app-tile-body` `.app-tile-desc` `.app-tile-look` `.app-tile-name` `.backups-drill` `.backups-error` `.backups-now` `.backups-restore` `.bform` `.bgrid` `.bmanage` `.boards-result` `.bp-h2` `.bp-h3` `.bp-w2` `.bp-w3` `.break` `.bt-bar` `.bt-grid` `.bt-group` `.bt-hist` `.bt-name` `.bt-procs` `.bt-row` `.bt-val` `.commit-body` `.commit-head` `.domain-records` `.explore-filters` `.explore-line` `.explore-main` `.explore-name` `.explore-row` `.fg-s0` `.fg-s1` `.fg-s2` `.fg-s3` `.fg-s4` `.fg-s5` `.fg-s6` `.fg-s7` `.filters` `.flag-form` `.front-admin` `.front-controls` `.front-crew` `.front-featured` `.front-featured-body` `.front-featured-look` `.front-issues` `.front-live` `.front-more` `.front-pick` `.front-pin-any` `.front-weights` `.frozen` `.frozen-form` `.frozen-sentence` `.headroom` `.issue-head` `.issue-meta` `.merge-box` `.merge-form` `.merge-headline` `.merge-sub` `.mirror-form` `.number` `.origin-link` `.origin-mark` `.origin-name` `.origin-row` `.owner` `.page-head` `.path-head` `.policy-line` `.qf` `.repo-actions` `.repo-audience` `.repo-desc` `.repo-layout` `.repo-lineage` `.repo-title` `.search` `.slash` `.stage-detail` `.stage-dot` `.steps` `.sub` `.sys-pair` `.table-wrap` `.terminal-line` `.tok` `.tree-row` `.tree-total` `.wall-cells` `.where-sep`
398399 - **`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`
399400 - **`style/content.ts`, prose (markdown)** — table cell alignment, built by `markdown.ts` as `ta-${align}`: `.ta-c` `.ta-r`
400401 - **`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`
src/web/pages/boards-view.ts +2 −2
@@ -18,11 +18,11 @@ import { liveRegionsJs } from "../client.ts";
1818 import { notFoundPage } from "../chrome.ts";
1919 import { html, raw, when, type Html } from "../html.ts";
2020 import { fragment, page } from "../layout.ts";
21 import { btn, card, empty } from "../ui.ts";
21 import { btn, card, chartBox, empty } from "../ui.ts";
2222 import { flowPanel } from "./flow-panel.ts";
2323 import { isPinned, listPins } from "../../boards/pins.ts";
2424 import { panelData, runFor } from "../../boards/panel_data.ts";
25 import { cellsOf, chartBox, paramsOf, queryFields, readParams } from "./boards.ts";
25 import { cellsOf, paramsOf, queryFields, readParams } from "./boards.ts";
2626
2727 /** The events the panels' queries read (runs, pulls, checks, issues, usage): any of them may change a chart. */
2828 const READS = "run.queued,run.started,run.finished,pull.*,check.*,issue.*,usage.*";
src/web/pages/boards.ts +2 −6
@@ -7,7 +7,7 @@
77 * narrowed to that point's window and listed by the source's first column.
88 * Anyone else gets the not-found page.
99 */
10 import { CHART_NAMES, CHARTS, drawChart, usesSeries, type Cell, type Chart } from "../../boards/chart.ts";
10 import { CHART_NAMES, CHARTS, usesSeries, type Cell, type Chart } from "../../boards/chart.ts";
1111 import { runFor } from "../../boards/panel_data.ts";
1212 import { runQuery, type Query, type Row } from "../../boards/query.ts";
1313 import { gaps, SYSTEM } from "../../boards/sampler.ts";
@@ -18,7 +18,7 @@ import { notePhase, takeSessionTime, timePhase, timePhaseAsync } from "../../cor
1818 import { notFoundPage } from "../chrome.ts";
1919 import { html, raw, when } from "../html.ts";
2020 import { page } from "../layout.ts";
21 import { btn, card, empty } from "../ui.ts";
21 import { btn, card, chartBox, empty } from "../ui.ts";
2222
2323 const BUCKET_MS = { minute: 60_000, hour: 3_600_000, day: 86_400_000 } as const;
2424 const AGGREGATES = ["count", "sum", "avg", "max"] as const;
@@ -82,10 +82,6 @@ export const cellsOf = (rows: readonly Row[], hrefs: readonly (string | null)[]
8282 ...(hrefs[i] ? { href: hrefs[i]! } : {}),
8383 }));
8484
85 /** A drawn chart in the box that holds its space. */
86 export const chartBox = (chart: Chart, cells: readonly Cell[], title: string) =>
87 html`<div class="chart-box${chart === "table" ? " is-table" : ""}">${raw(drawChart(chart, cells, title))}</div>`;
88
8985 const link = (p: Params) => {
9086 const s = new URLSearchParams();
9187 for (const [k, v] of Object.entries(p)) if (v !== "") s.set(k, v);
src/web/pages/system.ts +2 −3
@@ -10,7 +10,6 @@
1010 * (RSS) beside its JS heap, hourly for 24 h (#2151): RSS climbing while the
1111 * heap stays flat is native memory growth. Drawn on the server as SVG.
1212 */
13 import { drawChart } from "../../boards/chart.ts";
1413 import { canSeeMemory, memoryHours } from "../../boards/memory.ts";
1514 import { telemetryReader } from "../../actions/telemetry.ts";
1615 import type { Ctx } from "../../http/context.ts";
@@ -24,7 +23,7 @@ import { notFoundPage, signInFirst } from "../chrome.ts";
2423 import { html, raw, type Html } from "../html.ts";
2524 import { page } from "../layout.ts";
2625 import { systemLiveJs } from "../system-live.ts";
27 import { badge, card, empty, meter, pill, row } from "../ui.ts";
26 import { badge, card, chartBox, empty, meter, pill, row } from "../ui.ts";
2827
2928 /** A group's memory, and a cell's beside its cap and who holds the cap. */
3029 const memoryOf = (g: { rssBytes: number; capBytes: number | null; capBy: "kernel" | "watch" | null }): Html =>
@@ -80,7 +79,7 @@ function memorySection(ctx: Ctx): Html {
8079 const hours = timePhase(ctx.request, "memory", () => memoryHours(ctx.app.db, Date.now()));
8180 const series = (title: string, pick: (h: (typeof hours)[number]) => number) =>
8281 card(
83 html`<div class="chart-box">${raw(drawChart("line", hours.map((h) => ({ label: hhmm(h.at).slice(11), value: pick(h) / 1_048_576 })), title))}</div>`,
82 chartBox("line", hours.map((h) => ({ label: hhmm(h.at).slice(11), value: pick(h) / 1_048_576 })), title),
8483 { head: html`<h2>${title}</h2>` },
8584 );
8685 return hours.length === 0
src/web/ui.ts +9
@@ -21,6 +21,7 @@ import {
2121 tokenNote,
2222 type TokenBasis,
2323 } from "../kernel/tokens-format.ts";
24 import { drawChart, type Cell, type Chart } from "../boards/chart.ts";
2425 import { icon, type IconName } from "./icons.ts";
2526 import { Html, html, raw, when } from "./html.ts";
2627
@@ -724,6 +725,14 @@ export function meter(percent: number, label: string, tone?: "attn"): Html {
724725 >${value}%</meter>`;
725726 }
726727
728 /**
729 * A drawn chart in the box that holds its space: `drawChart`'s svg `.chart`
730 * inside `.chart-box`, which a table panel marks `.is-table`.
731 */
732 export function chartBox(chart: Chart, cells: readonly Cell[], title: string): Html {
733 return html`<div class="chart-box${chart === "table" ? " is-table" : ""}">${raw(drawChart(chart, cells, title))}</div>`;
734 }
735
727736 /** A short object id in a monospace chip — always the first 7 characters. */
728737 export function sha(oid: string, href?: string): Html {
729738 const short = oid.slice(0, 7);
test/boards-page.test.ts +5 −1
@@ -184,7 +184,9 @@ describe("every chart type from one seeded query", () => {
184184 const count = (body: string, re: RegExp) => body.match(re)?.length ?? 0;
185185
186186 test("line draws one point per day, the series set aside", async () => {
187 expect(count(await drawn("line"), /<circle cx="[^"]*" cy="[^"]*" r="4">/g)).toBe(3);
187 const body = await drawn("line");
188 expect(count(body, /<circle cx="[^"]*" cy="[^"]*" r="4">/g)).toBe(3);
189 expect(count(body, /<div class="chart-box"><svg [^>]*class="chart"/g)).toBe(1);
188190 });
189191 test("bar draws one bar per day", async () => {
190192 expect(count(await drawn("bar"), /<rect class="bar"/g)).toBe(3);
@@ -206,6 +208,8 @@ describe("every chart type from one seeded query", () => {
206208 const table = body.match(/<table class="table chart-table">[\s\S]*?<\/table>/)![0];
207209 expect(count(table, /<tr>/g)).toBe(1 + 5);
208210 expect(count(body, /<table/g)).toBe(1);
211 // The panel is chartBox()'s frame, marked as a table (#4282).
212 expect(body).toContain('<div class="chart-box is-table">');
209213 });
210214 test("heat strip draws days × kinds cells; each filled cell links to its rows", async () => {
211215 const body = await drawn("heat");
test/system-memory.test.ts +2
@@ -80,6 +80,8 @@ describe("GET /system", () => {
8080 expect(body).toContain("RSS (MiB, hourly mean)");
8181 expect(body).toContain("JS heap (MiB, hourly mean)");
8282 expect(body.match(/<svg/g)?.length).toBeGreaterThanOrEqual(2);
83 // Both series are drawn in chartBox()'s frame (#4282).
84 expect(body.match(/<div class="chart-box"><svg [^>]*class="chart"/g)?.length).toBe(2);
8385 expect(body).toContain("120.0 MiB");
8486 expect(body).toContain("40.0 MiB");
8587 });
test/ui-chart-box.test.ts added +26
@@ -0,0 +1,26 @@
1 /**
2 * The chart frame (#4282): `chartBox()` is the one place `.chart-box` is
3 * written, around `drawChart`'s svg, with `.is-table` for a table panel.
4 * /system and the board pages assert they send this frame in their own tests.
5 */
6 import { describe, expect, test } from "bun:test";
7 import { drawChart } from "../src/boards/chart.ts";
8 import { chartBox } from "../src/web/ui.ts";
9
10 const cells = [{ label: "a", value: 1 }, { label: "b", value: 2 }];
11
12 describe("chartBox", () => {
13 test("wraps drawChart's svg in the frame", () => {
14 expect(String(chartBox("line", cells, "Runs"))).toBe(`<div class="chart-box">${drawChart("line", cells, "Runs")}</div>`);
15 expect(String(chartBox("number", cells, "Runs"))).toMatch(/^<div class="chart-box"><svg[^>]*class="chart"[\s\S]*<text class="big"/);
16 });
17
18 test("a table panel carries is-table", () => {
19 expect(String(chartBox("table", cells, "Runs"))).toStartWith(`<div class="chart-box is-table">`);
20 expect(String(chartBox("bar", cells, "Runs"))).not.toContain("is-table");
21 });
22
23 test("the title is escaped", () => {
24 expect(String(chartBox("line", cells, `<script>"`))).not.toContain("<script>");
25 });
26 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs