Public: anyone can read this; the crew can change it.
docs/DESIGN.md opens with the design principles in under 30 lines, with the home page and Security settings as worked examples #9601
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/9576-docs-design-md-opens-with-the-design-pri → main 3 commits fork 1 thread · 1 unresolved
3 files
changed +35
−4
docs/DESIGN.md +21
| @@ -1,5 +1,26 @@ | ||
| 1 | 1 | # DESIGN.md — the forge's design system |
| 2 | 2 | |
| 3 | **The principles, in short.** (The numbered list in §1 is the long form.) | |
| 4 | ||
| 5 | 1. One confident sentence says what the page is for; nothing explains itself twice. | |
| 6 | 2. The real product is the visual: show the actual rows, not a picture of them. | |
| 7 | 3. Generous rhythm: space between things, not lines. | |
| 8 | 4. One background, one text colour (`--bg`, `--fg`), muted for the rest. | |
| 9 | 5. At most one accent, `--accent`, for interaction alone. Enforced: `accent` in `scripts/design-lint.ts`. | |
| 10 | 6. Few borders: a hairline where a thing ends, never a box around a thing. | |
| 11 | 7. Rows over boxes: a list is rows, a fact is a `dl` row, not a card. | |
| 12 | 8. Plain words, as in `docs/WRITING.md`. | |
| 13 | ||
| 14 | **Worked examples.** | |
| 15 | - `src/web/studio/home.ts`, `studioHome`: the `<h1 class="sr-only">Home</h1>` then `tellBox` is | |
| 16 | one sentence of purpose (1) and the real composer, not a picture of one (2); `<section class="stack">` | |
| 17 | gives the rhythm (3); the lines `<p class="small muted"><a href="/feed">The feed</a></p>` are | |
| 18 | muted text, not buttons or boxes (4, 7). | |
| 19 | - `src/web/pages/security.ts`, `securityPage`: `<dl class="status-rows">` with one `statusRow` | |
| 20 | per fact (Traffic, Backup, Admin accounts, …) is rows over boxes (7); a row that leads | |
| 21 | anywhere ends in one `<a>` (Traffic, Backups), the only interactive colour (5); each row says | |
| 22 | its state in a few plain words, with a toned `badge` for the verdict (Recent, Overdue) (8). | |
| 23 | ||
| 3 | 24 | One vocabulary, two surfaces. The web renders it as HTML and CSS; a terminal UI |
| 4 | 25 | renders the same names as cells and boxes. If a component is not in these files, |
| 5 | 26 | it does not exist — and if you find yourself hand-rolling markup a page already |
scripts/design-lint.ts +9 −4
| @@ -33,8 +33,9 @@ | ||
| 33 | 33 | * The quiet-page rules (#8775, #8719) are held per file in `docs/design/quiet-debt.json`, |
| 34 | 34 | * the same ratchet, and may only go down: `label` ("Status: Open"), `caps` |
| 35 | 35 | * (`text-transform: uppercase`), `echo` (an icon's aria-label equal to its visible |
| 36 | * text) and `inline-colour` (a colour name in a `style=""`). They match markup and | |
| 37 | * style, never a paragraph's words. Named in docs/design/areas/quiet.md. | |
| 36 | * text), `inline-colour` (a colour name in a `style=""`) and `accent` (a second | |
| 37 | * accent token such as `--accent-2`; only `--accent` and `--accent-fg` exist). | |
| 38 | * They match markup and style, never a paragraph's words. Named in docs/design/areas/quiet.md. | |
| 38 | 39 | * |
| 39 | 40 | * A red names the file:line, the literal and what to use instead, so one pass |
| 40 | 41 | * fixes it; run `bun scripts/design-lint.ts` before pushing. A new class is named |
| @@ -315,9 +316,9 @@ export function compare(base: Baseline, now: Scan, design: string, areas: readon | ||
| 315 | 316 | } |
| 316 | 317 | |
| 317 | 318 | /** The quiet-page rules a machine can check (#8775, from #8719): markup and style only, never prose. */ |
| 318 | export type QuietRule = "label" | "caps" | "echo" | "inline-colour" | "keyhint"; | |
| 319 | export type QuietRule = "label" | "caps" | "echo" | "inline-colour" | "keyhint" | "accent"; | |
| 319 | 320 | export type QuietFinding = { line: number; rule: QuietRule; text: string; use: string }; |
| 320 | export const QUIET_RULES: readonly QuietRule[] = ["label", "caps", "echo", "inline-colour", "keyhint"]; | |
| 321 | export const QUIET_RULES: readonly QuietRule[] = ["label", "caps", "echo", "inline-colour", "keyhint", "accent"]; | |
| 321 | 322 | const STATUS_WORD = "open|closed|merged|draft|done|failed|passing|failing|pending|running|queued|shipped|blocked|active|ready"; |
| 322 | 323 | |
| 323 | 324 | /** Where `text` breaks a quiet-page rule: file line, the offence, and what to use instead. */ |
| @@ -347,6 +348,10 @@ export function quiet(text: string): QuietFinding[] { | ||
| 347 | 348 | out.push({ line: at(m.index!), rule: "keyhint", text: m[0], use: "hintAttr(key): a tooltip on hover and an overlay on request; an inline hint only inside a menu item" }); |
| 348 | 349 | } |
| 349 | 350 | } |
| 351 | // At most one accent (docs/DESIGN.md, principles): `--accent` and `--accent-fg` exist; a second is a second voice. | |
| 352 | for (const m of code.matchAll(/--accent(?!-fg\b)(?:[-_][a-z0-9_-]*|\d+)(?![\w-])/gi)) { | |
| 353 | out.push({ line: at(m.index!), rule: "accent", text: m[0], use: "var(--accent) for interaction alone, and var(--fg) or var(--muted-fg) for emphasis; one accent per page" }); | |
| 354 | } | |
| 350 | 355 | // A colour name in an inline style="": the stylesheet scan reads css templates only. |
| 351 | 356 | for (const m of code.matchAll(/\bstyle="([^"]*)"/g)) { |
| 352 | 357 | const bad = m[1]!.match(NAMED_RE); |
test/design-lint.test.ts +5
| @@ -234,6 +234,11 @@ describe("design check: the quiet-page rules (#8775)", () => { | ||
| 234 | 234 | expect(rules('h = `<button aria-label="Merge">Merge</button>`')).toEqual([]); |
| 235 | 235 | }); |
| 236 | 236 | |
| 237 | test("accent: a second accent token is a finding; --accent and --accent-fg are not", () => { | |
| 238 | expect(rules("c = /* css */ `a{color:var(--accent);background:var(--accent-fg)}`")).toEqual([]); | |
| 239 | expect(rules("c = /* css */ `a{color:var(--accent-2)} b{--accent-alt:#fff}`")).toEqual(["accent", "accent"]); | |
| 240 | }); | |
| 241 | ||
| 237 | 242 | test("inline-colour: a colour name in style, not a token", () => { |
| 238 | 243 | expect(rules('h = `<i style="color: red"></i>`')).toEqual(["inline-colour"]); |
| 239 | 244 | expect(rules('h = `<i style="color: var(--danger)"></i>`')).toEqual([]); |
Wrong:
securityPagealso renders success badges ("No secrets found", "N checks passed"), not only on problems. Fix this clause and check the "single <a> per row" claim against the code.