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

Merged opened this

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

Merged

scout/forge:scout/9576-docs-design-md-opens-with-the-design-pri → main 3 commits fork 1 thread · 1 unresolved

  • Merged. scout/9576-docs-design-md-opens-with-the-design-pri was merged into main as 3f7f1ac.

Review

Sign in to review this change.
3 files changed +35 −4
docs/DESIGN.md +21
@@ -1,5 +1,26 @@
11 # DESIGN.md — the forge's design system
22
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
outdated

Wrong: securityPage also 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.

22 its state in a few plain words, with a toned `badge` for the verdict (Recent, Overdue) (8).
23
324 One vocabulary, two surfaces. The web renders it as HTML and CSS; a terminal UI
425 renders the same names as cells and boxes. If a component is not in these files,
526 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 @@
3333 * The quiet-page rules (#8775, #8719) are held per file in `docs/design/quiet-debt.json`,
3434 * the same ratchet, and may only go down: `label` ("Status: Open"), `caps`
3535 * (`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.
3839 *
3940 * A red names the file:line, the literal and what to use instead, so one pass
4041 * 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
315316 }
316317
317318 /** 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";
319320 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"];
321322 const STATUS_WORD = "open|closed|merged|draft|done|failed|passing|failing|pending|running|queued|shipped|blocked|active|ready";
322323
323324 /** 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[] {
347348 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" });
348349 }
349350 }
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 }
350355 // A colour name in an inline style="": the stylesheet scan reads css templates only.
351356 for (const m of code.matchAll(/\bstyle="([^"]*)"/g)) {
352357 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)", () => {
234234 expect(rules('h = `<button aria-label="Merge">Merge</button>`')).toEqual([]);
235235 });
236236
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
237242 test("inline-colour: a colour name in style, not a token", () => {
238243 expect(rules('h = `<i style="color: red"></i>`')).toEqual(["inline-colour"]);
239244 expect(rules('h = `<i style="color: var(--danger)"></i>`')).toEqual([]);

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs