Public: anyone can read this; the crew can change it.
The forge looks like one thought-through product again: a design audit and one design system across every page #2685
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/2678-the-forge-looks-like-one-thought-through → main 2 commits fork 2 threads · 2 unresolved
6 files
changed +1073
−0
README.md +9
| @@ -1201,11 +1201,20 @@ forge icon. | ||
| 1201 | 1201 | |
| 1202 | 1202 | sh |
| 1203 | 1203 | bun test # protocol-level + adversarial suite |
| 1204 | bun scripts/design-lint.ts # no colour, off-scale font-size or component class beyond docs/design/baseline.json | |
| 1204 | 1205 | bun x tsc --noEmit # zero type errors is the standard |
| 1205 | 1206 | bun run check # exactly what the forge runs against every push |
| 1206 | 1207 | bun run check:record # the same, and rewrites scripts/test-times.json |
| 1207 | 1208 | |
| 1208 | 1209 | |
| 1210 | The design check (`test/design-lint.test.ts`, run in the gate) fails a pull that | |
| 1211 | adds a colour literal, an off-scale `font-size` or a component class beyond | |
| 1212 | `docs/design/baseline.json`; the message names the file. The way out is to use | |
| 1213 | a token (`var(--…)`) or a named component from `docs/DESIGN.md`; when a change | |
| 1214 | lowers the counts or adds a component to DESIGN.md §5, run | |
| 1215 | `bun scripts/design-lint.ts --update` and commit the baseline. See | |
| 1216 | `docs/design/plan.md`. | |
| 1217 | ||
| 1209 | 1218 | `bun run check` is the gate: the same command `forge.json` declares, so what |
| 1210 | 1219 | it says here is what the deploy will say. It type-checks once, then runs the |
| 1211 | 1220 | test files `scripts/gate.ts` admits — every `test/*.test.ts` not in its |
docs/design/audit.md added +74
| @@ -0,0 +1,74 @@ | ||
| 1 | # Design audit — 2026-10-19 (#2678): a source-drift report | |
| 2 | ||
| 3 | The owner's words: "a giant pile of interfaces with no apparent reason". This | |
| 4 | audit says what is true, what it rests on, and what it does not yet cover. | |
| 5 | ||
| 6 | ## What this audit is not | |
| 7 | ||
| 8 | It is a report on drift in the source, **not** a judgement of how the pages | |
| 9 | look. It has **no screenshots**, so it says nothing about layout shift, 360px, | |
| 10 | contrast or dark mode; the verdicts in Finding 2 are about purpose and overlap | |
| 11 | read from code, and are provisional until the sweep (#2690) lands. The agent that wrote it runs in a sandbox that cannot | |
| 12 | open a browser or reach a running forge, so the desktop/360px × light/dark sweep | |
| 13 | is not here; it is the first item of the plan (#2690) for a run that can. Everything else is read from the source: `src/web/**`, `docs/DESIGN.md`, | |
| 14 | and the counts from `bun scripts/design-lint.ts`. | |
| 15 | ||
| 16 | ## Finding 1 — the design system exists; the pages left it | |
| 17 | ||
| 18 | `docs/DESIGN.md` already is "one design system": tokens (palettes as data in | |
| 19 | `src/design/themes.ts`), a type scale (11.5 / 12.5 / 13 / 14 / 15 / 16 / 20px), | |
| 20 | a 4px grid, three radii, one accent, ~70 named components. The clean app | |
| 21 | template's advantage is not a better system; it is that it never grew past it. | |
| 22 | ||
| 23 | The forge's own source has drifted, measured today: | |
| 24 | ||
| 25 | | drift | count | where most | | |
| 26 | | ------------------------------------------------ | ----: | ----------------------------------------------------------------------- | | |
| 27 | | hex / `rgb()` / `hsl()` literals outside themes | 95 | `style/content.ts` (23), `studio/home.ts` (7), `pages/looks.ts` (6), `pages/site.ts` (3) | | |
| 28 | | `font-size` off the scale (13.5, 12, 11, 10, 22, 18, 72 …) | 67 | `style/content.ts` (31), `style/components.ts` (16), `pages/map.ts` (12), `pages/board.ts` (6) | | |
| 29 | | classes defined in stylesheets | 619 | DESIGN.md §5 names ~70 components; the rest are page-local | | |
| 30 | ||
| 31 | 619 classes against ~70 documented components is the "pile": most pages grew | |
| 32 | their own vocabulary instead of composing the named one. | |
| 33 | ||
| 34 | ## Finding 2 — the pages, and what each is for | |
| 35 | ||
| 36 | Judged by the question "what would the owner do here that they cannot do | |
| 37 | elsewhere?". Page sizes are source lines, a rough proxy for how much each | |
| 38 | invented. | |
| 39 | ||
| 40 | | page | for | verdict | | |
| 41 | | ------------------ | ----------------------------------------------- | ------------------------------------------------------------------- | | |
| 42 | | `/` (studio home) | where I am, what needs me | keep; redo as KPIs the owner pins, not a to-do list (#2665) | | |
| 43 | | `/boards` | saved queries as panels | keep; selectors unusable (#2666); becomes the home's panel source | | |
| 44 | | `/system` | realtime machine state (btop) | keep; redo on the system (#2678 plan item 4) | | |
| 45 | | `/flow` | pipeline and bottleneck (topbar button, admin) | overlaps `/system` and boards: merge as a board panel set | | |
| 46 | | `/settings/usage`, `/settings/capacity`, `/settings/telemetry`, spend | four pages about "how much is being used" | one page, tabs by `tabs()` — usage and spend are the same question | | |
| 47 | | `/settings/model`, `/settings/cloud`, `/settings/backups`, `/settings/resilience` | machine/provider/recovery configuration | keep as settings sections; resilience (51 lines) folds into backups | | |
| 48 | | `/map` | 1,806 lines, 12 off-scale sizes, own colours | the largest private vocabulary; review what it shows that `/boards` and the issue dependency list do not | | |
| 49 | | `/audit`, `waves`, `variants`, `looks`, `returns`, `chain` | operator views of the crew's work | each needs a stated reason to be a page rather than a board panel | | |
| 50 | | `/settings/*` rest | account, keys, tokens, origins, crew | fine as one settings shell (`settingsLayout()`); no change | | |
| 51 | ||
| 52 | Duplication to resolve: three places answer "what is the forge doing right now" | |
| 53 | (home, `/flow`, `/system`); four answer "what does it cost / how full is it". | |
| 54 | ||
| 55 | ## Finding 3 — what the clean app template does better | |
| 56 | ||
| 57 | - One page, one purpose, one primary action. The forge's top bar carries | |
| 58 | Flow, search, help, theme, New, user — six controls before the page begins. | |
| 59 | - It has no page-local CSS. Every forge page file that carries colour or size | |
| 60 | literals (list above) is a page that reinvented a token. | |
| 61 | - First paint is the product. Several forge pages are tables of everything; | |
| 62 | the template shows three numbers and a list. | |
| 63 | ||
| 64 | ## Not covered | |
| 65 | ||
| 66 | Screenshots, contrast measurement per palette, the TUI surface, and pages | |
| 67 | behind a specific repo (`/:owner/:name/*`), which the repo header and tabs | |
| 68 | already unify and which are not where the drift is. | |
| 69 | ||
| 70 | ## Plan | |
| 71 | ||
| 72 | `docs/design/plan.md` — small issues, in order. The check that holds the line | |
| 73 | meanwhile is `scripts/design-lint.ts` (run by `test/design-lint.test.ts`, so it | |
| 74 | runs in the gate). |
docs/design/baseline.json added +793
| @@ -0,0 +1,793 @@ | ||
| 1 | { | |
| 2 | "files": { | |
| 3 | "src/web/chrome.ts": { | |
| 4 | "colour": 2, | |
| 5 | "size": 0 | |
| 6 | }, | |
| 7 | "src/web/layout.ts": { | |
| 8 | "colour": 1, | |
| 9 | "size": 0 | |
| 10 | }, | |
| 11 | "src/web/pages/account.ts": { | |
| 12 | "colour": 1, | |
| 13 | "size": 0 | |
| 14 | }, | |
| 15 | "src/web/pages/apps.ts": { | |
| 16 | "colour": 1, | |
| 17 | "size": 0 | |
| 18 | }, | |
| 19 | "src/web/pages/board.ts": { | |
| 20 | "colour": 1, | |
| 21 | "size": 6 | |
| 22 | }, | |
| 23 | "src/web/pages/claim.ts": { | |
| 24 | "colour": 1, | |
| 25 | "size": 0 | |
| 26 | }, | |
| 27 | "src/web/pages/conflicts.ts": { | |
| 28 | "colour": 0, | |
| 29 | "size": 1 | |
| 30 | }, | |
| 31 | "src/web/pages/everyday.ts": { | |
| 32 | "colour": 1, | |
| 33 | "size": 0 | |
| 34 | }, | |
| 35 | "src/web/pages/family.ts": { | |
| 36 | "colour": 1, | |
| 37 | "size": 0 | |
| 38 | }, | |
| 39 | "src/web/pages/forges.ts": { | |
| 40 | "colour": 1, | |
| 41 | "size": 0 | |
| 42 | }, | |
| 43 | "src/web/pages/limb.ts": { | |
| 44 | "colour": 1, | |
| 45 | "size": 0 | |
| 46 | }, | |
| 47 | "src/web/pages/limb_replan.ts": { | |
| 48 | "colour": 1, | |
| 49 | "size": 0 | |
| 50 | }, | |
| 51 | "src/web/pages/looks.ts": { | |
| 52 | "colour": 6, | |
| 53 | "size": 0 | |
| 54 | }, | |
| 55 | "src/web/pages/map.ts": { | |
| 56 | "colour": 3, | |
| 57 | "size": 12 | |
| 58 | }, | |
| 59 | "src/web/pages/milestones.ts": { | |
| 60 | "colour": 2, | |
| 61 | "size": 0 | |
| 62 | }, | |
| 63 | "src/web/pages/new-forge.ts": { | |
| 64 | "colour": 1, | |
| 65 | "size": 0 | |
| 66 | }, | |
| 67 | "src/web/pages/next-steps.ts": { | |
| 68 | "colour": 2, | |
| 69 | "size": 0 | |
| 70 | }, | |
| 71 | "src/web/pages/settings.ts": { | |
| 72 | "colour": 1, | |
| 73 | "size": 0 | |
| 74 | }, | |
| 75 | "src/web/pages/site.ts": { | |
| 76 | "colour": 3, | |
| 77 | "size": 0 | |
| 78 | }, | |
| 79 | "src/web/standards.ts": { | |
| 80 | "colour": 2, | |
| 81 | "size": 0 | |
| 82 | }, | |
| 83 | "src/web/studio/app.ts": { | |
| 84 | "colour": 2, | |
| 85 | "size": 0 | |
| 86 | }, | |
| 87 | "src/web/studio/asks.ts": { | |
| 88 | "colour": 1, | |
| 89 | "size": 0 | |
| 90 | }, | |
| 91 | "src/web/studio/asks_page.ts": { | |
| 92 | "colour": 2, | |
| 93 | "size": 0 | |
| 94 | }, | |
| 95 | "src/web/studio/change.ts": { | |
| 96 | "colour": 2, | |
| 97 | "size": 0 | |
| 98 | }, | |
| 99 | "src/web/studio/cost_line.ts": { | |
| 100 | "colour": 1, | |
| 101 | "size": 0 | |
| 102 | }, | |
| 103 | "src/web/studio/crew.ts": { | |
| 104 | "colour": 1, | |
| 105 | "size": 0 | |
| 106 | }, | |
| 107 | "src/web/studio/explore.ts": { | |
| 108 | "colour": 2, | |
| 109 | "size": 0 | |
| 110 | }, | |
| 111 | "src/web/studio/feed.ts": { | |
| 112 | "colour": 1, | |
| 113 | "size": 0 | |
| 114 | }, | |
| 115 | "src/web/studio/home.ts": { | |
| 116 | "colour": 7, | |
| 117 | "size": 0 | |
| 118 | }, | |
| 119 | "src/web/studio/home_tabs.ts": { | |
| 120 | "colour": 2, | |
| 121 | "size": 0 | |
| 122 | }, | |
| 123 | "src/web/studio/make_it_yours.ts": { | |
| 124 | "colour": 1, | |
| 125 | "size": 0 | |
| 126 | }, | |
| 127 | "src/web/studio/nouns.ts": { | |
| 128 | "colour": 1, | |
| 129 | "size": 0 | |
| 130 | }, | |
| 131 | "src/web/studio/now.ts": { | |
| 132 | "colour": 1, | |
| 133 | "size": 0 | |
| 134 | }, | |
| 135 | "src/web/studio/previews.ts": { | |
| 136 | "colour": 2, | |
| 137 | "size": 0 | |
| 138 | }, | |
| 139 | "src/web/studio/shelf.ts": { | |
| 140 | "colour": 2, | |
| 141 | "size": 0 | |
| 142 | }, | |
| 143 | "src/web/studio/style.ts": { | |
| 144 | "colour": 2, | |
| 145 | "size": 1 | |
| 146 | }, | |
| 147 | "src/web/studio/tell.ts": { | |
| 148 | "colour": 1, | |
| 149 | "size": 0 | |
| 150 | }, | |
| 151 | "src/web/studio/toolbar.ts": { | |
| 152 | "colour": 2, | |
| 153 | "size": 0 | |
| 154 | }, | |
| 155 | "src/web/studio/words.ts": { | |
| 156 | "colour": 1, | |
| 157 | "size": 0 | |
| 158 | }, | |
| 159 | "src/web/style/components.ts": { | |
| 160 | "colour": 5, | |
| 161 | "size": 16 | |
| 162 | }, | |
| 163 | "src/web/style/content.ts": { | |
| 164 | "colour": 23, | |
| 165 | "size": 31 | |
| 166 | }, | |
| 167 | "src/web/ui.ts": { | |
| 168 | "colour": 1, | |
| 169 | "size": 0 | |
| 170 | } | |
| 171 | }, | |
| 172 | "classes": [ | |
| 173 | "about", | |
| 174 | "act", | |
| 175 | "add", | |
| 176 | "agent-brief", | |
| 177 | "ahead", | |
| 178 | "app-shelf", | |
| 179 | "app-strip", | |
| 180 | "app-tile", | |
| 181 | "app-tile-body", | |
| 182 | "app-tile-desc", | |
| 183 | "app-tile-look", | |
| 184 | "app-tile-name", | |
| 185 | "app-url", | |
| 186 | "ask-acts", | |
| 187 | "ask-card", | |
| 188 | "ask-group", | |
| 189 | "ask-kind", | |
| 190 | "ask-meta", | |
| 191 | "ask-more-acts", | |
| 192 | "ask-pick", | |
| 193 | "ask-pick-hit", | |
| 194 | "ask-short", | |
| 195 | "ask-waiting", | |
| 196 | "audit", | |
| 197 | "audit-alarm", | |
| 198 | "audit-answer", | |
| 199 | "audit-app", | |
| 200 | "audit-cells", | |
| 201 | "audit-change", | |
| 202 | "audit-h2", | |
| 203 | "audit-problems", | |
| 204 | "audit-said", | |
| 205 | "audit-sum", | |
| 206 | "audit-trail", | |
| 207 | "audit-unread", | |
| 208 | "audit-wide", | |
| 209 | "avatar", | |
| 210 | "backups-drill", | |
| 211 | "backups-error", | |
| 212 | "backups-now", | |
| 213 | "backups-restore", | |
| 214 | "bad", | |
| 215 | "badge", | |
| 216 | "bar", | |
| 217 | "bare", | |
| 218 | "behind", | |
| 219 | "bform", | |
| 220 | "bgrid", | |
| 221 | "big", | |
| 222 | "blob-view", | |
| 223 | "bmanage", | |
| 224 | "board", | |
| 225 | "board-about", | |
| 226 | "board-answer", | |
| 227 | "board-app", | |
| 228 | "board-app-mode", | |
| 229 | "board-app-name", | |
| 230 | "board-app-nums", | |
| 231 | "board-app-week", | |
| 232 | "board-asked", | |
| 233 | "board-asks", | |
| 234 | "board-bar", | |
| 235 | "board-brand", | |
| 236 | "board-card", | |
| 237 | "board-clock", | |
| 238 | "board-count", | |
| 239 | "board-decision", | |
| 240 | "board-decision-main", | |
| 241 | "board-declined", | |
| 242 | "board-declined-row", | |
| 243 | "board-declined-says", | |
| 244 | "board-dot", | |
| 245 | "board-elapsed", | |
| 246 | "board-fill", | |
| 247 | "board-forge", | |
| 248 | "board-grid", | |
| 249 | "board-group", | |
| 250 | "board-h", | |
| 251 | "board-inferred", | |
| 252 | "board-key", | |
| 253 | "board-keys", | |
| 254 | "board-kind", | |
| 255 | "board-landed", | |
| 256 | "board-landed-meta", | |
| 257 | "board-landed-title", | |
| 258 | "board-list", | |
| 259 | "board-live", | |
| 260 | "board-outcome", | |
| 261 | "board-p", | |
| 262 | "board-parked-row", | |
| 263 | "board-pillar", | |
| 264 | "board-pillars", | |
| 265 | "board-pillars-h", | |
| 266 | "board-quiet", | |
| 267 | "board-region", | |
| 268 | "board-run", | |
| 269 | "board-run-meta", | |
| 270 | "board-run-title", | |
| 271 | "board-run-who", | |
| 272 | "board-scope", | |
| 273 | "board-spend", | |
| 274 | "board-spend-detail", | |
| 275 | "board-state", | |
| 276 | "board-sub", | |
| 277 | "board-sub-name", | |
| 278 | "board-sub-scope", | |
| 279 | "board-subs", | |
| 280 | "board-thumb", | |
| 281 | "board-top", | |
| 282 | "board-when", | |
| 283 | "board-window", | |
| 284 | "board-window-name", | |
| 285 | "board-windows", | |
| 286 | "boards-result", | |
| 287 | "bp-h2", | |
| 288 | "bp-h3", | |
| 289 | "bp-w2", | |
| 290 | "bp-w3", | |
| 291 | "branch-div", | |
| 292 | "branch-row", | |
| 293 | "break", | |
| 294 | "btn", | |
| 295 | "btn-icon", | |
| 296 | "byline", | |
| 297 | "byline-name", | |
| 298 | "card", | |
| 299 | "card-body", | |
| 300 | "card-foot", | |
| 301 | "card-head", | |
| 302 | "cat", | |
| 303 | "chart", | |
| 304 | "chart-box", | |
| 305 | "check", | |
| 306 | "check-progress", | |
| 307 | "check-steps", | |
| 308 | "checks-line", | |
| 309 | "claim-link-box", | |
| 310 | "clone", | |
| 311 | "clone-text", | |
| 312 | "close-into", | |
| 313 | "close-reason", | |
| 314 | "code", | |
| 315 | "code-hit", | |
| 316 | "code-layout", | |
| 317 | "code-line", | |
| 318 | "code-main", | |
| 319 | "code-split", | |
| 320 | "color-field", | |
| 321 | "comment", | |
| 322 | "comment-body", | |
| 323 | "comment-form", | |
| 324 | "commit-body", | |
| 325 | "commit-head", | |
| 326 | "composer", | |
| 327 | "composer-bar", | |
| 328 | "connect-more", | |
| 329 | "connect-path", | |
| 330 | "count", | |
| 331 | "counts", | |
| 332 | "crumbs", | |
| 333 | "dash", | |
| 334 | "dc", | |
| 335 | "dc-review", | |
| 336 | "dcx", | |
| 337 | "del", | |
| 338 | "device-code", | |
| 339 | "dh", | |
| 340 | "dialog", | |
| 341 | "dialog-body", | |
| 342 | "dialog-foot", | |
| 343 | "dialog-head", | |
| 344 | "diff", | |
| 345 | "diff-body", | |
| 346 | "diff-file", | |
| 347 | "diff-head", | |
| 348 | "diff-note", | |
| 349 | "diff-path", | |
| 350 | "diff-stat", | |
| 351 | "diff-stub", | |
| 352 | "diff-summary", | |
| 353 | "diff-table", | |
| 354 | "diff-viewed", | |
| 355 | "dl", | |
| 356 | "dl-review", | |
| 357 | "dn", | |
| 358 | "dn-comment", | |
| 359 | "domain-records", | |
| 360 | "dot", | |
| 361 | "edit-row", | |
| 362 | "editor-commit", | |
| 363 | "editor-gutter", | |
| 364 | "editor-head", | |
| 365 | "editor-hint", | |
| 366 | "editor-mirror", | |
| 367 | "editor-path", | |
| 368 | "editor-preview", | |
| 369 | "editor-stack", | |
| 370 | "editor-tab", | |
| 371 | "editor-target", | |
| 372 | "editor-wrap", | |
| 373 | "empty", | |
| 374 | "empty-title", | |
| 375 | "env-card", | |
| 376 | "env-error", | |
| 377 | "env-facts", | |
| 378 | "error", | |
| 379 | "error-note", | |
| 380 | "everyday-ask", | |
| 381 | "ex-dir", | |
| 382 | "ex-empty", | |
| 383 | "ex-file", | |
| 384 | "ex-find", | |
| 385 | "ex-head", | |
| 386 | "ex-list", | |
| 387 | "ex-more", | |
| 388 | "ex-name", | |
| 389 | "ex-ref", | |
| 390 | "ex-twist", | |
| 391 | "explore-filters", | |
| 392 | "explore-line", | |
| 393 | "explore-main", | |
| 394 | "explore-name", | |
| 395 | "explore-row", | |
| 396 | "explorer", | |
| 397 | "explorer-panel", | |
| 398 | "explorer-toggle", | |
| 399 | "fcard", | |
| 400 | "fcard-acts", | |
| 401 | "fcard-app", | |
| 402 | "fcard-form", | |
| 403 | "fcard-head", | |
| 404 | "fcard-more", | |
| 405 | "fcard-reason", | |
| 406 | "fcard-reply", | |
| 407 | "fcard-text", | |
| 408 | "fcard-unblock", | |
| 409 | "fcard-who", | |
| 410 | "fcard-why", | |
| 411 | "feed", | |
| 412 | "feed-head", | |
| 413 | "fg-s0", | |
| 414 | "fg-s1", | |
| 415 | "fg-s2", | |
| 416 | "fg-s3", | |
| 417 | "fg-s4", | |
| 418 | "fg-s5", | |
| 419 | "fg-s6", | |
| 420 | "fg-s7", | |
| 421 | "field", | |
| 422 | "field-row", | |
| 423 | "files-head", | |
| 424 | "filters", | |
| 425 | "finder", | |
| 426 | "finder-note", | |
| 427 | "flag-form", | |
| 428 | "fleet-assume", | |
| 429 | "fleet-axis", | |
| 430 | "fleet-grid", | |
| 431 | "fleet-legend", | |
| 432 | "fleet-panel-name", | |
| 433 | "fleet-pick", | |
| 434 | "fleet-svg", | |
| 435 | "fleet-swatch", | |
| 436 | "fleet-table", | |
| 437 | "fleet-ticks", | |
| 438 | "fleet-window", | |
| 439 | "fleet-windows", | |
| 440 | "flow-bar", | |
| 441 | "flow-out", | |
| 442 | "following", | |
| 443 | "following-actions", | |
| 444 | "following-eyebrow", | |
| 445 | "following-foot", | |
| 446 | "following-head", | |
| 447 | "following-sentence", | |
| 448 | "foot", | |
| 449 | "forges-sent", | |
| 450 | "fork-bar", | |
| 451 | "form-actions", | |
| 452 | "front-admin", | |
| 453 | "front-controls", | |
| 454 | "front-crew", | |
| 455 | "front-featured", | |
| 456 | "front-featured-body", | |
| 457 | "front-featured-look", | |
| 458 | "front-issues", | |
| 459 | "front-live", | |
| 460 | "front-more", | |
| 461 | "front-pick", | |
| 462 | "front-pin-any", | |
| 463 | "front-weights", | |
| 464 | "frozen", | |
| 465 | "frozen-form", | |
| 466 | "frozen-sentence", | |
| 467 | "grants", | |
| 468 | "grants-foot", | |
| 469 | "grow", | |
| 470 | "handoff", | |
| 471 | "handoff-links", | |
| 472 | "headroom", | |
| 473 | "held-bar", | |
| 474 | "held-done_on_main", | |
| 475 | "held-key", | |
| 476 | "held-open_pull", | |
| 477 | "held-parked_twice", | |
| 478 | "held-planner", | |
| 479 | "held-red_fix", | |
| 480 | "held-run_cap", | |
| 481 | "held-running", | |
| 482 | "held-setting", | |
| 483 | "held-stalled", | |
| 484 | "held-startable", | |
| 485 | "hero", | |
| 486 | "hero-terminal", | |
| 487 | "hex", | |
| 488 | "hide-lg", | |
| 489 | "hide-sm", | |
| 490 | "hint", | |
| 491 | "home-app", | |
| 492 | "home-apps", | |
| 493 | "home-apps-verbs", | |
| 494 | "home-pick", | |
| 495 | "home-rail", | |
| 496 | "home-tabs", | |
| 497 | "ico", | |
| 498 | "ico-loader", | |
| 499 | "input", | |
| 500 | "issue-head", | |
| 501 | "issue-meta", | |
| 502 | "js-only", | |
| 503 | "kv", | |
| 504 | "label", | |
| 505 | "limb-confirm", | |
| 506 | "limb-rows", | |
| 507 | "limb-why", | |
| 508 | "lineage", | |
| 509 | "lineage-children", | |
| 510 | "lineage-here", | |
| 511 | "list", | |
| 512 | "live-note", | |
| 513 | "live-pill", | |
| 514 | "live-status", | |
| 515 | "live-toggle", | |
| 516 | "ln", | |
| 517 | "ln-forge", | |
| 518 | "ln-pulse", | |
| 519 | "ln-say", | |
| 520 | "ln-tool", | |
| 521 | "ln-you", | |
| 522 | "log-tail-row", | |
| 523 | "look", | |
| 524 | "look-attached", | |
| 525 | "look-attached-phone", | |
| 526 | "look-attachments", | |
| 527 | "look-card", | |
| 528 | "look-desktop", | |
| 529 | "look-now", | |
| 530 | "look-of", | |
| 531 | "look-pair", | |
| 532 | "look-phone", | |
| 533 | "look-thumb", | |
| 534 | "map-ask", | |
| 535 | "map-autonomy", | |
| 536 | "map-breath", | |
| 537 | "map-canvas", | |
| 538 | "map-card", | |
| 539 | "map-chart", | |
| 540 | "map-counts", | |
| 541 | "map-dot", | |
| 542 | "map-edge", | |
| 543 | "map-edge-crit", | |
| 544 | "map-edge-done", | |
| 545 | "map-empty", | |
| 546 | "map-flow", | |
| 547 | "map-lane", | |
| 548 | "map-lane-caret", | |
| 549 | "map-lane-crew", | |
| 550 | "map-lane-head", | |
| 551 | "map-lane-name", | |
| 552 | "map-lane-word", | |
| 553 | "map-lanes", | |
| 554 | "map-level", | |
| 555 | "map-list", | |
| 556 | "map-main", | |
| 557 | "map-meta", | |
| 558 | "map-node", | |
| 559 | "map-pillars", | |
| 560 | "map-rule", | |
| 561 | "map-spend", | |
| 562 | "map-spend-fill", | |
| 563 | "map-spend-track", | |
| 564 | "map-svg", | |
| 565 | "map-title", | |
| 566 | "map-tool", | |
| 567 | "map-tools", | |
| 568 | "map-who", | |
| 569 | "map-who-i", | |
| 570 | "map-who-n", | |
| 571 | "map-word", | |
| 572 | "map-wrap", | |
| 573 | "mark", | |
| 574 | "mb", | |
| 575 | "menu", | |
| 576 | "menu-item", | |
| 577 | "menu-panel", | |
| 578 | "menu-sep", | |
| 579 | "menu-title", | |
| 580 | "merge-box", | |
| 581 | "merge-form", | |
| 582 | "merge-headline", | |
| 583 | "merge-sub", | |
| 584 | "mirror-form", | |
| 585 | "mirrored", | |
| 586 | "ml", | |
| 587 | "mono", | |
| 588 | "ms-actions", | |
| 589 | "ms-bar", | |
| 590 | "ms-count", | |
| 591 | "ms-figures", | |
| 592 | "ms-head", | |
| 593 | "ms-limb", | |
| 594 | "ms-limbs", | |
| 595 | "ms-more", | |
| 596 | "ms-move", | |
| 597 | "ms-name", | |
| 598 | "ms-planner", | |
| 599 | "ms-rows", | |
| 600 | "ms-section", | |
| 601 | "ms-state", | |
| 602 | "ms-top", | |
| 603 | "ms-trim", | |
| 604 | "ms-weight", | |
| 605 | "mt", | |
| 606 | "muted", | |
| 607 | "name-mark", | |
| 608 | "narrow", | |
| 609 | "narrow-wide", | |
| 610 | "needs-you", | |
| 611 | "next-crew", | |
| 612 | "next-step", | |
| 613 | "next-step-apps", | |
| 614 | "next-step-main", | |
| 615 | "next-step-mark", | |
| 616 | "next-step-title", | |
| 617 | "next-steps", | |
| 618 | "no-hits", | |
| 619 | "nonl", | |
| 620 | "now", | |
| 621 | "num", | |
| 622 | "number", | |
| 623 | "ok", | |
| 624 | "origin-chip", | |
| 625 | "origin-link", | |
| 626 | "origin-mark", | |
| 627 | "origin-name", | |
| 628 | "origin-row", | |
| 629 | "owner", | |
| 630 | "page-head", | |
| 631 | "pager", | |
| 632 | "palette", | |
| 633 | "palette-empty", | |
| 634 | "palette-foot", | |
| 635 | "palette-group", | |
| 636 | "palette-heading", | |
| 637 | "palette-input", | |
| 638 | "palette-item", | |
| 639 | "palette-list", | |
| 640 | "passkey-note", | |
| 641 | "path-head", | |
| 642 | "pill", | |
| 643 | "pinned-failures", | |
| 644 | "pinned-slot", | |
| 645 | "policy-line", | |
| 646 | "preview", | |
| 647 | "propose-line", | |
| 648 | "prose", | |
| 649 | "qf", | |
| 650 | "quiet-details", | |
| 651 | "rc", | |
| 652 | "rc-body", | |
| 653 | "rc-form", | |
| 654 | "rc-head", | |
| 655 | "rc-hidden", | |
| 656 | "rc-outdated", | |
| 657 | "rc-resolve", | |
| 658 | "rc-resolved", | |
| 659 | "rc-resolved-line", | |
| 660 | "reduced", | |
| 661 | "repo-actions", | |
| 662 | "repo-audience", | |
| 663 | "repo-desc", | |
| 664 | "repo-head", | |
| 665 | "repo-layout", | |
| 666 | "repo-lineage", | |
| 667 | "repo-origin", | |
| 668 | "repo-title", | |
| 669 | "repo-where", | |
| 670 | "review-body", | |
| 671 | "review-card", | |
| 672 | "review-verdicts", | |
| 673 | "reviews-chip", | |
| 674 | "row", | |
| 675 | "row-actions", | |
| 676 | "row-lead", | |
| 677 | "row-main", | |
| 678 | "row-meta", | |
| 679 | "row-title", | |
| 680 | "row-trail", | |
| 681 | "run-actions", | |
| 682 | "run-jump", | |
| 683 | "run-live", | |
| 684 | "run-log", | |
| 685 | "run-steer", | |
| 686 | "run-summary", | |
| 687 | "search", | |
| 688 | "seg", | |
| 689 | "sep", | |
| 690 | "settings", | |
| 691 | "settings-main", | |
| 692 | "settings-nav", | |
| 693 | "sha", | |
| 694 | "shelf-desc", | |
| 695 | "shelf-facts", | |
| 696 | "shelf-meta", | |
| 697 | "shell", | |
| 698 | "shell-wide", | |
| 699 | "skip", | |
| 700 | "slash", | |
| 701 | "small", | |
| 702 | "spacer", | |
| 703 | "spend-table", | |
| 704 | "spend-totals", | |
| 705 | "split", | |
| 706 | "sprite", | |
| 707 | "sr-only", | |
| 708 | "stack", | |
| 709 | "stack-tight", | |
| 710 | "stage-detail", | |
| 711 | "stage-dot", | |
| 712 | "stages", | |
| 713 | "step-state", | |
| 714 | "steps", | |
| 715 | "sticky-action", | |
| 716 | "studio", | |
| 717 | "studio-ask-actions", | |
| 718 | "studio-crew", | |
| 719 | "studio-head", | |
| 720 | "studio-list", | |
| 721 | "studio-live", | |
| 722 | "studio-main", | |
| 723 | "studio-preview", | |
| 724 | "studio-preview-head", | |
| 725 | "studio-rail", | |
| 726 | "studio-tell", | |
| 727 | "studio-who", | |
| 728 | "studio-word", | |
| 729 | "sub", | |
| 730 | "swatch", | |
| 731 | "sys-pair", | |
| 732 | "ta-c", | |
| 733 | "ta-r", | |
| 734 | "table", | |
| 735 | "table-scroll", | |
| 736 | "tabs", | |
| 737 | "tell-here", | |
| 738 | "terminal-line", | |
| 739 | "tests-bar", | |
| 740 | "tests-words", | |
| 741 | "theme-apply", | |
| 742 | "theme-option", | |
| 743 | "tick", | |
| 744 | "timeline", | |
| 745 | "tl-dot", | |
| 746 | "tl-item", | |
| 747 | "toast", | |
| 748 | "toast-body", | |
| 749 | "tok", | |
| 750 | "tok-attribute", | |
| 751 | "tok-comment", | |
| 752 | "tok-constant", | |
| 753 | "tok-decorator", | |
| 754 | "tok-deleted", | |
| 755 | "tok-emphasis", | |
| 756 | "tok-function", | |
| 757 | "tok-heading", | |
| 758 | "tok-inserted", | |
| 759 | "tok-keyword", | |
| 760 | "tok-link", | |
| 761 | "tok-meta", | |
| 762 | "tok-number", | |
| 763 | "tok-operator", | |
| 764 | "tok-plain", | |
| 765 | "tok-property", | |
| 766 | "tok-punctuation", | |
| 767 | "tok-regex", | |
| 768 | "tok-string", | |
| 769 | "tok-tag", | |
| 770 | "tok-type", | |
| 771 | "tok-variable", | |
| 772 | "topbar", | |
| 773 | "topbar-asks", | |
| 774 | "topbar-search", | |
| 775 | "topbar-tools", | |
| 776 | "tree-row", | |
| 777 | "tree-total", | |
| 778 | "truncate", | |
| 779 | "txt", | |
| 780 | "verdict-stale", | |
| 781 | "wall-cells", | |
| 782 | "wd", | |
| 783 | "where", | |
| 784 | "where-block", | |
| 785 | "where-dot", | |
| 786 | "where-note", | |
| 787 | "where-pin", | |
| 788 | "where-place", | |
| 789 | "where-sep", | |
| 790 | "who", | |
| 791 | "wordmark" | |
| 792 | ] | |
| 793 | } |
docs/design/plan.md added +26
| @@ -0,0 +1,26 @@ | ||
| 1 | # Design plan (#2678) | |
| 2 | ||
| 3 | The line is held by `bun scripts/design-lint.ts`: a pull may not add a colour | |
| 4 | literal, an off-scale `font-size`, or a component class beyond | |
| 5 | `docs/design/baseline.json`. Paying down lowers the baseline | |
| 6 | (`bun scripts/design-lint.ts --update`, reviewed in the pull); it never rises | |
| 7 | without a DESIGN.md §5 entry first. | |
| 8 | ||
The plan says each item is one issue, but none were filed. Create them via the API and link the dependencies (items 2 and 3 are already #2665 and #2666), so the plan lives in the map and not only in a markdown list. |
||
| 9 | Each item is one filed issue, small enough for one run; items 4–9 wait for the screenshot sweep (#2690) in the map. | |
| 10 | ||
| 11 | 1. **#2690 Screenshot sweep** — every top-level page, desktop + 360px, light + dark, | |
| 12 | committed under `docs/design/shots/`; fills the gap in the audit. | |
| 13 | 2. **Home as KPIs** — #2665 (already filed). | |
| 14 | 3. **Boards selectors** — #2666 (already filed). | |
| 15 | 4. **#2691 `/system` on the system** — rebuild with `card`, `row`, `pill`, tokens only. | |
| 16 | 5. **#2692 Merge usage / spend / capacity / telemetry** into one "Usage" page with `tabs()`. | |
| 17 | 6. **#2693 Fold `/flow` into boards** as panels; drop the topbar Flow button. | |
| 18 | 7. **#2694 Fold resilience into backups.** | |
| 19 | 8. **#2695 `/map`: account for it** — what it shows that nothing else does; cut the | |
| 20 | rest; move its 12 off-scale sizes and 3 colours onto tokens. | |
| 21 | 9. **#2696 Operator views** (`audit`, `waves`, `variants`, `looks`, `returns`, | |
| 22 | `chain`): one line each on why it is a page; retire or turn into board panels. | |
| 23 | 10. **#2697 Tokenise `style/content.ts`** — 23 colours, 31 sizes; the largest debt. | |
| 24 | 11. **#2698 Tokenise `style/components.ts`, `studio/home.ts`, `pages/looks.ts`, `pages/site.ts`.** | |
| 25 | 12. **#2699 Top bar diet** — at most search, New, and the user menu on the left of the fold. | |
| 26 | 13. **#2700 Class rule against DESIGN.md §5** — the baseline holds 619 grandfathered classes, so today the check stops new ones, not old ones. |
scripts/design-lint.ts added +139
| @@ -0,0 +1,139 @@ | ||
| 1 | /** | |
| 2 | * The design check (#2678): a pull may not add a colour, a font size or a | |
| 3 | * component class the design system (`docs/DESIGN.md`) does not already have. | |
| 4 | * | |
| 5 | * The system is not new; the pages drifted from it. So this is a ratchet, not | |
| 6 | * a purge: `docs/design/baseline.json` records what the drift is today, per | |
| 7 | * file, and a pull fails when it goes up. Paying a debt down is always allowed | |
| 8 | * (and `bun scripts/design-lint.ts --update` writes the lower number down). | |
| 9 | * | |
| 10 | * colour a hex, rgb(), hsl() or named-colour literal, or a color-mix() that | |
| 11 | * mixes no `var(--token)`, in src/web — a colour is a token | |
| 12 | * size a font-size in px off the type scale (§3) | |
| 13 | * class a class defined in a `/* css *\/` template that the baseline does | |
| 14 | * not know — a new component must be named in DESIGN.md first | |
| 15 | * | |
| 16 | * Known gaps, so nobody mistakes the gate for more than it is: the class rule | |
| 17 | * checks the baseline, not DESIGN.md §5 (619 classes are grandfathered until | |
| 18 | * #2700); colours built in JS strings other than the forms above, `oklch()`, | |
| 19 | * `lab()` and system colours are not seen; sizes in `rem`/`em` are not | |
| 20 | * counted. Stricter patterns belong here, with a baseline update. | |
| 21 | * | |
| 22 | * The theme palettes (`src/design/themes.ts`) are where colours live and are | |
| 23 | * not scanned. Plain data in, plain data out: `scan` takes files, `compare` | |
| 24 | * takes two scans. | |
| 25 | */ | |
| 26 | ||
| 27 | import { readdirSync, readFileSync, writeFileSync } from "node:fs"; | |
| 28 | import { join, relative } from "node:path"; | |
| 29 | ||
| 30 | /** The type scale in docs/DESIGN.md §3, in px. */ | |
| 31 | export const TYPE_SCALE: readonly number[] = [11.5, 12.5, 13, 14, 15, 16, 20]; | |
| 32 | ||
| 33 | const NAMED = [ | |
| 34 | "black", "white", "red", "green", "blue", "yellow", "orange", "purple", "pink", | |
| 35 | "gray", "grey", "silver", "gold", "navy", "teal", "cyan", "magenta", "maroon", | |
| 36 | "olive", "lime", "aqua", "fuchsia", "brown", "crimson", "coral", "indigo", "violet", | |
| 37 | ]; | |
| 38 | const NAMED_RE = new RegExp( | |
| 39 | `(?:color|background(?:-color)?|border(?:-[a-z]+)*|fill|stroke|outline(?:-color)?)\\s*:[^;{}]*\\b(?:${NAMED.join("|")})\\b`, | |
| 40 | "gi", | |
| 41 | ); | |
| 42 | ||
| 43 | export type Counts = { colour: number; size: number }; | |
| 44 | export type Scan = { | |
| 45 | files: Record<string, Counts>; | |
| 46 | classes: string[]; | |
| 47 | }; | |
| 48 | ||
| 49 | const SKIP = /(^|\/)(og\/vendor)\//; | |
| 50 | ||
| 51 | export function sourceFiles(root: string): string[] { | |
| 52 | const out: string[] = []; | |
| 53 | const walk = (dir: string) => { | |
| 54 | for (const e of readdirSync(dir, { withFileTypes: true })) { | |
| 55 | const p = join(dir, e.name); | |
| 56 | if (e.isDirectory()) walk(p); | |
| 57 | else if (e.name.endsWith(".ts") && !SKIP.test(p)) out.push(p); | |
| 58 | } | |
| 59 | }; | |
| 60 | walk(join(root, "src/web")); | |
| 61 | return out.sort(); | |
| 62 | } | |
| 63 | ||
| 64 | /** The CSS inside `/* css *\/ \`…\`` templates — the only place a class is defined. */ | |
| 65 | function cssBlocks(text: string): string[] { | |
| 66 | const out: string[] = []; | |
| 67 | const re = /\/\* css \*\/\s*`((?:[^`\\]|\\.)*)`/g; | |
| 68 | for (let m = re.exec(text); m; m = re.exec(text)) out.push(m[1]!); | |
| 69 | return out; | |
| 70 | } | |
| 71 | ||
| 72 | export function countFile(text: string): Counts { | |
| 73 | const mixes = (text.match(/color-mix\([^;{}]*\)/g) ?? []).filter((m) => !m.includes("var(")).length; | |
| 74 | const colour = | |
| 75 | (text.match(/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b(?![0-9a-zA-Z_-])/g) ?? []).length + | |
| 76 | (text.match(/\b(?:rgba?|hsla?)\(/g) ?? []).length + | |
| 77 | cssBlocks(text).reduce((n, css) => n + (css.match(NAMED_RE) ?? []).length, 0) + | |
| 78 | mixes; | |
| 79 | let size = 0; | |
| 80 | for (const m of text.matchAll(/font-size:\s*([0-9.]+)px/g)) { | |
| 81 | if (!TYPE_SCALE.includes(Number(m[1]))) size++; | |
| 82 | } | |
| 83 | return { colour, size }; | |
| 84 | } | |
| 85 | ||
| 86 | export function classesIn(text: string): string[] { | |
| 87 | const set = new Set<string>(); | |
| 88 | for (const css of cssBlocks(text)) { | |
| 89 | const bare = css.replace(/\/\*[\s\S]*?\*\//g, ""); | |
| 90 | // Selectors only: what precedes a `{`, minus declarations. | |
| 91 | for (const sel of bare.matchAll(/([^{};]+)\{/g)) { | |
| 92 | for (const c of sel[1]!.matchAll(/(?:^|[\s,>+~(\]:)])\.([a-z][a-z0-9_-]*)/gi)) set.add(c[1]!); | |
| 93 | } | |
| 94 | } | |
| 95 | return [...set]; | |
| 96 | } | |
| 97 | ||
| 98 | export function scan(root: string): Scan { | |
| 99 | const files: Record<string, Counts> = {}; | |
| 100 | const classes = new Set<string>(); | |
| 101 | for (const f of sourceFiles(root)) { | |
| 102 | const text = readFileSync(f, "utf8"); | |
| 103 | const c = countFile(text); | |
| 104 | if (c.colour || c.size) files[relative(root, f)] = c; | |
| 105 | for (const k of classesIn(text)) classes.add(k); | |
| 106 | } | |
| 107 | return { files, classes: [...classes].sort() }; | |
| 108 | } | |
| 109 | ||
| 110 | /** What a pull made worse, one line each. Empty means the check passes. */ | |
| 111 | export function compare(base: Scan, now: Scan): string[] { | |
| 112 | const out: string[] = []; | |
| 113 | for (const [file, c] of Object.entries(now.files)) { | |
| 114 | const b = base.files[file] ?? { colour: 0, size: 0 }; | |
| 115 | if (c.colour > b.colour) out.push(`${file}: ${c.colour - b.colour} new colour literal(s) — use a token from themes.ts (var(--…))`); | |
| 116 | if (c.size > b.size) out.push(`${file}: ${c.size - b.size} new off-scale font-size — the scale is ${TYPE_SCALE.join(", ")}px`); | |
| 117 | } | |
| 118 | const known = new Set(base.classes); | |
| 119 | for (const k of now.classes) { | |
| 120 | if (!known.has(k)) out.push(`.${k}: a component class the design system does not have — name it in docs/DESIGN.md §5, then run --update`); | |
| 121 | } | |
| 122 | return out; | |
| 123 | } | |
| 124 | ||
| 125 | const BASELINE = "docs/design/baseline.json"; | |
| 126 | ||
| 127 | if (import.meta.main) { | |
| 128 | const root = process.cwd(); | |
| 129 | const now = scan(root); | |
| 130 | if (process.argv.includes("--update")) { | |
| 131 | writeFileSync(join(root, BASELINE), JSON.stringify(now, null, 2) + "\n"); | |
| 132 | console.log(`wrote ${BASELINE}`); | |
| 133 | } else { | |
| 134 | const problems = compare(JSON.parse(readFileSync(join(root, BASELINE), "utf8")), now); | |
| 135 | for (const p of problems) console.error(p); | |
| 136 | if (problems.length) process.exit(1); | |
| 137 | console.log("design: no colour, size or component added outside the system"); | |
| 138 | } | |
| 139 | } |
test/design-lint.test.ts added +32
| @@ -0,0 +1,32 @@ | ||
| 1 | import { describe, expect, test } from "bun:test"; | |
| 2 | import { readFileSync } from "node:fs"; | |
| 3 | import { classesIn, compare, countFile, scan } from "../scripts/design-lint.ts"; | |
| 4 | ||
| 5 | describe("design check: nothing added outside the system (#2678)", () => { | |
| 6 | test("the tree is no worse than its baseline", () => { | |
| 7 | const base = JSON.parse(readFileSync("docs/design/baseline.json", "utf8")); | |
| 8 | expect(compare(base, scan("."))).toEqual([]); | |
| 9 | }); | |
| 10 | ||
| 11 | test("counts colour literals and off-scale sizes, not on-scale ones", () => { | |
| 12 | expect(countFile("a{color:#fff;background:#a1b2c3;border:1px solid rgba(0,0,0,.1)}")).toEqual({ colour: 3, size: 0 }); | |
| 13 | expect(countFile("a{font-size: 13px} b{font-size: 14px} c{font-size: 13.5px} d{font-size: 72px}")).toEqual({ colour: 0, size: 2 }); | |
| 14 | expect(countFile("x = /* css */ `a{color: red; border: 1px solid navy; background: var(--card)}`")).toEqual({ colour: 2, size: 0 }); | |
| 15 | expect(countFile("a{background:color-mix(in srgb, red 10%, white)} b{background:color-mix(in srgb, var(--x) 12%, transparent)}").colour).toBe(1); | |
| 16 | expect(countFile("a{color:var(--fg)} // issue #2678")).toEqual({ colour: 0, size: 0 }); | |
| 17 | }); | |
| 18 | ||
| 19 | test("finds the classes a css template defines, and only those", () => { | |
| 20 | const text = "const x = a.ts; const css = /* css */ `.btn:hover, .card > .row-title { color: red }\n.new-thing { top: 0.5px }`; foo.bar"; | |
| 21 | expect(classesIn(text).sort()).toEqual(["btn", "card", "new-thing", "row-title"]); | |
| 22 | }); | |
| 23 | ||
| 24 | test("fails a pull that adds a colour, a size or a component", () => { | |
| 25 | const base = { files: { "src/web/x.ts": { colour: 1, size: 1 } }, classes: ["btn"] }; | |
| 26 | expect(compare(base, base)).toEqual([]); | |
| 27 | expect(compare(base, { files: { "src/web/x.ts": { colour: 0, size: 0 } }, classes: ["btn"] })).toEqual([]); | |
| 28 | expect(compare(base, { ...base, files: { "src/web/x.ts": { colour: 2, size: 1 } } })).toHaveLength(1); | |
| 29 | expect(compare(base, { ...base, files: { "src/web/y.ts": { colour: 0, size: 1 } } })).toHaveLength(1); | |
| 30 | expect(compare(base, { ...base, classes: ["btn", "gizmo"] })[0]).toContain(".gizmo"); | |
| 31 | }); | |
| 32 | }); |
new above
offline
A design audit with no screenshots cannot judge layout shift, 360px, contrast or dark mode, which is the experience. Add the sweep (plan item 1) in this pull, or narrow the title and claims to a source-drift report.