Public: anyone can read this; the crew can change it.
Design: tokenise style/components.ts, studio/home.ts, pages/looks.ts, pages/site.ts #2705
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/2698-design-tokenise-style-components-ts-stud → main 6 commits fork
4 files
changed +43
−164
docs/design/baseline.json +4 −144
| @@ -1,172 +1,32 @@ | ||
| 1 | 1 | { |
| 2 | 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 | 3 | "src/web/pages/board.ts": { |
| 20 | "colour": 1, | |
| 4 | "colour": 0, | |
| 21 | 5 | "size": 6 |
| 22 | 6 | }, |
| 23 | "src/web/pages/claim.ts": { | |
| 24 | "colour": 1, | |
| 25 | "size": 0 | |
| 26 | }, | |
| 27 | 7 | "src/web/pages/conflicts.ts": { |
| 28 | 8 | "colour": 0, |
| 29 | 9 | "size": 1 |
| 30 | 10 | }, |
| 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 | 11 | "src/web/pages/map.ts": { |
| 56 | 12 | "colour": 3, |
| 57 | 13 | "size": 12 |
| 58 | 14 | }, |
| 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 | 15 | "src/web/standards.ts": { |
| 80 | 16 | "colour": 2, |
| 81 | 17 | "size": 0 |
| 82 | 18 | }, |
| 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 | 19 | "src/web/studio/style.ts": { |
| 144 | 20 | "colour": 2, |
| 145 | 21 | "size": 1 |
| 146 | 22 | }, |
| 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 | 23 | "src/web/style/components.ts": { |
| 160 | "colour": 5, | |
| 161 | "size": 16 | |
| 24 | "colour": 3, | |
| 25 | "size": 2 | |
| 162 | 26 | }, |
| 163 | 27 | "src/web/style/content.ts": { |
| 164 | "colour": 23, | |
| 28 | "colour": 19, | |
| 165 | 29 | "size": 31 |
| 166 | }, | |
| 167 | "src/web/ui.ts": { | |
| 168 | "colour": 1, | |
| 169 | "size": 0 | |
| 170 | 30 | } |
| 171 | 31 | }, |
| 172 | 32 | "classes": [ |
scripts/design-lint.ts +13 −3
| @@ -69,11 +69,21 @@ function cssBlocks(text: string): string[] { | ||
| 69 | 69 | return out; |
| 70 | 70 | } |
| 71 | 71 | |
| 72 | /** | |
| 73 | * The text without its comments, so an issue reference (`#599`) is not read | |
| 74 | * as a three-digit hex. A line comment starts a line or follows whitespace, | |
| 75 | * which keeps `https://` in a string. | |
| 76 | */ | |
| 77 | export function uncommented(text: string): string { | |
| 78 | return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|\s)\/\/.*$/gm, "$1"); | |
| 79 | } | |
| 80 | ||
| 72 | 81 | export function countFile(text: string): Counts { |
| 73 | const mixes = (text.match(/color-mix\([^;{}]*\)/g) ?? []).filter((m) => !m.includes("var(")).length; | |
| 82 | const code = uncommented(text); | |
| 83 | const mixes = (code.match(/color-mix\([^;{}]*\)/g) ?? []).filter((m) => !m.includes("var(")).length; | |
| 74 | 84 | 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 + | |
| 85 | (code.match(/#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b(?![0-9a-zA-Z_-])/g) ?? []).length + | |
| 86 | (code.match(/\b(?:rgba?|hsla?)\(/g) ?? []).length + | |
| 77 | 87 | cssBlocks(text).reduce((n, css) => n + (css.match(NAMED_RE) ?? []).length, 0) + |
| 78 | 88 | mixes; |
| 79 | 89 | let size = 0; |
src/web/style/components.ts +19 −17
| @@ -208,17 +208,19 @@ kbd { | ||
| 208 | 208 | min-width: 20px; height: 20px; padding: 0 5px; |
| 209 | 209 | border: 1px solid var(--border); border-bottom-width: 2px; |
| 210 | 210 | border-radius: var(--radius-sm); background: var(--muted); |
| 211 | font-family: var(--mono); font-size: 11px; font-weight: 500; color: var(--muted-fg); | |
| 211 | font-family: var(--mono); font-size: 11.5px; font-weight: 500; color: var(--muted-fg); | |
| 212 | 212 | } |
| 213 | 213 | |
| 214 | 214 | .sha { |
| 215 | font-family: var(--mono); font-size: 12px; padding: 1px 5px; | |
| 215 | font-family: var(--mono); font-size: 12.5px; padding: 1px 5px; | |
| 216 | 216 | border-radius: 4px; background: var(--muted); color: var(--muted-fg); |
| 217 | 217 | font-variant-numeric: tabular-nums; |
| 218 | 218 | } |
| 219 | 219 | a.sha:hover { color: var(--accent); } |
| 220 | 220 | |
| 221 | /* ---- avatar ---- */ | |
| 221 | /* ---- avatar ---- | |
| 222 | The hue is the person's (--hue), so it is mixed here, not a palette token; | |
| 223 | the 20px and 16px circles' initials sit below the type scale on purpose. */ | |
| 222 | 224 | .avatar { |
| 223 | 225 | display: inline-flex; align-items: center; justify-content: center; flex: none; |
| 224 | 226 | width: 20px; height: 20px; border-radius: 999px; |
| @@ -228,14 +230,14 @@ a.sha:hover { color: var(--accent); } | ||
| 228 | 230 | font-size: 9.5px; font-weight: 650; letter-spacing: 0; text-transform: uppercase; |
| 229 | 231 | user-select: none; |
| 230 | 232 | } |
| 231 | .avatar[data-size="md"] { width: 28px; height: 28px; font-size: 12px; } | |
| 232 | .avatar[data-size="lg"] { width: 56px; height: 56px; font-size: 22px; border-radius: var(--radius-lg); } | |
| 233 | .avatar[data-size="md"] { width: 28px; height: 28px; font-size: 12.5px; } | |
| 234 | .avatar[data-size="lg"] { width: 56px; height: 56px; font-size: 20px; border-radius: var(--radius-lg); } | |
| 233 | 235 | |
| 234 | 236 | .byline { display: inline-flex; align-items: center; gap: 5px; min-width: 0; } |
| 235 | 237 | .byline a:hover { color: var(--accent); } |
| 236 | 238 | .name-mark { |
| 237 | 239 | display: inline-block; padding: 0 5px; border-radius: 4px; |
| 238 | font-size: 10px; font-weight: 600; line-height: 15px; letter-spacing: 0.04em; | |
| 240 | font-size: 11.5px; font-weight: 600; line-height: 15px; letter-spacing: 0.04em; | |
| 239 | 241 | text-transform: uppercase; white-space: nowrap; vertical-align: 1px; |
| 240 | 242 | color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); |
| 241 | 243 | } |
| @@ -350,7 +352,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 350 | 352 | /* ---- table ---- */ |
| 351 | 353 | .table { width: 100%; font-size: 13px; } |
| 352 | 354 | .table th { |
| 353 | text-align: left; font-weight: 550; font-size: 12px; color: var(--muted-fg); | |
| 355 | text-align: left; font-weight: 550; font-size: 12.5px; color: var(--muted-fg); | |
| 354 | 356 | padding: 8px 12px; border-bottom: 1px solid var(--border); white-space: nowrap; |
| 355 | 357 | } |
| 356 | 358 | .table td { |
| @@ -367,7 +369,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 367 | 369 | border-bottom: 1px solid var(--border); |
| 368 | 370 | overflow-x: auto; scrollbar-width: none; |
| 369 | 371 | scroll-snap-type: x proximity; |
| 370 | mask-image: linear-gradient(90deg, #000 0 calc(100% - 20px), transparent); | |
| 372 | mask-image: linear-gradient(90deg, var(--fg) 0 calc(100% - 20px), transparent); | |
| 371 | 373 | } |
| 372 | 374 | .tabs::-webkit-scrollbar { display: none; } |
| 373 | 375 | .tabs > a { |
| @@ -429,7 +431,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 429 | 431 | @keyframes pop { from { opacity: 0; transform: translateY(-4px); } } |
| 430 | 432 | .menu-title { |
| 431 | 433 | display: block; padding: 6px 10px 4px; |
| 432 | font-size: 11px; font-weight: 600; letter-spacing: .08em; | |
| 434 | font-size: 11.5px; font-weight: 600; letter-spacing: .08em; | |
| 433 | 435 | text-transform: uppercase; color: var(--faint); |
| 434 | 436 | } |
| 435 | 437 | .menu-item { |
| @@ -518,7 +520,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 518 | 520 | |
| 519 | 521 | /* ---- tell the crew, in place: fixed, so nothing shifts ---- */ |
| 520 | 522 | .tell-here { position: fixed; left: 0; right: 0; bottom: 0; z-index: 50; width: min(620px, 100%); margin: 0 auto; padding: 8px; } |
| 521 | .tell-here .comment-form { margin: 0; box-shadow: var(--shadow-lg, 0 8px 24px rgb(0 0 0 / .25)); } | |
| 523 | .tell-here .comment-form { margin: 0; box-shadow: var(--shadow-2); } | |
| 522 | 524 | |
| 523 | 525 | /* ---- command palette ---- */ |
| 524 | 526 | /* A dialog centres itself with its own auto margins, which the reset zeroes |
| @@ -541,7 +543,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 541 | 543 | .palette-group + .palette-group { margin-top: 4px; } |
| 542 | 544 | .palette-heading { |
| 543 | 545 | padding: 8px 10px 4px; |
| 544 | font-size: 11px; font-weight: 600; letter-spacing: .08em; | |
| 546 | font-size: 11.5px; font-weight: 600; letter-spacing: .08em; | |
| 545 | 547 | text-transform: uppercase; color: var(--faint); |
| 546 | 548 | } |
| 547 | 549 | .palette-item { |
| @@ -555,14 +557,14 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 555 | 557 | .palette-item .cat { |
| 556 | 558 | flex: none; max-width: 45%; margin-left: auto; |
| 557 | 559 | overflow: hidden; text-overflow: ellipsis; white-space: nowrap; |
| 558 | font-size: 12px; color: var(--faint); | |
| 560 | font-size: 12.5px; color: var(--faint); | |
| 559 | 561 | } |
| 560 | 562 | /* One highlight. The pointer moves it (the script listens) rather than |
| 561 | 563 | painting a second one under the cursor; the finder keeps plain hover. */ |
| 562 | 564 | .palette-item[aria-selected="true"], .finder .palette-item:hover { background: var(--muted); } |
| 563 | 565 | .palette-foot { |
| 564 | 566 | display: flex; align-items: center; gap: 10px; padding: 8px 14px; |
| 565 | border-top: 1px solid var(--border); font-size: 12px; color: var(--faint); | |
| 567 | border-top: 1px solid var(--border); font-size: 12.5px; color: var(--faint); | |
| 566 | 568 | } |
| 567 | 569 | .palette-empty { |
| 568 | 570 | display: flex; align-items: center; justify-content: center; gap: 6px; |
| @@ -575,7 +577,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 575 | 577 | margin: 16px 0 0; padding: 10px 12px; |
| 576 | 578 | border: 1px solid var(--border); border-radius: var(--radius); |
| 577 | 579 | background: var(--card); box-shadow: var(--shadow-1); |
| 578 | font-size: 13.5px; | |
| 580 | font-size: 14px; | |
| 579 | 581 | animation: pop 160ms var(--ease) both; |
| 580 | 582 | } |
| 581 | 583 | .toast .ico { margin-top: 1px; } |
| @@ -604,7 +606,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 604 | 606 | bottom: calc(100% + 6px); left: 50%; transform: translateX(-50%); |
| 605 | 607 | padding: 4px 8px; border-radius: var(--radius-sm); |
| 606 | 608 | background: var(--primary); color: var(--primary-fg); |
| 607 | font-size: 12px; font-weight: 500; white-space: nowrap; | |
| 609 | font-size: 12.5px; font-weight: 500; white-space: nowrap; | |
| 608 | 610 | opacity: 0; pointer-events: none; transition: opacity 120ms var(--ease); |
| 609 | 611 | } |
| 610 | 612 | [data-tip][data-tip-side="bottom"]::after { bottom: auto; top: calc(100% + 6px); } |
| @@ -683,7 +685,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 683 | 685 | .settings-nav a { |
| 684 | 686 | display: inline-flex; align-items: center; gap: 6px; |
| 685 | 687 | padding: 0 12px; min-height: 36px; white-space: nowrap; |
| 686 | font-size: 13.5px; color: var(--muted-fg); border-radius: var(--radius-sm); | |
| 688 | font-size: 14px; color: var(--muted-fg); border-radius: var(--radius-sm); | |
| 687 | 689 | } |
| 688 | 690 | .settings-nav a:hover { background: var(--muted); color: var(--fg); } |
| 689 | 691 | .settings-nav a[aria-current="page"] { color: var(--fg); font-weight: 550; background: var(--muted); } |
| @@ -699,7 +701,7 @@ a.row:hover .row-title { color: var(--accent); } | ||
| 699 | 701 | /* ---- clone strip ---- */ |
| 700 | 702 | .clone { |
| 701 | 703 | display: flex; align-items: center; gap: 6px; |
| 702 | font-family: var(--mono); font-size: 12px; | |
| 704 | font-family: var(--mono); font-size: 12.5px; | |
| 703 | 705 | padding: 6px 8px; border: 1px solid var(--border); border-radius: var(--radius-sm); |
| 704 | 706 | background: var(--muted); min-width: 0; |
| 705 | 707 | } |
test/design-lint.test.ts +7
| @@ -16,6 +16,13 @@ describe("design check: nothing added outside the system (#2678)", () => { | ||
| 16 | 16 | expect(countFile("a{color:var(--fg)} // issue #2678")).toEqual({ colour: 0, size: 0 }); |
| 17 | 17 | }); |
| 18 | 18 | |
| 19 | test("an issue reference in a comment is not a colour; the same digits in code are", () => { | |
| 20 | expect(countFile("// the feed (#599), whatever the mode (#625)\nx = 1;").colour).toBe(0); | |
| 21 | expect(countFile("/**\n * Who may fetch a picture (#727, #738).\n */\nx = 1;").colour).toBe(0); | |
| 22 | expect(countFile("a{color:#999}").colour).toBe(1); | |
| 23 | expect(countFile('const u = "https://x.test/#abc";').colour).toBe(1); | |
| 24 | }); | |
| 25 | ||
| 19 | 26 | test("finds the classes a css template defines, and only those", () => { |
| 20 | 27 | 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 | 28 | expect(classesIn(text).sort()).toEqual(["btn", "card", "new-thing", "row-title"]); |
new above
offline