Public: anyone can read this; the crew can change it.
Design: the class rule checks DESIGN.md section 5, not only the baseline #2707
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2700-design-the-class-rule-checks-design-md-s → main 7 commits fork
10 files
changed +151
−714
README.md +8 −7
| @@ -1214,19 +1214,20 @@ forge icon. | ||
| 1214 | 1214 | |
| 1215 | 1215 | sh |
| 1216 | 1216 | bun test # protocol-level + adversarial suite |
| 1217 | bun scripts/design-lint.ts # no colour, off-scale font-size or component class beyond docs/design/baseline.json | |
| 1217 | bun scripts/design-lint.ts # no colour or off-scale font-size beyond docs/design/baseline.json, no class docs/DESIGN.md §5 does not name | |
| 1218 | 1218 | bun x tsc --noEmit # zero type errors is the standard |
| 1219 | 1219 | bun run check # exactly what the forge runs against every push |
| 1220 | 1220 | bun run check:record # the same, and rewrites scripts/test-times.json |
| 1221 | 1221 | |
| 1222 | 1222 | |
| 1223 | 1223 | The design check (`test/design-lint.test.ts`, run in the gate) fails a pull that |
| 1224 | adds a colour literal, an off-scale `font-size` or a component class beyond | |
| 1225 | `docs/design/baseline.json`; the message names the file. The way out is to use | |
| 1226 | a token (`var(--…)`) or a named component from `docs/DESIGN.md`; when a change | |
| 1227 | lowers the counts or adds a component to DESIGN.md §5, run | |
| 1228 | `bun scripts/design-lint.ts --update` and commit the baseline. See | |
| 1229 | `docs/design/plan.md`. | |
| 1224 | adds a colour literal or an off-scale `font-size` beyond | |
| 1225 | `docs/design/baseline.json`, or defines a class `docs/DESIGN.md` §5 does not | |
| 1226 | name; the message names the file or the class. The way out is to use a token | |
| 1227 | (`var(--…)`) or a component §5 names; a new component goes into the §5 table | |
| 1228 | first. When a change lowers the counts, run `bun scripts/design-lint.ts | |
| 1229 | --update` and commit the baseline; when it deletes the last rule for a class | |
| 1230 | in the §5d register, strike the name there too. See `docs/design/plan.md`. | |
| 1230 | 1231 | |
| 1231 | 1232 | `bun run check` is the gate: the same command `forge.json` declares, so what |
| 1232 | 1233 | it says here is what the deploy will say. It type-checks once, then runs the |
docs/DESIGN.md +32
Diff not shown — a line 2,326 characters wide (limit 2,000). +32
View filedocs/design/baseline.json +1 −630
| @@ -16,638 +16,9 @@ | ||
| 16 | 16 | "colour": 2, |
| 17 | 17 | "size": 0 |
| 18 | 18 | }, |
| 19 | "src/web/studio/style.ts": { | |
| 20 | "colour": 2, | |
| 21 | "size": 1 | |
| 22 | }, | |
| 23 | 19 | "src/web/style/components.ts": { |
| 24 | 20 | "colour": 3, |
| 25 | 21 | "size": 2 |
| 26 | }, | |
| 27 | "src/web/style/content.ts": { | |
| 28 | "colour": 19, | |
| 29 | "size": 31 | |
| 30 | 22 | } |
| 31 | }, | |
| 32 | "classes": [ | |
| 33 | "about", | |
| 34 | "act", | |
| 35 | "add", | |
| 36 | "agent-brief", | |
| 37 | "ahead", | |
| 38 | "app-shelf", | |
| 39 | "app-strip", | |
| 40 | "app-tile", | |
| 41 | "app-tile-body", | |
| 42 | "app-tile-desc", | |
| 43 | "app-tile-look", | |
| 44 | "app-tile-name", | |
| 45 | "app-url", | |
| 46 | "ask-acts", | |
| 47 | "ask-card", | |
| 48 | "ask-group", | |
| 49 | "ask-kind", | |
| 50 | "ask-meta", | |
| 51 | "ask-more-acts", | |
| 52 | "ask-pick", | |
| 53 | "ask-pick-hit", | |
| 54 | "ask-short", | |
| 55 | "ask-waiting", | |
| 56 | "audit", | |
| 57 | "audit-alarm", | |
| 58 | "audit-answer", | |
| 59 | "audit-app", | |
| 60 | "audit-cells", | |
| 61 | "audit-change", | |
| 62 | "audit-h2", | |
| 63 | "audit-problems", | |
| 64 | "audit-said", | |
| 65 | "audit-sum", | |
| 66 | "audit-trail", | |
| 67 | "audit-unread", | |
| 68 | "audit-wide", | |
| 69 | "avatar", | |
| 70 | "backups-drill", | |
| 71 | "backups-error", | |
| 72 | "backups-now", | |
| 73 | "backups-restore", | |
| 74 | "bad", | |
| 75 | "badge", | |
| 76 | "bar", | |
| 77 | "bare", | |
| 78 | "behind", | |
| 79 | "bform", | |
| 80 | "bgrid", | |
| 81 | "big", | |
| 82 | "blob-view", | |
| 83 | "bmanage", | |
| 84 | "board", | |
| 85 | "board-about", | |
| 86 | "board-answer", | |
| 87 | "board-app", | |
| 88 | "board-app-mode", | |
| 89 | "board-app-name", | |
| 90 | "board-app-nums", | |
| 91 | "board-app-week", | |
| 92 | "board-asked", | |
| 93 | "board-asks", | |
| 94 | "board-bar", | |
| 95 | "board-brand", | |
| 96 | "board-card", | |
| 97 | "board-clock", | |
| 98 | "board-count", | |
| 99 | "board-decision", | |
| 100 | "board-decision-main", | |
| 101 | "board-declined", | |
| 102 | "board-declined-row", | |
| 103 | "board-declined-says", | |
| 104 | "board-dot", | |
| 105 | "board-elapsed", | |
| 106 | "board-fill", | |
| 107 | "board-forge", | |
| 108 | "board-grid", | |
| 109 | "board-group", | |
| 110 | "board-h", | |
| 111 | "board-inferred", | |
| 112 | "board-key", | |
| 113 | "board-keys", | |
| 114 | "board-kind", | |
| 115 | "board-landed", | |
| 116 | "board-landed-meta", | |
| 117 | "board-landed-title", | |
| 118 | "board-list", | |
| 119 | "board-live", | |
| 120 | "board-outcome", | |
| 121 | "board-p", | |
| 122 | "board-parked-row", | |
| 123 | "board-pillar", | |
| 124 | "board-pillars", | |
| 125 | "board-pillars-h", | |
| 126 | "board-quiet", | |
| 127 | "board-region", | |
| 128 | "board-run", | |
| 129 | "board-run-meta", | |
| 130 | "board-run-title", | |
| 131 | "board-run-who", | |
| 132 | "board-scope", | |
| 133 | "board-spend", | |
| 134 | "board-spend-detail", | |
| 135 | "board-state", | |
| 136 | "board-sub", | |
| 137 | "board-sub-name", | |
| 138 | "board-sub-scope", | |
| 139 | "board-subs", | |
| 140 | "board-thumb", | |
| 141 | "board-top", | |
| 142 | "board-when", | |
| 143 | "board-window", | |
| 144 | "board-window-name", | |
| 145 | "board-windows", | |
| 146 | "boards-result", | |
| 147 | "bp-h2", | |
| 148 | "bp-h3", | |
| 149 | "bp-w2", | |
| 150 | "bp-w3", | |
| 151 | "branch-div", | |
| 152 | "branch-row", | |
| 153 | "break", | |
| 154 | "btn", | |
| 155 | "btn-icon", | |
| 156 | "byline", | |
| 157 | "byline-name", | |
| 158 | "card", | |
| 159 | "card-body", | |
| 160 | "card-foot", | |
| 161 | "card-head", | |
| 162 | "cat", | |
| 163 | "chart", | |
| 164 | "chart-box", | |
| 165 | "check", | |
| 166 | "check-progress", | |
| 167 | "check-steps", | |
| 168 | "checks-line", | |
| 169 | "claim-link-box", | |
| 170 | "clone", | |
| 171 | "clone-text", | |
| 172 | "close-into", | |
| 173 | "close-reason", | |
| 174 | "code", | |
| 175 | "code-hit", | |
| 176 | "code-layout", | |
| 177 | "code-line", | |
| 178 | "code-main", | |
| 179 | "code-split", | |
| 180 | "color-field", | |
| 181 | "comment", | |
| 182 | "comment-body", | |
| 183 | "comment-form", | |
| 184 | "commit-body", | |
| 185 | "commit-head", | |
| 186 | "composer", | |
| 187 | "composer-bar", | |
| 188 | "connect-more", | |
| 189 | "connect-path", | |
| 190 | "count", | |
| 191 | "counts", | |
| 192 | "crumbs", | |
| 193 | "dash", | |
| 194 | "dc", | |
| 195 | "dc-review", | |
| 196 | "dcx", | |
| 197 | "del", | |
| 198 | "device-code", | |
| 199 | "dh", | |
| 200 | "dialog", | |
| 201 | "dialog-body", | |
| 202 | "dialog-foot", | |
| 203 | "dialog-head", | |
| 204 | "diff", | |
| 205 | "diff-body", | |
| 206 | "diff-file", | |
| 207 | "diff-head", | |
| 208 | "diff-note", | |
| 209 | "diff-path", | |
| 210 | "diff-stat", | |
| 211 | "diff-stub", | |
| 212 | "diff-summary", | |
| 213 | "diff-table", | |
| 214 | "diff-viewed", | |
| 215 | "dl", | |
| 216 | "dl-review", | |
| 217 | "dn", | |
| 218 | "dn-comment", | |
| 219 | "domain-records", | |
| 220 | "dot", | |
| 221 | "edit-row", | |
| 222 | "editor-commit", | |
| 223 | "editor-gutter", | |
| 224 | "editor-head", | |
| 225 | "editor-hint", | |
| 226 | "editor-mirror", | |
| 227 | "editor-path", | |
| 228 | "editor-preview", | |
| 229 | "editor-stack", | |
| 230 | "editor-tab", | |
| 231 | "editor-target", | |
| 232 | "editor-wrap", | |
| 233 | "empty", | |
| 234 | "empty-title", | |
| 235 | "env-card", | |
| 236 | "env-error", | |
| 237 | "env-facts", | |
| 238 | "error", | |
| 239 | "error-note", | |
| 240 | "everyday-ask", | |
| 241 | "ex-dir", | |
| 242 | "ex-empty", | |
| 243 | "ex-file", | |
| 244 | "ex-find", | |
| 245 | "ex-head", | |
| 246 | "ex-list", | |
| 247 | "ex-more", | |
| 248 | "ex-name", | |
| 249 | "ex-ref", | |
| 250 | "ex-twist", | |
| 251 | "explore-filters", | |
| 252 | "explore-line", | |
| 253 | "explore-main", | |
| 254 | "explore-name", | |
| 255 | "explore-row", | |
| 256 | "explorer", | |
| 257 | "explorer-panel", | |
| 258 | "explorer-toggle", | |
| 259 | "fcard", | |
| 260 | "fcard-acts", | |
| 261 | "fcard-app", | |
| 262 | "fcard-form", | |
| 263 | "fcard-head", | |
| 264 | "fcard-more", | |
| 265 | "fcard-reason", | |
| 266 | "fcard-reply", | |
| 267 | "fcard-text", | |
| 268 | "fcard-unblock", | |
| 269 | "fcard-who", | |
| 270 | "fcard-why", | |
| 271 | "feed", | |
| 272 | "feed-head", | |
| 273 | "fg-s0", | |
| 274 | "fg-s1", | |
| 275 | "fg-s2", | |
| 276 | "fg-s3", | |
| 277 | "fg-s4", | |
| 278 | "fg-s5", | |
| 279 | "fg-s6", | |
| 280 | "fg-s7", | |
| 281 | "field", | |
| 282 | "field-row", | |
| 283 | "files-head", | |
| 284 | "filters", | |
| 285 | "finder", | |
| 286 | "finder-note", | |
| 287 | "flag-form", | |
| 288 | "fleet-assume", | |
| 289 | "fleet-axis", | |
| 290 | "fleet-grid", | |
| 291 | "fleet-legend", | |
| 292 | "fleet-panel-name", | |
| 293 | "fleet-pick", | |
| 294 | "fleet-svg", | |
| 295 | "fleet-swatch", | |
| 296 | "fleet-table", | |
| 297 | "fleet-ticks", | |
| 298 | "fleet-window", | |
| 299 | "fleet-windows", | |
| 300 | "flow-bar", | |
| 301 | "flow-out", | |
| 302 | "following", | |
| 303 | "following-actions", | |
| 304 | "following-eyebrow", | |
| 305 | "following-foot", | |
| 306 | "following-head", | |
| 307 | "following-sentence", | |
| 308 | "foot", | |
| 309 | "forges-sent", | |
| 310 | "fork-bar", | |
| 311 | "form-actions", | |
| 312 | "front-admin", | |
| 313 | "front-controls", | |
| 314 | "front-crew", | |
| 315 | "front-featured", | |
| 316 | "front-featured-body", | |
| 317 | "front-featured-look", | |
| 318 | "front-issues", | |
| 319 | "front-live", | |
| 320 | "front-more", | |
| 321 | "front-pick", | |
| 322 | "front-pin-any", | |
| 323 | "front-weights", | |
| 324 | "frozen", | |
| 325 | "frozen-form", | |
| 326 | "frozen-sentence", | |
| 327 | "grants", | |
| 328 | "grants-foot", | |
| 329 | "grow", | |
| 330 | "handoff", | |
| 331 | "handoff-links", | |
| 332 | "headroom", | |
| 333 | "held-bar", | |
| 334 | "held-done_on_main", | |
| 335 | "held-key", | |
| 336 | "held-open_pull", | |
| 337 | "held-parked_twice", | |
| 338 | "held-planner", | |
| 339 | "held-red_fix", | |
| 340 | "held-run_cap", | |
| 341 | "held-running", | |
| 342 | "held-setting", | |
| 343 | "held-stalled", | |
| 344 | "held-startable", | |
| 345 | "hero", | |
| 346 | "hero-terminal", | |
| 347 | "hex", | |
| 348 | "hide-lg", | |
| 349 | "hide-sm", | |
| 350 | "hint", | |
| 351 | "home-app", | |
| 352 | "home-apps", | |
| 353 | "home-apps-verbs", | |
| 354 | "home-pick", | |
| 355 | "home-rail", | |
| 356 | "home-tabs", | |
| 357 | "ico", | |
| 358 | "ico-loader", | |
| 359 | "input", | |
| 360 | "issue-head", | |
| 361 | "issue-meta", | |
| 362 | "js-only", | |
| 363 | "kv", | |
| 364 | "label", | |
| 365 | "limb-confirm", | |
| 366 | "limb-rows", | |
| 367 | "limb-why", | |
| 368 | "lineage", | |
| 369 | "lineage-children", | |
| 370 | "lineage-here", | |
| 371 | "list", | |
| 372 | "live-note", | |
| 373 | "live-pill", | |
| 374 | "live-status", | |
| 375 | "live-toggle", | |
| 376 | "ln", | |
| 377 | "ln-forge", | |
| 378 | "ln-pulse", | |
| 379 | "ln-say", | |
| 380 | "ln-tool", | |
| 381 | "ln-you", | |
| 382 | "log-tail-row", | |
| 383 | "look", | |
| 384 | "look-attached", | |
| 385 | "look-attached-phone", | |
| 386 | "look-attachments", | |
| 387 | "look-card", | |
| 388 | "look-desktop", | |
| 389 | "look-now", | |
| 390 | "look-of", | |
| 391 | "look-pair", | |
| 392 | "look-phone", | |
| 393 | "look-thumb", | |
| 394 | "map-ask", | |
| 395 | "map-autonomy", | |
| 396 | "map-breath", | |
| 397 | "map-canvas", | |
| 398 | "map-card", | |
| 399 | "map-chart", | |
| 400 | "map-counts", | |
| 401 | "map-dot", | |
| 402 | "map-edge", | |
| 403 | "map-edge-crit", | |
| 404 | "map-edge-done", | |
| 405 | "map-empty", | |
| 406 | "map-flow", | |
| 407 | "map-lane", | |
| 408 | "map-lane-caret", | |
| 409 | "map-lane-crew", | |
| 410 | "map-lane-head", | |
| 411 | "map-lane-name", | |
| 412 | "map-lane-word", | |
| 413 | "map-lanes", | |
| 414 | "map-level", | |
| 415 | "map-list", | |
| 416 | "map-main", | |
| 417 | "map-meta", | |
| 418 | "map-node", | |
| 419 | "map-pillars", | |
| 420 | "map-rule", | |
| 421 | "map-spend", | |
| 422 | "map-spend-fill", | |
| 423 | "map-spend-track", | |
| 424 | "map-svg", | |
| 425 | "map-title", | |
| 426 | "map-tool", | |
| 427 | "map-tools", | |
| 428 | "map-who", | |
| 429 | "map-who-i", | |
| 430 | "map-who-n", | |
| 431 | "map-word", | |
| 432 | "map-wrap", | |
| 433 | "mark", | |
| 434 | "mb", | |
| 435 | "menu", | |
| 436 | "menu-item", | |
| 437 | "menu-panel", | |
| 438 | "menu-sep", | |
| 439 | "menu-title", | |
| 440 | "merge-box", | |
| 441 | "merge-form", | |
| 442 | "merge-headline", | |
| 443 | "merge-sub", | |
| 444 | "mirror-form", | |
| 445 | "mirrored", | |
| 446 | "ml", | |
| 447 | "mono", | |
| 448 | "ms-actions", | |
| 449 | "ms-bar", | |
| 450 | "ms-count", | |
| 451 | "ms-figures", | |
| 452 | "ms-head", | |
| 453 | "ms-limb", | |
| 454 | "ms-limbs", | |
| 455 | "ms-more", | |
| 456 | "ms-move", | |
| 457 | "ms-name", | |
| 458 | "ms-planner", | |
| 459 | "ms-rows", | |
| 460 | "ms-section", | |
| 461 | "ms-state", | |
| 462 | "ms-top", | |
| 463 | "ms-trim", | |
| 464 | "ms-weight", | |
| 465 | "mt", | |
| 466 | "muted", | |
| 467 | "name-mark", | |
| 468 | "narrow", | |
| 469 | "narrow-wide", | |
| 470 | "needs-you", | |
| 471 | "next-crew", | |
| 472 | "next-step", | |
| 473 | "next-step-apps", | |
| 474 | "next-step-main", | |
| 475 | "next-step-mark", | |
| 476 | "next-step-title", | |
| 477 | "next-steps", | |
| 478 | "no-hits", | |
| 479 | "nonl", | |
| 480 | "now", | |
| 481 | "num", | |
| 482 | "number", | |
| 483 | "ok", | |
| 484 | "origin-chip", | |
| 485 | "origin-link", | |
| 486 | "origin-mark", | |
| 487 | "origin-name", | |
| 488 | "origin-row", | |
| 489 | "owner", | |
| 490 | "page-head", | |
| 491 | "pager", | |
| 492 | "palette", | |
| 493 | "palette-empty", | |
| 494 | "palette-foot", | |
| 495 | "palette-group", | |
| 496 | "palette-heading", | |
| 497 | "palette-input", | |
| 498 | "palette-item", | |
| 499 | "palette-list", | |
| 500 | "passkey-note", | |
| 501 | "path-head", | |
| 502 | "pill", | |
| 503 | "pinned-failures", | |
| 504 | "pinned-slot", | |
| 505 | "policy-line", | |
| 506 | "preview", | |
| 507 | "propose-line", | |
| 508 | "prose", | |
| 509 | "qf", | |
| 510 | "quiet-details", | |
| 511 | "rc", | |
| 512 | "rc-body", | |
| 513 | "rc-form", | |
| 514 | "rc-head", | |
| 515 | "rc-hidden", | |
| 516 | "rc-outdated", | |
| 517 | "rc-resolve", | |
| 518 | "rc-resolved", | |
| 519 | "rc-resolved-line", | |
| 520 | "reduced", | |
| 521 | "repo-actions", | |
| 522 | "repo-audience", | |
| 523 | "repo-desc", | |
| 524 | "repo-head", | |
| 525 | "repo-layout", | |
| 526 | "repo-lineage", | |
| 527 | "repo-origin", | |
| 528 | "repo-title", | |
| 529 | "repo-where", | |
| 530 | "review-body", | |
| 531 | "review-card", | |
| 532 | "review-verdicts", | |
| 533 | "reviews-chip", | |
| 534 | "row", | |
| 535 | "row-actions", | |
| 536 | "row-lead", | |
| 537 | "row-main", | |
| 538 | "row-meta", | |
| 539 | "row-title", | |
| 540 | "row-trail", | |
| 541 | "run-actions", | |
| 542 | "run-jump", | |
| 543 | "run-live", | |
| 544 | "run-log", | |
| 545 | "run-steer", | |
| 546 | "run-summary", | |
| 547 | "search", | |
| 548 | "seg", | |
| 549 | "sep", | |
| 550 | "settings", | |
| 551 | "settings-main", | |
| 552 | "settings-nav", | |
| 553 | "sha", | |
| 554 | "shelf-desc", | |
| 555 | "shelf-facts", | |
| 556 | "shelf-meta", | |
| 557 | "shell", | |
| 558 | "shell-wide", | |
| 559 | "skip", | |
| 560 | "slash", | |
| 561 | "small", | |
| 562 | "spacer", | |
| 563 | "spend-table", | |
| 564 | "spend-totals", | |
| 565 | "split", | |
| 566 | "sprite", | |
| 567 | "sr-only", | |
| 568 | "stack", | |
| 569 | "stack-tight", | |
| 570 | "stage-detail", | |
| 571 | "stage-dot", | |
| 572 | "stages", | |
| 573 | "step-state", | |
| 574 | "steps", | |
| 575 | "sticky-action", | |
| 576 | "studio", | |
| 577 | "studio-ask-actions", | |
| 578 | "studio-crew", | |
| 579 | "studio-head", | |
| 580 | "studio-list", | |
| 581 | "studio-live", | |
| 582 | "studio-main", | |
| 583 | "studio-preview", | |
| 584 | "studio-preview-head", | |
| 585 | "studio-rail", | |
| 586 | "studio-tell", | |
| 587 | "studio-who", | |
| 588 | "studio-word", | |
| 589 | "sub", | |
| 590 | "swatch", | |
| 591 | "sys-pair", | |
| 592 | "ta-c", | |
| 593 | "ta-r", | |
| 594 | "table", | |
| 595 | "table-scroll", | |
| 596 | "tabs", | |
| 597 | "tell-here", | |
| 598 | "terminal-line", | |
| 599 | "tests-bar", | |
| 600 | "tests-words", | |
| 601 | "theme-apply", | |
| 602 | "theme-option", | |
| 603 | "tick", | |
| 604 | "timeline", | |
| 605 | "tl-dot", | |
| 606 | "tl-item", | |
| 607 | "toast", | |
| 608 | "toast-body", | |
| 609 | "tok", | |
| 610 | "tok-attribute", | |
| 611 | "tok-comment", | |
| 612 | "tok-constant", | |
| 613 | "tok-decorator", | |
| 614 | "tok-deleted", | |
| 615 | "tok-emphasis", | |
| 616 | "tok-function", | |
| 617 | "tok-heading", | |
| 618 | "tok-inserted", | |
| 619 | "tok-keyword", | |
| 620 | "tok-link", | |
| 621 | "tok-meta", | |
| 622 | "tok-number", | |
| 623 | "tok-operator", | |
| 624 | "tok-plain", | |
| 625 | "tok-property", | |
| 626 | "tok-punctuation", | |
| 627 | "tok-regex", | |
| 628 | "tok-string", | |
| 629 | "tok-tag", | |
| 630 | "tok-type", | |
| 631 | "tok-variable", | |
| 632 | "topbar", | |
| 633 | "topbar-asks", | |
| 634 | "topbar-search", | |
| 635 | "topbar-tools", | |
| 636 | "tree-row", | |
| 637 | "tree-total", | |
| 638 | "truncate", | |
| 639 | "txt", | |
| 640 | "verdict-stale", | |
| 641 | "wall-cells", | |
| 642 | "wd", | |
| 643 | "where", | |
| 644 | "where-block", | |
| 645 | "where-dot", | |
| 646 | "where-note", | |
| 647 | "where-pin", | |
| 648 | "where-place", | |
| 649 | "where-sep", | |
| 650 | "who", | |
| 651 | "wordmark" | |
| 652 | ] | |
| 23 | } | |
| 653 | 24 | } |
docs/design/plan.md +6 −5
| @@ -1,10 +1,11 @@ | ||
| 1 | 1 | # Design plan (#2678) |
| 2 | 2 | |
| 3 | 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. | |
| 4 | literal or an off-scale `font-size` beyond `docs/design/baseline.json`, nor | |
| 5 | define a class DESIGN.md §5 does not name. Paying down lowers the baseline | |
| 6 | (`bun scripts/design-lint.ts --update`, reviewed in the pull) and strikes | |
| 7 | retired classes from the §5d register; neither ever grows back without a §5 | |
| 8 | entry first. | |
| 8 | 9 | |
| 9 | 10 | 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 | |
| @@ -23,4 +24,4 @@ Each item is one filed issue, small enough for one run; items 4–9 wait for the | ||
| 23 | 24 | 10. **#2697 Tokenise `style/content.ts`** — 23 colours, 31 sizes; the largest debt. |
| 24 | 25 | 11. **#2698 Tokenise `style/components.ts`, `studio/home.ts`, `pages/looks.ts`, `pages/site.ts`.** |
| 25 | 26 | 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. | |
| 27 | 13. **#2700 Class rule against DESIGN.md §5** — done: 13 classes no page drew are gone, the other 505 page-local ones are registered in DESIGN.md §5d, and the check reads §5 rather than the baseline. Items 4–12 retire §5d entries as they redo each page. |
scripts/design-lint.ts +59 −15
| @@ -10,18 +10,19 @@ | ||
| 10 | 10 | * colour a hex, rgb(), hsl() or named-colour literal, or a color-mix() that |
| 11 | 11 | * mixes no `var(--token)`, in src/web — a colour is a token |
| 12 | 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 | |
| 13 | * class a class defined in a `/* css *\/` template that DESIGN.md §5 does | |
| 14 | * not name — a new component is named there first (#2700). §5d | |
| 15 | * registers the page-local classes that predate the rule; a name | |
| 16 | * it keeps after its last rule is gone fails too, so it only shrinks | |
| 15 | 17 | * |
| 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()`, | |
| 18 | * Known gaps, so nobody mistakes the gate for more than it is: colours built | |
| 19 | * in JS strings other than the forms above, `oklch()`, | |
| 19 | 20 | * `lab()` and system colours are not seen; sizes in `rem`/`em` are not |
| 20 | 21 | * counted. Stricter patterns belong here, with a baseline update. |
| 21 | 22 | * |
| 22 | 23 | * The theme palettes (`src/design/themes.ts`) are where colours live and are |
| 23 | 24 | * not scanned. Plain data in, plain data out: `scan` takes files, `compare` |
| 24 | * takes two scans. | |
| 25 | * takes the baseline, a scan and the text of DESIGN.md. | |
| 25 | 26 | */ |
| 26 | 27 | |
| 27 | 28 | import { readdirSync, readFileSync, writeFileSync } from "node:fs"; |
| @@ -41,10 +42,8 @@ const NAMED_RE = new RegExp( | ||
| 41 | 42 | ); |
| 42 | 43 | |
| 43 | 44 | export type Counts = { colour: number; size: number }; |
| 44 | export type Scan = { | |
| 45 | files: Record<string, Counts>; | |
| 46 | classes: string[]; | |
| 47 | }; | |
| 45 | export type Baseline = { files: Record<string, Counts> }; | |
| 46 | export type Scan = Baseline & { classes: string[] }; | |
| 48 | 47 | |
| 49 | 48 | const SKIP = /(^|\/)(og\/vendor)\//; |
| 50 | 49 | |
| @@ -105,6 +104,44 @@ export function classesIn(text: string): string[] { | ||
| 105 | 104 | return [...set]; |
| 106 | 105 | } |
| 107 | 106 | |
| 107 | /** The section of DESIGN.md from the heading starting `## <n>.` to the next `## `. */ | |
| 108 | function section(design: string, n: string): string { | |
| 109 | const start = design.search(new RegExp(`^## ${n.replace(".", "\\.")}`, "m")); | |
| 110 | if (start < 0) return ""; | |
| 111 | const end = design.slice(start + 1).search(/^## /m); | |
| 112 | return end < 0 ? design.slice(start) : design.slice(start, start + 1 + end); | |
| 113 | } | |
| 114 | ||
| 115 | /** The classes a stretch of DESIGN.md names: a selector's `.name` chain inside | |
| 116 | * a code span (which, as in Markdown, may wrap a line but not a paragraph), | |
| 117 | * bare or after an element (`pre.run-log`). A dotted word that is not a | |
| 118 | * selector — `ui.ts`, `codeberg.org`, `status.reviews` — names nothing. */ | |
| 119 | const SELECTOR = /(?<![\w/.$-])(?:(?:a|button|details|div|fieldset|form|input|label|li|ol|pre|section|select|span|table|textarea|ul)(?=\.))?((?:\.[a-z][a-z0-9_-]*)+)/gi; | |
| 120 | ||
| 121 | function namedIn(text: string): string[] { | |
| 122 | const set = new Set<string>(); | |
| 123 | for (const span of text.matchAll(/`((?:[^`\n]|\n(?!\n))+)`/g)) { | |
| 124 | for (const chain of span[1]!.matchAll(SELECTOR)) { | |
| 125 | for (const c of chain[1]!.split(".").slice(1)) set.add(c); | |
| 126 | } | |
| 127 | } | |
| 128 | return [...set].sort(); | |
| 129 | } | |
| 130 | ||
| 131 | /** Every class DESIGN.md §5 names, its subsections and the §5d register included. */ | |
| 132 | export function documentedClasses(design: string): string[] { | |
| 133 | return namedIn(section(design, "5.")); | |
| 134 | } | |
| 135 | ||
| 136 | /** The §5d register alone: the page-local classes that predate the rule. */ | |
| 137 | export function registeredClasses(design: string): string[] { | |
| 138 | const s5 = section(design, "5."); | |
| 139 | const start = s5.search(/^### 5d\./m); | |
| 140 | if (start < 0) return []; | |
| 141 | const end = s5.slice(start).search(/\n##+ /); | |
| 142 | return namedIn(end < 0 ? s5.slice(start) : s5.slice(start, start + end)); | |
| 143 | } | |
| 144 | ||
| 108 | 145 | export function scan(root: string): Scan { |
| 109 | 146 | const files: Record<string, Counts> = {}; |
| 110 | 147 | const classes = new Set<string>(); |
| @@ -118,30 +155,37 @@ export function scan(root: string): Scan { | ||
| 118 | 155 | } |
| 119 | 156 | |
| 120 | 157 | /** What a pull made worse, one line each. Empty means the check passes. */ |
| 121 | export function compare(base: Scan, now: Scan): string[] { | |
| 158 | export function compare(base: Baseline, now: Scan, design: string): string[] { | |
| 122 | 159 | const out: string[] = []; |
| 123 | 160 | for (const [file, c] of Object.entries(now.files)) { |
| 124 | 161 | const b = base.files[file] ?? { colour: 0, size: 0 }; |
| 125 | 162 | if (c.colour > b.colour) out.push(`${file}: ${c.colour - b.colour} new colour literal(s) — use a token from themes.ts (var(--…))`); |
| 126 | 163 | if (c.size > b.size) out.push(`${file}: ${c.size - b.size} new off-scale font-size — the scale is ${TYPE_SCALE.join(", ")}px`); |
| 127 | 164 | } |
| 128 | const known = new Set(base.classes); | |
| 165 | const named = new Set(documentedClasses(design)); | |
| 129 | 166 | for (const k of now.classes) { |
| 130 | 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`); | |
| 167 | if (!named.has(k)) out.push(`.${k}: a component class the design system does not have — use one docs/DESIGN.md §5 names, or name it there first`); | |
| 168 | } | |
| 169 | const defined = new Set(now.classes); | |
| 170 | for (const k of registeredClasses(design)) { | |
| 171 | if (!defined.has(k)) out.push(`.${k}: no stylesheet defines it any more — strike it from docs/DESIGN.md §5d`); | |
| 131 | 172 | } |
| 132 | 173 | return out; |
| 133 | 174 | } |
| 134 | 175 | |
| 135 | 176 | const BASELINE = "docs/design/baseline.json"; |
| 177 | const DESIGN = "docs/DESIGN.md"; | |
| 136 | 178 | |
| 137 | 179 | if (import.meta.main) { |
| 138 | 180 | const root = process.cwd(); |
| 139 | 181 | const now = scan(root); |
| 140 | 182 | if (process.argv.includes("--update")) { |
| 141 | writeFileSync(join(root, BASELINE), JSON.stringify(now, null, 2) + "\n"); | |
| 183 | const baseline: Baseline = { files: now.files }; | |
| 184 | writeFileSync(join(root, BASELINE), JSON.stringify(baseline, null, 2) + "\n"); | |
| 142 | 185 | console.log(`wrote ${BASELINE}`); |
| 143 | 186 | } else { |
| 144 | const problems = compare(JSON.parse(readFileSync(join(root, BASELINE), "utf8")), now); | |
| 187 | const base: Baseline = JSON.parse(readFileSync(join(root, BASELINE), "utf8")); | |
| 188 | const problems = compare(base, now, readFileSync(join(root, DESIGN), "utf8")); | |
| 145 | 189 | for (const p of problems) console.error(p); |
| 146 | 190 | if (problems.length) process.exit(1); |
| 147 | 191 | console.log("design: no colour, size or component added outside the system"); |
src/web/studio/style.ts +2 −41
| @@ -9,28 +9,11 @@ | ||
| 9 | 9 | * win from: nothing else in the sheet styles a `.studio-*` selector, and the |
| 10 | 10 | * few rules that reach a shared class (`.badge`, `.row-lead`, `.env-facts`) |
| 11 | 11 | * set properties those components do not. |
| 12 | * | |
| 13 | * `shell` and `rail` hold rules nothing in this directory draws any more, | |
| 14 | * and `rows` all but two of them. They are kept exactly as they were because | |
| 15 | * this is a pure move — deleting a rule changes what is served, and that is | |
| 16 | * its own issue. | |
| 17 | 12 | */ |
| 18 | 13 | |
| 19 | 14 | /** A part as written, without the newlines the template literal adds. */ |
| 20 | 15 | const part = (css: string): string => css.trim(); |
| 21 | 16 | |
| 22 | /** The first studio's own two-column shell, and the preview box it drew. */ | |
| 23 | const shell = part(/* css */ ` | |
| 24 | .studio { display: grid; grid-template-columns: minmax(0, 1fr) 260px; gap: 22px; margin-top: 16px; } | |
| 25 | @media (max-width: 860px) { .studio { grid-template-columns: 1fr; } } | |
| 26 | .studio-main { display: grid; gap: 18px; } | |
| 27 | .studio-preview { border: 1px solid var(--border); border-radius: var(--radius); padding: 12px 14px; background: var(--muted); } | |
| 28 | .studio-preview-head { display: flex; align-items: center; gap: 12px; } | |
| 29 | .studio-live { font-family: var(--mono); font-size: 11.5px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border); } | |
| 30 | .studio-live[data-state="running"] { color: var(--merged); border-color: var(--merged); } | |
| 31 | .studio-live[data-state="failed"] { color: var(--danger, #c33); } | |
| 32 | `); | |
| 33 | ||
| 34 | 17 | /** The box a person tells the crew from (`app.ts`). */ |
| 35 | 18 | const tellBox = part(/* css */ ` |
| 36 | 19 | .studio-tell textarea { min-height: 72px; resize: vertical; } |
| @@ -59,21 +42,14 @@ const wordBadges = part(/* css */ ` | ||
| 59 | 42 | .badge[data-word="working"] svg { animation: spin 1.1s linear infinite; } |
| 60 | 43 | `); |
| 61 | 44 | |
| 62 | /** The first studio's word rows, and the word itself where a row's meta | |
| 63 | * line still carries one (`home.ts`). */ | |
| 45 | /** The word itself where a row's meta line still carries one, and the | |
| 46 | * shelf row's meta line (`shelf.ts`). */ | |
| 64 | 47 | const rows = part(/* css */ ` |
| 65 | .studio-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; } | |
| 66 | .studio-list li { display: grid; grid-template-columns: 18px 84px minmax(0, 1fr) auto; gap: 10px; align-items: baseline; padding: 8px 10px; border: 1px solid var(--border); border-radius: var(--radius-sm); font-size: 14px; } | |
| 67 | .studio-list li svg { vertical-align: -3px; } | |
| 68 | .studio-who { font-weight: 600; } | |
| 69 | 48 | .studio-word { font-family: var(--mono); font-size: 11.5px; opacity: .8; white-space: nowrap; } |
| 70 | 49 | .shelf-meta { display: flex; gap: 0 4px; min-width: 0; } |
| 71 | 50 | .shelf-desc { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } |
| 72 | 51 | .shelf-facts { flex: 0 0 auto; max-width: 100%; } |
| 73 | 52 | .row-meta .studio-word { font-family: inherit; font-size: inherit; opacity: 1; font-weight: 560; } |
| 74 | .studio-list li[data-word="working"] .studio-word { color: var(--accent); opacity: 1; } | |
| 75 | .studio-list li[data-word="look"] .studio-word { color: var(--merged); opacity: 1; } | |
| 76 | .studio-list li[data-word="stopped"] .studio-word { color: var(--danger, #c33); opacity: 1; } | |
| 77 | 53 | `); |
| 78 | 54 | |
| 79 | 55 | /** The row of controls beside a list row: a stop, a change's buttons. */ |
| @@ -81,16 +57,6 @@ const rowActions = part(/* css */ ` | ||
| 81 | 57 | .studio-ask-actions { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } |
| 82 | 58 | `); |
| 83 | 59 | |
| 84 | /** The first studio's rail, and the crew list it held. */ | |
| 85 | const rail = part(/* css */ ` | |
| 86 | .studio-rail { display: grid; gap: 18px; align-content: start; } | |
| 87 | .studio-rail h3 { font-size: 11.5px; letter-spacing: .08em; text-transform: uppercase; opacity: .7; margin: 0 0 6px; font-weight: 600; } | |
| 88 | .studio-crew { list-style: none; margin: 0; padding: 0; display: grid; gap: 5px; font-size: 13.5px; } | |
| 89 | .studio-crew li { display: grid; grid-template-columns: 74px 1fr; gap: 8px; } | |
| 90 | .studio-crew li b { font-weight: 600; } | |
| 91 | .studio-crew li span { opacity: .85; } | |
| 92 | `); | |
| 93 | ||
| 94 | 60 | /** The phone: one document, narrower. Each rule belongs to a component above |
| 95 | 61 | * and only ever applies at 600px and under, so nothing changes on a desktop. */ |
| 96 | 62 | const phone = part(/* css */ ` |
| @@ -105,10 +71,7 @@ const phone = part(/* css */ ` | ||
| 105 | 71 | .studio-tell .bar .btn, .studio-ask-actions .btn { min-height: 44px; } |
| 106 | 72 | .fcard-acts .btn, .fcard-form .btn { min-height: 44px; } |
| 107 | 73 | .studio-ask-actions { display: flex; flex-direction: column; align-items: stretch; white-space: normal; width: 100%; } |
| 108 | .studio-list li { grid-template-columns: 18px minmax(0, 1fr); row-gap: 2px; } | |
| 109 | .studio-list li > :nth-child(n+3) { grid-column: 2; } | |
| 110 | 74 | .studio-word { white-space: normal; } |
| 111 | .studio .row-meta, .studio-list .row-meta { white-space: normal; overflow-wrap: anywhere; } | |
| 112 | 75 | .shelf-meta { flex-wrap: wrap; } |
| 113 | 76 | .shelf-desc { flex: 1 1 100%; } |
| 114 | 77 | .studio-tell textarea, .studio-tell .input { font-size: 16px; } |
| @@ -118,14 +81,12 @@ const phone = part(/* css */ ` | ||
| 118 | 81 | import { feedCss } from "./feed_style.ts"; |
| 119 | 82 | |
| 120 | 83 | export const studioCss = `@layer components {\n${[ |
| 121 | shell, | |
| 122 | 84 | tellBox, |
| 123 | 85 | appHead, |
| 124 | 86 | justChanged, |
| 125 | 87 | wordBadges, |
| 126 | 88 | rows, |
| 127 | 89 | rowActions, |
| 128 | rail, | |
| 129 | 90 | part(feedCss), |
| 130 | 91 | phone, |
| 131 | 92 | ].join("\n\n")}\n}`; |
src/web/style/base.ts −2
| @@ -116,8 +116,6 @@ h3 { font-size: 14px; font-weight: 600; line-height: 1.4; } | ||
| 116 | 116 | } |
| 117 | 117 | .wordmark:hover { color: var(--fg); } |
| 118 | 118 | .wordmark:hover .mark { transform: rotate(135deg); } |
| 119 | .topbar-asks { display: inline-flex; flex: none; text-decoration: none; } | |
| 120 | .topbar-asks:hover .pill { background: color-mix(in srgb, var(--accent) 24%, transparent); } | |
| 121 | 119 | .topbar .spacer { flex: 1; } |
| 122 | 120 | .topbar-tools { display: flex; align-items: center; gap: 6px; flex: 0 1 auto; min-width: 0; } |
| 123 | 121 | /* The page never scrolls sideways. A strip that must — the tabs, a code |
src/web/style/content.ts −5
| @@ -824,8 +824,6 @@ form.inline { display: inline; margin-left: auto; } | ||
| 824 | 824 | .reviews-chip .ico { width: 14px; height: 14px; } |
| 825 | 825 | .reviews-chip .ok { display: inline-flex; align-items: center; gap: 4px; color: var(--success); } |
| 826 | 826 | .reviews-chip .bad { display: inline-flex; align-items: center; gap: 4px; color: var(--danger); } |
| 827 | .files-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; } | |
| 828 | .files-head h2 { margin-bottom: 0; } | |
| 829 | 827 | |
| 830 | 828 | /* ---- apps, runs, and the crew ---- */ |
| 831 | 829 | /* The strip a repo home wears when it runs: state, commit, URL, previews, |
| @@ -942,7 +940,6 @@ form.inline { display: inline; margin-left: auto; } | ||
| 942 | 940 | .log-tail-row { padding: 0 14px 10px; } |
| 943 | 941 | .run-log.log-tail { max-height: 8.5em; min-height: 0; margin: 6px 0 0; font-size: 12.5px; } |
| 944 | 942 | .table .truncate { max-width: 34ch; display: inline-block; vertical-align: bottom; } |
| 945 | .agent-brief { white-space: pre-wrap; font-size: 13px; color: var(--muted-fg); max-height: 6.5em; overflow: hidden; } | |
| 946 | 943 | |
| 947 | 944 | /* ---- proposing a change ---- */ |
| 948 | 945 | /* The editor's commit block, when the commit is a contribution: the whole |
| @@ -1184,8 +1181,6 @@ textarea.editor:focus { outline: none; } | ||
| 1184 | 1181 | background: var(--card); min-height: 12rem; |
| 1185 | 1182 | } |
| 1186 | 1183 | .editor-commit { margin-top: 12px; } |
| 1187 | .editor-hint { margin: 6px 2px 0; font-size: 12.5px; color: var(--faint); } | |
| 1188 | .editor-hint kbd { margin: 0 2px; } | |
| 1189 | 1184 | .editor-target { border: 0; padding: 0; margin: 4px 0 0; } |
| 1190 | 1185 | .editor-target legend { font-weight: 600; margin-bottom: 6px; } |
| 1191 | 1186 | .editor-target .check { display: flex; } |
test/design-lint.test.ts +42 −8
| @@ -1,11 +1,25 @@ | ||
| 1 | 1 | import { describe, expect, test } from "bun:test"; |
| 2 | 2 | import { readFileSync } from "node:fs"; |
| 3 | import { classesIn, compare, countFile, scan } from "../scripts/design-lint.ts"; | |
| 3 | import { classesIn, compare, countFile, documentedClasses, registeredClasses, scan } from "../scripts/design-lint.ts"; | |
| 4 | ||
| 5 | /** A DESIGN.md in miniature: the §5 table, a §5d register, and a §6 that names a class too. */ | |
| 6 | const DESIGN = [ | |
| 7 | "## 4. Space", | |
| 8 | "`.spacing-only` is named before §5.", | |
| 9 | "## 5. The component vocabulary", | |
| 10 | "| **button** | `.btn` | in `src/web/ui.ts` |", | |
| 11 | "| **run log** | `pre.run-log` (`.byline.origin-by`) | at `hello.localhost`, `status.reviews` |", | |
| 12 | "### 5d. Page-local classes", | |
| 13 | "- **`pages/x.ts`** — the x page: `.x-card` `.x-wrapped", | |
| 14 | "` `.x-row`", | |
| 15 | "## 6. Keyboard", | |
| 16 | "`.keys-only` is named after §5.", | |
| 17 | ].join("\n"); | |
| 4 | 18 | |
| 5 | 19 | describe("design check: nothing added outside the system (#2678)", () => { |
| 6 | 20 | test("the tree is no worse than its baseline", () => { |
| 7 | 21 | const base = JSON.parse(readFileSync("docs/design/baseline.json", "utf8")); |
| 8 | expect(compare(base, scan("."))).toEqual([]); | |
| 22 | expect(compare(base, scan("."), readFileSync("docs/DESIGN.md", "utf8"))).toEqual([]); | |
| 9 | 23 | }); |
| 10 | 24 | |
| 11 | 25 | test("counts colour literals and off-scale sizes, not on-scale ones", () => { |
| @@ -28,12 +42,32 @@ describe("design check: nothing added outside the system (#2678)", () => { | ||
| 28 | 42 | expect(classesIn(text).sort()).toEqual(["btn", "card", "new-thing", "row-title"]); |
| 29 | 43 | }); |
| 30 | 44 | |
| 45 | test("§5 names a class in a code span, as a selector, wrapped or not — and only §5 does (#2700)", () => { | |
| 46 | expect(documentedClasses(DESIGN)).toEqual(["btn", "byline", "origin-by", "run-log", "x-card", "x-row", "x-wrapped"]); | |
| 47 | expect(registeredClasses(DESIGN)).toEqual(["x-card", "x-row", "x-wrapped"]); | |
| 48 | expect(documentedClasses("## 4. Space\n`.a`")).toEqual([]); | |
| 49 | }); | |
| 50 | ||
| 31 | 51 | test("fails a pull that adds a colour, a size or a component", () => { |
| 32 | const base = { files: { "src/web/x.ts": { colour: 1, size: 1 } }, classes: ["btn"] }; | |
| 33 | expect(compare(base, base)).toEqual([]); | |
| 34 | expect(compare(base, { files: { "src/web/x.ts": { colour: 0, size: 0 } }, classes: ["btn"] })).toEqual([]); | |
| 35 | expect(compare(base, { ...base, files: { "src/web/x.ts": { colour: 2, size: 1 } } })).toHaveLength(1); | |
| 36 | expect(compare(base, { ...base, files: { "src/web/y.ts": { colour: 0, size: 1 } } })).toHaveLength(1); | |
| 37 | expect(compare(base, { ...base, classes: ["btn", "gizmo"] })[0]).toContain(".gizmo"); | |
| 52 | const base = { files: { "src/web/x.ts": { colour: 1, size: 1 } } }; | |
| 53 | const now = { ...base, classes: ["btn", "run-log", "x-card", "x-row", "x-wrapped"] }; | |
| 54 | expect(compare(base, now, DESIGN)).toEqual([]); | |
| 55 | expect(compare(base, { ...now, files: { "src/web/x.ts": { colour: 0, size: 0 } } }, DESIGN)).toEqual([]); | |
| 56 | expect(compare(base, { ...now, files: { "src/web/x.ts": { colour: 2, size: 1 } } }, DESIGN)).toHaveLength(1); | |
| 57 | expect(compare(base, { ...now, files: { "src/web/y.ts": { colour: 0, size: 1 } } }, DESIGN)).toHaveLength(1); | |
| 58 | expect(compare(base, { ...now, classes: [...now.classes, "gizmo"] }, DESIGN)[0]).toContain(".gizmo"); | |
| 59 | // Named outside §5 is not named: §4 and §6 are not the vocabulary. | |
| 60 | expect(compare(base, { ...now, classes: [...now.classes, "keys-only", "spacing-only"] }, DESIGN)).toHaveLength(2); | |
| 61 | }); | |
| 62 | ||
| 63 | test("fails a register entry whose last rule is gone, so §5d only shrinks (#2700)", () => { | |
| 64 | const base = { files: {} }; | |
| 65 | const problems = compare(base, { files: {}, classes: ["btn", "x-card", "x-wrapped"] }, DESIGN); | |
| 66 | expect(problems).toEqual([".x-row: no stylesheet defines it any more — strike it from docs/DESIGN.md §5d"]); | |
| 67 | }); | |
| 68 | ||
| 69 | test("every class the stylesheets define is one DESIGN.md §5 names (#2700)", () => { | |
| 70 | const named = new Set(documentedClasses(readFileSync("docs/DESIGN.md", "utf8"))); | |
| 71 | expect(scan(".").classes.filter((k) => !named.has(k))).toEqual([]); | |
| 38 | 72 | }); |
| 39 | 73 | }); |
test/studio-phone.test.ts +1 −1
| @@ -6,7 +6,7 @@ describe("the studio at 360px", () => { | ||
| 6 | 6 | const phone = studioCss.slice(studioCss.indexOf("@media (max-width: 600px) {\n .studio-head")); |
| 7 | 7 | |
| 8 | 8 | test("every component the issue names has a phone rule", () => { |
| 9 | for (const sel of [".studio-head", ".studio-tell", ".fcard-head", ".studio-ask-actions", ".studio-list li", ".studio-word"]) { | |
| 9 | for (const sel of [".studio-head", ".studio-tell", ".fcard-head", ".studio-ask-actions", ".studio-word"]) { | |
| 10 | 10 | expect(phone).toContain(sel); |
| 11 | 11 | } |
| 12 | 12 | }); |
new above
offline