@radicool/throughline 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +214 -0
  3. package/adapters/codex/AGENTS.md +29 -0
  4. package/adapters/codex/codex-mcp.toml +4 -0
  5. package/adapters/codex/prompts/component-builder.md +272 -0
  6. package/adapters/codex/prompts/component-pipeline.md +81 -0
  7. package/adapters/codex/prompts/design-system-audit.md +122 -0
  8. package/adapters/codex/prompts/design-system-status.md +41 -0
  9. package/adapters/codex/prompts/figma-environment-setup.md +456 -0
  10. package/adapters/codex/prompts/icon-system-builder.md +268 -0
  11. package/adapters/codex/prompts/new-component.md +13 -0
  12. package/adapters/codex/prompts/repository-builder.md +163 -0
  13. package/adapters/codex/prompts/retrofit-planner.md +136 -0
  14. package/adapters/codex/prompts/start.md +22 -0
  15. package/adapters/codex/prompts/storybook-chromatic-builder.md +261 -0
  16. package/adapters/codex/prompts/sync-figma-tokens.md +22 -0
  17. package/adapters/codex/prompts/token-builder.md +361 -0
  18. package/adapters/codex/prompts/token-crosswalk-builder.md +120 -0
  19. package/adapters/codex/prompts/token-sheet-builder.md +162 -0
  20. package/adapters/codex/prompts/token-sync-layer.md +232 -0
  21. package/adapters/cursor/.cursor/commands/design-system-status.md +41 -0
  22. package/adapters/cursor/.cursor/commands/new-component.md +13 -0
  23. package/adapters/cursor/.cursor/commands/start.md +22 -0
  24. package/adapters/cursor/.cursor/commands/sync-figma-tokens.md +22 -0
  25. package/adapters/cursor/.cursor/mcp.json +14 -0
  26. package/adapters/cursor/.cursor/rules/component-builder.mdc +276 -0
  27. package/adapters/cursor/.cursor/rules/component-pipeline.mdc +85 -0
  28. package/adapters/cursor/.cursor/rules/design-system-audit.mdc +126 -0
  29. package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +460 -0
  30. package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +272 -0
  31. package/adapters/cursor/.cursor/rules/repository-builder.mdc +167 -0
  32. package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +140 -0
  33. package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +265 -0
  34. package/adapters/cursor/.cursor/rules/token-builder.mdc +365 -0
  35. package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +124 -0
  36. package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +166 -0
  37. package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +236 -0
  38. package/adapters/generic/AGENTS.md +46 -0
  39. package/adapters/generic/commands/design-system-status.md +41 -0
  40. package/adapters/generic/commands/new-component.md +13 -0
  41. package/adapters/generic/commands/start.md +22 -0
  42. package/adapters/generic/commands/sync-figma-tokens.md +22 -0
  43. package/adapters/generic/skills/component-builder/SKILL.md +272 -0
  44. package/adapters/generic/skills/component-pipeline/SKILL.md +81 -0
  45. package/adapters/generic/skills/design-system-audit/SKILL.md +122 -0
  46. package/adapters/generic/skills/figma-environment-setup/SKILL.md +456 -0
  47. package/adapters/generic/skills/icon-system-builder/SKILL.md +268 -0
  48. package/adapters/generic/skills/repository-builder/SKILL.md +163 -0
  49. package/adapters/generic/skills/retrofit-planner/SKILL.md +136 -0
  50. package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +261 -0
  51. package/adapters/generic/skills/token-builder/SKILL.md +361 -0
  52. package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +120 -0
  53. package/adapters/generic/skills/token-sheet-builder/SKILL.md +162 -0
  54. package/adapters/generic/skills/token-sync-layer/SKILL.md +232 -0
  55. package/package.json +29 -0
  56. package/references/brainstorm-before-build.md +139 -0
  57. package/references/brownfield-retrofit.md +93 -0
  58. package/references/coding-level.md +70 -0
  59. package/references/crosswalk-schema.md +113 -0
  60. package/references/figma-component-standards.md +532 -0
  61. package/references/figma-publishing.md +125 -0
  62. package/references/figma-scripting.md +278 -0
  63. package/references/manifest-schema.md +341 -0
  64. package/references/scaling-up-handoff.md +61 -0
  65. package/references/sync-adapters.md +141 -0
  66. package/scripts/README.md +78 -0
  67. package/scripts/adapters/emit-codex.mjs +58 -0
  68. package/scripts/adapters/emit-cursor.mjs +26 -0
  69. package/scripts/adapters/emit-generic.mjs +41 -0
  70. package/scripts/adapters/generate.mjs +73 -0
  71. package/scripts/adapters/read-sources.mjs +54 -0
  72. package/scripts/adapters/translate.mjs +51 -0
  73. package/scripts/build-reverse-index.mjs +56 -0
  74. package/scripts/crosswalk.schema.json +33 -0
  75. package/scripts/grep-color-usage.mjs +143 -0
  76. package/scripts/guard-token-removal.mjs +95 -0
  77. package/scripts/install.mjs +143 -0
  78. package/scripts/lib/crosswalk.mjs +91 -0
  79. package/scripts/validate-crosswalk.mjs +96 -0
@@ -0,0 +1,261 @@
1
+ # Storybook + Chromatic builder
2
+
3
+ Builds the code half of the component library: real components consuming the
4
+ synced tokens, stories documenting them, Chromatic for visual regression, and
5
+ Code Connect tying Figma to code when available.
6
+
7
+ ## Calibrate + prerequisites
8
+
9
+ Read `user.codingLevel` (`.throughline/references/coding-level.md`) and scale explanation.
10
+ This skill needs:
11
+
12
+ - A repo with the monorepo scaffold (`repository-builder`, at least
13
+ `local-git`). Offer to run it if missing.
14
+ - Synced tokens in `packages/tokens` (`token-sync-layer`) so components consume
15
+ real token files. Offer to run sync if missing.
16
+ - Figma components (`components.built`) with their slot contracts, so stories
17
+ reflect the real component APIs.
18
+
19
+ Strongly recommend `github` stage so Chromatic's CI integration works; it can be
20
+ set up locally first and wired to CI when the remote exists.
21
+
22
+ ## Step 1 — Stand up Storybook
23
+
24
+ Initialize Storybook in `packages/ui` (or the components package), configured for
25
+ the framework the tokens were synced for (e.g. React + Vite for a shadcn/Tailwind
26
+ system). Wire it to consume `packages/tokens` output so stories render with the
27
+ real design tokens (import the generated CSS/theme). Checkpoint: confirm
28
+ Storybook runs and shows the token-themed canvas.
29
+
30
+ **pnpm build-script allowlist (first-run gotcha).** On pnpm workspaces, the
31
+ `@storybook/react-vite` install pulls `esbuild`, whose postinstall is blocked by
32
+ pnpm's default `onlyBuiltDependencies` policy — Storybook then fails to start with a
33
+ binary-not-found error. When installing Storybook in a pnpm workspace, add `esbuild`
34
+ (and any other native postinstall dep Storybook pulls) to root `package.json`
35
+ `"pnpm": { "onlyBuiltDependencies": [...] }` as part of setup, so first run works.
36
+
37
+ **Don't duplicate the app's CSS — share it.** Storybook needs the same Tailwind v4
38
+ `@utility` typography rules the app uses to render correctly. **Before adding any,
39
+ check whether the app's global stylesheet (e.g. `apps/web/app/globals.css`) already
40
+ defines them.** If it does, extract the shared `@utility` blocks into a single
41
+ source — e.g. `packages/ui/src/typography.css`, added to the UI package's `exports`
42
+ (`"./typography.css"`) — and replace the inline copies in *both* the app and the UI
43
+ package with one `@import`. If they don't exist yet, create that shared file from
44
+ the start. Copying the rules inline into a second file starts immediate drift (one
45
+ side ends up on the wrong `--font-*` var and nobody notices). Keep Storybook-only
46
+ concerns (Google Fonts `@import`, `:root` font-var fallbacks) in a *separate*
47
+ `styles.css` layer so the shared typography file stays clean and app-shareable.
48
+
49
+ ## Step 2 — Build code components from the Figma spec + slot contracts
50
+
51
+ For each Figma component (`components.built`), build its code counterpart
52
+ consuming tokens and implementing the **slot contracts** captured by
53
+ component-builder:
54
+
55
+ - **Icon-set slots** → a prop typed to the icon set (`leadingIcon?`), defaulting
56
+ to the canonical icon name if specified, imported from the installed icon
57
+ package (`lucide-react` etc.).
58
+ - **Typed-component slots** → a prop typed to the DS component (`avatar?`),
59
+ resolved via deterministic naming.
60
+ - **General adornment slots** → a `ReactNode` prop (`endAdornment?`); **Figma
61
+ slots on composites** (card body, modal content) → `children` / a composition
62
+ prop, since a Figma slot is the design-tool expression of React composition.
63
+ - **Show/hide** is prop optionality, never a redundant boolean.
64
+ - Variant matrices (type/size/state) become the component's props/variants.
65
+
66
+ Match the deterministic naming so `Button` (Figma) ↔ `Button` (code).
67
+
68
+ ## Step 3 — Generate stories (subagent-driven, parallel)
69
+
70
+ Story generation is independent and verifiable per component, so use the
71
+ **subagent-driven model** (code-gen skills parallelize; this is the opposite of
72
+ the sequential Figma-authoring skills): dispatch one subagent per component to
73
+ write its stories — a story per meaningful variant, controls wired to props,
74
+ slot props demonstrated. Each subagent verifies its work (the story builds and
75
+ renders). Two-stage review (does it match the component spec; is it quality
76
+ code) before combining.
77
+
78
+ **Controls must actually drive the component (args-through render).** A `render`
79
+ that ignores its args silently breaks the Controls panel — the toggle writes to
80
+ `args` but nothing re-renders. So:
81
+
82
+ - Always thread args through: **`render: (args) => <Component {...args} />`**, never
83
+ `render: () => <Component variant="..." />` (hardcoded props make the variant
84
+ radio and other controls dead). Add fixed children/body *after* the spread:
85
+ `render: (args) => <Card {...args}>{body}</Card>`.
86
+ - **`ReactElement` slot props** (e.g. `avatar?: React.ReactElement`) can't be driven
87
+ by an auto-generated object control — the panel renders a broken `[object Object]`
88
+ input. Suppress it (`avatar: { control: false, table: { disable: true } }`) and
89
+ add a **boolean helper arg** under a `Slots` category that the render function maps
90
+ to a concrete element — e.g. a `showAvatar` toggle → `avatar={showAvatar ? <Avatar
91
+ … /> : undefined}`. This gives a real, working slot toggle without asking the user
92
+ to type JSX into the panel.
93
+
94
+ **Icon gallery story** — don't write a story per library icon. Generate ONE
95
+ searchable gallery story that imports the icon package and renders the grid
96
+ (optionally with click-to-copy import names). This mirrors the Figma Icons page.
97
+ Custom icons (SVGR-generated, owned by the repo) get normal individual stories
98
+ like any component.
99
+
100
+ ## Step 4 — Set up Chromatic
101
+
102
+ Set up Chromatic for visual regression testing. Generate the config and the CI
103
+ workflow that runs Chromatic on PRs. This needs a `CHROMATIC_PROJECT_TOKEN`:
104
+ - Following the secrets discipline (`.throughline/references/coding-level.md`), the token
105
+ value never passes through chat. Tell the user where to get it (Chromatic's
106
+ project setup page after signing in) and where it goes — `.env` locally
107
+ (gitignored) and the GitHub Actions secrets vault for CI. The user places it.
108
+ - This is the moment a previously-taught env-file concept becomes concrete
109
+ (repository-builder taught it; here they actually set the value, because
110
+ they've now decided to use Chromatic).
111
+ - Verify: after they add the secret, confirm CI runs Chromatic and goes green.
112
+
113
+ Scale all of this to `codingLevel` — full teaching for `new`, terse for
114
+ `comfortable`.
115
+
116
+ ### TurboSnap vs. design tokens — default to full snapshots
117
+
118
+ **Recommendation: leave TurboSnap OFF for a design system.** Snapshot every story
119
+ on every run. TurboSnap (`onlyChanged: true`) only re-snapshots stories whose
120
+ changed files it can trace incrementally — and for a token-driven system that
121
+ model is fundamentally fragile, because **token changes are global**: one token
122
+ edit can restyle every component, the opposite of the localized change TurboSnap
123
+ is built for.
124
+
125
+ Concretely, TurboSnap keeps missing token changes in two independent ways:
126
+
127
+ - **It doesn't trace changes inside a linked workspace package** resolved under
128
+ `node_modules` (e.g. `@<scope>/tokens` → `packages/tokens` build output), so a
129
+ token-only PR — the everyday `/sync-figma-tokens` loop — traces nothing and
130
+ reports "Capturing 0 snapshots." False green.
131
+ - **Its diffing is incremental against the previous build on the branch.** Once a
132
+ build has "consumed" a token change, a later commit (a workflow tweak, say)
133
+ won't re-snapshot it either. The `externals` option is only a partial
134
+ mitigation and, given the incremental model, is easy to defeat in practice.
135
+
136
+ So for a design system the robust default is: **snapshot all stories, always.** At
137
+ typical counts (dozens of stories) this is cheap and can *never* miss a global
138
+ token change. Only consider TurboSnap if the story count grows large enough that
139
+ full-run cost genuinely matters — and even then, treat any token change as
140
+ requiring a full run.
141
+
142
+ Configure Chromatic to snapshot everything (do **not** set `onlyChanged`), and
143
+ verify a token-only PR re-snapshots all stories — they should flip orange against
144
+ the green baseline.
145
+
146
+ ## Step 5 — Code Connect (plan-gated, skip gracefully)
147
+
148
+ Code Connect ties Figma components to their code counterparts so Figma's dev
149
+ mode shows the real code. It's plan-gated (Figma Organization/Enterprise).
150
+
151
+ - Detect or ask whether the user's plan supports Code Connect.
152
+ - **If yes:** wire it up — map each Figma component to its code component,
153
+ including the slot contracts (this is the formal home of the icon/component
154
+ slot bindings). Record `storybook.codeConnect` = `true`.
155
+ - **If no:** skip gracefully and say why in plain terms ("Code Connect needs a
156
+ Figma Organization plan — we'll skip it; everything else works, and your
157
+ component spec in the repo still records the Figma↔code mapping"). Don't block
158
+ the rest of the setup.
159
+ - **Publishing & pending swap upgrades:** Code Connect and typed slot mappings
160
+ line up best once the Figma library is published (see
161
+ `.throughline/references/figma-publishing.md`). If
162
+ `components.instanceSwapUpgradePending` is non-empty, those components still owe
163
+ a *typed instance-swap dropdown in Figma* — added by a later component-builder
164
+ run after the user publishes. This does **not** block the code side: implement
165
+ each slot prop from the recorded slot contract regardless of the Figma dropdown.
166
+
167
+ ## Step 6 — Finalize component status (Figma write-back)
168
+
169
+ A component built and storied here is now **done** — but its Figma doc card was
170
+ stamped `draft` by component-builder and won't change on its own. Once the code
171
+ component renders and its stories build (and the user has approved the result),
172
+ **promote each finalized component to `stable`** so the design system tells the
173
+ truth in both places.
174
+
175
+ **Confirm the write-back once, up front.** Before touching Figma, state the batched
176
+ change in one line and get a yes — *"I'll update the N doc cards in Figma: flip the
177
+ status chips amber → green and set Last Updated to today. Confirm?"* The user
178
+ approved the *components*, but writing to their Figma file is a separate external-
179
+ system action that needs explicit consent (and an unannounced write trips the safety
180
+ classifier, forcing the round-trip anyway). One confirmation covers all cards.
181
+
182
+ For every component you finalized in this run, follow the **"Promoting a
183
+ component's status (write-back on finalize)"** routine in
184
+ `.throughline/references/figma-component-standards.md` (which also covers
185
+ the `figma_execute` scripting gotchas — `getNodeByIdAsync` and an explicit
186
+ `timeout` for the multi-card write):
187
+
188
+ - Set `components.meta[name].status` = `"stable"` and refresh
189
+ `components.meta[name].updatedAt` to today.
190
+ - If Figma is connected (per `figma.mechanism`), open the component's doc card and
191
+ update the `Status Label` text to `stable`, re-bind the `Status` chip fill to
192
+ the **success** semantic color variable (mode-aware, not a hardcoded hex), and
193
+ set `Last Updated` to today's date — then screenshot to confirm the chip
194
+ recolored and the date changed.
195
+ - If Figma isn't connected, still update the manifest and tell the user the card
196
+ will reconcile next Figma session (or offer to reconnect and fix it now).
197
+
198
+ Once confirmed, do the whole batch in one pass as part of finishing — don't make
199
+ the user re-approve each chip. (`stable` is the finalized status; if a component is
200
+ intentionally still experimental, leave it at `beta` and say so.)
201
+
202
+ ## Step 7 — Update manifest + hand off
203
+
204
+ Set `storybook.initialized` = `true`, `storybook.chromatic` accordingly,
205
+ `storybook.codeConnect` accordingly. Append `storybook-chromatic-builder` to
206
+ `completedSkills`. (Per-component `status`/`updatedAt` were already updated in
207
+ Step 6.) Note the ongoing loop: new components flow through the
208
+ component-pipeline orchestrator; token changes flow through `/sync-figma-tokens`.
209
+
210
+ ## Brownfield: baseline before retrofit + the verification triad
211
+
212
+ On a **retrofit** (`tokens.intakeMode: "retrofit"`), the order of operations and the
213
+ verification bar are stricter than greenfield. **Read
214
+ `.throughline/references/brownfield-retrofit.md`** — the safe sequence and the
215
+ verification triad live there.
216
+
217
+ **Capture the Chromatic baseline BEFORE the code retrofit.** Run `build-storybook` +
218
+ Chromatic to establish a green baseline *before* any token/color code is changed. Then,
219
+ when the retrofit lands, every diff is either an **intended drift-fix** (a color the
220
+ audit flagged as wrong) or a **regression** — and the baseline is the only thing that
221
+ tells them apart. Baseline *after* the retrofit and you've thrown away that signal.
222
+
223
+ **The verification triad — all three are necessary; no single check catches everything:**
224
+ 1. **`check-types` (TypeScript)** — catches type errors, but is **blind to Tailwind
225
+ silent no-ops**: a deleted color utility just stops applying, with no type error
226
+ (guardrail 4).
227
+ 2. **`build-storybook` + Chromatic snapshots** — the visual-regression net, but **blind
228
+ to story-unreachable code**. `build-storybook` only compiles SCSS that some story
229
+ actually imports; a dead `@import` of a deleted partial, or a route's styles no story
230
+ renders, compiles "fine" here and breaks only in the app. **Chromatic — not
231
+ `tsc`/build — is the source of truth** for whether a color-utility removal was safe.
232
+ 3. **Run the actual app + spot-check 5–7 real routes** — the only thing that exercises
233
+ story-unreachable SCSS (guardrail 5). Don't declare an SCSS/color change done on the
234
+ strength of a green build alone.
235
+
236
+ **Don't assume the stack (§11).** This triad names Chromatic + `build-storybook`
237
+ because that's the case-study tooling. Detect what the repo actually uses — read its
238
+ `package.json` scripts for the real type-check / build / visual-test commands — and map
239
+ the triad onto them (or degrade gracefully and say so) rather than asserting commands
240
+ that may not exist.
241
+
242
+ ## What this skill must NOT do
243
+
244
+ - Never finish a finalized component while leaving its Figma doc card on `draft`
245
+ — promote the status and write it back (Step 6).
246
+ - Never write a story per library icon — one gallery story.
247
+ - Never put a secret value through chat or commit it — user places it, scaled to
248
+ level.
249
+ - Never block setup when Code Connect is unavailable — skip gracefully.
250
+ - Never hardcode token values in components — consume `packages/tokens`.
251
+ - Never rely on TurboSnap (`onlyChanged: true`) for a token-driven design system
252
+ — its incremental model keeps missing global token changes. Default to full
253
+ snapshots (every story, every run); revisit only at large story counts.
254
+ - Never use the sequential model for story-gen — parallelize via subagents.
255
+ - Never capture the Chromatic baseline *after* a code retrofit — baseline before, so
256
+ intended drift-fixes are distinguishable from regressions.
257
+ - Never trust `tsc`/build to catch Tailwind color-utility removal — it's a silent
258
+ no-op; Chromatic is the source of truth (guardrail 4).
259
+ - Never declare an SCSS/color change done on a green build alone — run the app and
260
+ spot-check 5–7 real routes (guardrail 5).
261
+
@@ -0,0 +1,361 @@
1
+ # Token builder
2
+
3
+ > **Critical collection rule:** this system uses **one collection per category per tier** — never one giant `Primitives` + one giant `Semantic`. Color, spacing, typography, radius, and border each get their own primitive collection and their own semantic collection. This is non-negotiable: modes (Light/Dark, Desktop/Mobile) belong to a collection, so mixing categories in one collection forces unrelated variables to share the same mode axis.
4
+
5
+ > **Model tip (#3):** building a coherent token system (ramps, type scale, modes,
6
+ > primitive→semantic aliasing) is reasoning-heavy. It runs on your session model —
7
+ > Sonnet is a solid default; Opus helps for large or multi-mode systems. See the
8
+ > model guide in the ThroughLine README.
9
+
10
+ Creates a two-tier design token system as Figma variables. Each tier is split
11
+ into **one collection per category** (color, spacing, type, radius, border) so
12
+ every category owns its own modes — see Step 1 for why. Within a collection,
13
+ Figma variables are grouped with `/` and omit the category prefix (the
14
+ collection already carries it); the sync layer later derives the dotted logical
15
+ name (`color.gray.50`) by prefixing the collection's category.
16
+
17
+ - **Primitives** — raw values with no meaning attached: `gray/50`, `space/4`
18
+ in their category's primitive collection. The full palette of possible values.
19
+ - **Semantic** — meaning-bearing tokens that **alias onto primitives**:
20
+ `bg/default` → `{gray/50}`, `text/primary` → `{gray/900}`. This is the layer
21
+ the rest of the system consumes.
22
+
23
+ The power of two tiers: change a primitive once and every semantic token
24
+ referencing it updates automatically, which cascades through sheets, components,
25
+ and synced code. Preserving these aliases (not flattening them) is what makes
26
+ runtime theming work later, so **always create semantic tokens as references to
27
+ primitives, never as copied literal values.**
28
+
29
+ ## Prerequisites
30
+
31
+ This skill needs a live Figma connection. Read the manifest
32
+ (`.throughline/references/manifest-schema.md` for the schema) and check `figma.connected`.
33
+ Then do a cheap liveness read to confirm the connection is actually live right
34
+ now. If it isn't, say so plainly and offer to run the `figma-environment-setup`
35
+ skill first: "Figma isn't connected yet — want me to set that up? It's a
36
+ one-time thing." Don't proceed until Figma is reachable.
37
+
38
+ Use the write mechanism recorded in `figma.mechanism` (default `console-mcp`).
39
+ With Console MCP, prefer `figma_execute` to create variables in scripted loops
40
+ rather than one tool call per variable — this is dramatically more
41
+ token-efficient for the potentially hundreds of primitives across modes.
42
+
43
+ **Scripting via `figma_execute`:** see
44
+ `.throughline/references/figma-scripting.md` for the shared gotchas —
45
+ the single-bridge-instance preflight (concurrent writes corrupt the file), the
46
+ `dynamic-page` async APIs (synchronous document-wide getters like
47
+ `getLocalVariableCollections` **and** setters like `figma.currentPage =` /
48
+ `node.textStyleId =` throw — use `getLocalVariableCollectionsAsync`,
49
+ `setCurrentPageAsync`, `setTextStyleIdAsync`, …), the `resize()` axis-lock trap,
50
+ and why large `WRAP` grids time out. For a simple verification read, prefer the
51
+ dedicated `figma_get_variables` tool (it handles dynamic-page correctly and
52
+ resolves aliases with `resolveAliases: true`) over a hand-written script.
53
+
54
+ ## Step 1 — Brainstorm the structure (before building anything)
55
+
56
+ Run the protocol in `.throughline/references/brainstorm-before-build.md`. **First establish
57
+ the intake mode** (generative / descriptive / import) per that reference — it
58
+ changes how much you generate versus preserve. **In import mode, do not stop at a
59
+ 1:1 transcription of what the user fed you** (a brand guide or marketing site is
60
+ almost always a partial system): after preserving and organizing their values,
61
+ run the **completeness pass** from that reference — diff against the full-system
62
+ model and proactively propose the missing pieces (tonal ramps, neutral ramp,
63
+ state colors, dark mode, semantic roles, elevation/radius scales) as a
64
+ preserve-first, opt-in menu derived from their existing values. Then lock the
65
+ structure with the user, in readable chunks:
66
+
67
+ - **Seeds and direction** — capture what the user actually has: brand color(s),
68
+ font(s), aesthetic words (modern, rounded, dense, comfortable), reference
69
+ images/URLs, or an existing token set to import. In generative mode, this is
70
+ the seed you expand from; in import mode, this is the set you organize.
71
+ - **Color ramps** — which hues (e.g. gray, brand primary, success, warning,
72
+ danger), and how many steps each (a 50–900 ramp of ~10 steps is a sensible
73
+ default). In generative mode, derive a full tonal ramp and harmonized
74
+ supporting families from the user's seed color(s).
75
+ - **Modes** — does the system need light + dark? Brand variants? Density? This
76
+ is high-impact and shapes everything downstream (it determines how many values
77
+ each variable holds and how the sync layer emits themes), so brainstorm it
78
+ carefully. Default: light + dark.
79
+ - **Spacing scale** — base unit and steps (e.g. 4px base: 0, 1, 2, 3, 4, 6, 8,
80
+ 12, 16, 24...). Default to a 4px-based scale; "dense" pulls it tighter,
81
+ "comfortable" looser.
82
+ - **Type scale** — font families, the size ramp, weights, line-heights. In
83
+ generative mode, propose complementary font pairings from the user's seed
84
+ font. Default to a modular scale.
85
+ - **Radius / border / shadow** scales as needed. "Rounded" → larger radius
86
+ scale.
87
+ - **Primitive naming convention** — the single most important decision, because
88
+ the semantic tier aliases onto these names and renaming later breaks every
89
+ alias. Lock it explicitly. In Figma, name variables **without the category
90
+ prefix** (the collection supplies it) and group with `/`: e.g. `gray/50`,
91
+ `space/4`, `radius/md` inside their category's primitive collection. The sync
92
+ layer derives the dotted logical identity (e.g. `color.gray.50`) by prefixing
93
+ the collection's category. Keep names **neutral and semantic — never
94
+ framework-specific** (don't name a token `--background` to match shadcn; the
95
+ adapter renames per framework later). Figma is framework-agnostic; the adapter
96
+ absorbs all framework-specific shaping.
97
+ - **Tiers** — default to **two-tier** (primitive + semantic). Only raise the
98
+ option of a third **component** tier if the user signals multi-brand,
99
+ white-labeling, or a very large/robust component library — for a single-brand
100
+ project it adds complexity without payoff, so don't even surface it. If the
101
+ user opts in, component tokens alias onto semantic tokens (e.g.
102
+ `bg/primary` → `{bg/emphasis}`).
103
+ - **Collection structure** — two *tiers*, but **one collection per category per
104
+ tier**, never one giant `Primitives` + one giant `Semantic`. In Figma a mode
105
+ axis (Light/Dark, Desktop/Mobile, Brand) belongs to the *collection*, so every
106
+ variable in a collection is forced to share its modes. Putting `space` in the
107
+ same collection as `color` drags spacing into Light/Dark, which is meaningless.
108
+ Default layout (single-brand): private primitive collections `_Color/Primitive`,
109
+ `_Typography/Primitive`, `_Radius/Primitive`, `_Border/Primitive`, and a
110
+ **public** `Spacing/Primitive`; published semantic collections `Color/Semantic`
111
+ (Light/Dark), `Spacing/Semantic`, `Typography/Semantic`, `Radius/Semantic`,
112
+ `Border/Semantic`. Privacy is the leading-`_` prefix. `size/icon/*` lives in
113
+ `Spacing/Primitive`; don't create a `Sizing` collection unless control
114
+ heights/avatars become real tokens.
115
+ - **Multi-brand** — keep two axes in two collections so they don't multiply.
116
+ Brand lives on `_Color/Primitive` (modes = Brand A, Brand B = raw palettes);
117
+ Theme lives on `Color/Semantic` (modes = Light, Dark). A frame sets both
118
+ independently and Figma resolves `bg/default`(Light) → `{gray/50}` → Brand A's
119
+ gray. This keeps each collection ≤2 modes — under the Figma **Professional cap
120
+ of 4 modes/collection**. (Brand-on-primitive assumes brands differ in raw
121
+ palette, same role mapping; if a brand needs a *different* mapping, it also
122
+ becomes a mode on `Color/Semantic`.)
123
+
124
+ Show the proposed full structure back to the user and get sign-off before
125
+ creating anything.
126
+
127
+ ### The structural-consistency rule (prevents the "different every time" problem)
128
+
129
+ Every token concern gets **both** a primitive and a semantic collection, even
130
+ when the semantic tier is a 1:1 passthrough today. A dimensional semantic tier
131
+ (`Spacing/Semantic`, `Radius/Semantic`, `Border/Semantic`) is justified by a
132
+ **plausible future mode axis** — e.g. adding Desktop/Mobile to spacing later —
133
+ which only works if the semantic collection already exists to carry that axis.
134
+ Building every concern the same way is also what makes the output consistent
135
+ run-to-run instead of an arbitrary guess about which categories to duplicate.
136
+
137
+ The one guardrail: **consistency does not license invented roles.** Semantic
138
+ names must be real usage roles (`inset/md`, `width/focus`, `text/primary`),
139
+ never just renamed primitive steps (`space/12`, `width/1`). A passthrough role
140
+ with a meaningful name is fine; a fake role nobody applies is not. If you can't
141
+ name a genuine role for a category, give it semantic roles that map to actual
142
+ usage rather than mirroring the primitive scale step-for-step.
143
+
144
+ ## Step 1.5 — Brownfield: refine existing variables in place (don't rebuild)
145
+
146
+ If this is a **retrofit** (`tokens.intakeMode: "retrofit"`, or the file already has
147
+ variables), do **not** create a fresh set on top of the old one. Refine what exists,
148
+ in place. **Read `.throughline/references/brownfield-retrofit.md` first** —
149
+ guardrail 3 is the whole point of this branch.
150
+
151
+ **The hard rule (guardrail 3):** rename and realign variables **in place** to preserve
152
+ their Figma IDs. **Never delete-and-recreate** — every binding (a populated file can
153
+ have thousands) references the variable *id*, so recreating under the same name still
154
+ unbinds everything. A rename keeps the id; a delete+create does not.
155
+
156
+ **The refine loop:**
157
+
158
+ 1. **Read what exists** (read discipline). Use `figma_get_variables` after
159
+ `await figma.loadAllPagesAsync()`. List the existing collections, variables, and
160
+ values. Treat a `0`/empty first read as suspect — re-read before concluding the file
161
+ is empty.
162
+ 2. **Snapshot bindings before.** Run the binding-survival audit in
163
+ `.throughline/references/figma-scripting.md` to record the total consuming
164
+ binding count *before* any change. This is your guardrail-3 tripwire.
165
+ 3. **Rename / realign in place.** Use the rename operation that keeps the id (e.g.
166
+ `figma_rename_variable` / setting `variable.name`), and update values in place. Map
167
+ old names to the new two-tier scheme; record each old→new mapping (the crosswalk
168
+ consumes it later via `token-crosswalk-builder`).
169
+ 4. **Snapshot bindings after, and assert equality.** Re-run the binding-survival audit.
170
+ The total **must be unchanged**. A drop means a binding was severed — STOP, find the
171
+ delete-and-recreate that slipped in, and restore from the baseline.
172
+ 5. **Add genuinely-new variables** (the `added` rows) as normal creates — only the
173
+ *existing* ones must be renamed rather than recreated.
174
+
175
+ After the refine loop, continue with the normal tiers below for any net-new structure,
176
+ but **skip recreating anything that already exists**. The primitive/semantic checkpoint
177
+ (Step 2's PAUSE) still applies: lock names before building dependent semantics.
178
+
179
+ ## Step 2 — Build the PRIMITIVE tier, then PAUSE
180
+
181
+ Create the **per-category primitive collections** and their variables via a
182
+ scripted loop on the active write mechanism. Default set:
183
+
184
+ - `_Color/Primitive` — color ramps (`gray/50…900`, `brand/50…900`,
185
+ `success`/`warning`/`danger`, `white`, `black`).
186
+ - `Spacing/Primitive` — the spacing scale (`space/0,2,4,8,12,16,24,32,48,64`)
187
+ **plus** `size/icon/{sm,md,lg}`.
188
+ - `_Typography/Primitive` — `family/*`, `size/*`, `weight/*`, `lineHeight/*`,
189
+ `letterSpacing/*`.
190
+ - `_Radius/Primitive` — `radius/{none,sm,md,lg,xl,full}`.
191
+ - `_Border/Primitive` — `width/{0,1,2,4}`. **Do not skip border width** — borders
192
+ need a width primitive, not only a color.
193
+
194
+ **Privacy:** the leading-`_` prefix hides a collection from the published
195
+ library. Keep color/type/radius/border primitives private (they're always
196
+ consumed through semantics or styles). Make **`Spacing/Primitive` public** (no
197
+ underscore) — spacing semantics are intentionally minimal, so designers will grab
198
+ raw `space/*` for one-off gaps. Note the trade-off in your checkpoint summary: a
199
+ value applied directly from `Spacing/Primitive` is *frozen across device modes*;
200
+ only `Spacing/Semantic` carries future Desktop/Mobile responsiveness.
201
+
202
+ **Modes at the primitive tier:** primitives are usually mode-free (a single
203
+ *Value* mode). The exception is multi-brand: give `_Color/Primitive` a Brand mode
204
+ axis (Brand A, Brand B) holding each brand's raw palette. All other primitive
205
+ collections stay single-mode.
206
+
207
+ Then **stop and checkpoint.** This is the critical seam: semantic tokens are
208
+ about to alias onto these primitives, so the primitive names and values must be
209
+ right *before* you build on them. Show the user the created collections —
210
+ summarize the ramps and scales, note which are public vs private, and if helpful
211
+ mention the token-sheet-builder skill can render them visually later. Ask for
212
+ explicit confirmation: "Here are your primitives. Once you're happy, I'll build
213
+ the semantic layer on top — and after that, renaming primitives gets disruptive,
214
+ so this is the moment to adjust names or values."
215
+
216
+ Update the manifest: `tokens.primitivesBuilt` = `true`, add every created
217
+ collection name to `tokens.collections`.
218
+
219
+ Do not proceed to the semantic tier until the user confirms.
220
+
221
+ ## Step 3 — Build the SEMANTIC tier as aliases
222
+
223
+ Create the **per-category semantic collections**, where every variable
224
+ **references a primitive**, not a literal value. Bind each semantic variable to
225
+ its primitive so the alias is live. Default set and modes:
226
+
227
+ - `Color/Semantic` — modes **Light, Dark** (+ a Brand mode only if a brand needs
228
+ a different *mapping*, not just a different palette). Roles: `bg/{default,
229
+ subtle,muted,emphasis,inverse}`, `text/{primary,secondary,disabled,inverse,
230
+ link,onEmphasis}`, `border/{default,subtle,focus,emphasis}`,
231
+ `status/{success,warning,danger}/{bg,text,border}`,
232
+ `shadow/{ambient,key}`. The **`shadow/*`** roles are the colors the elevation
233
+ effect styles consume (Step 4) — they carry alpha and are **mode-aware**: a
234
+ low-alpha near-black in Light, and a darker/higher-alpha value in Dark, so
235
+ toggling modes recolors every elevation. They live here in `Color/Semantic`
236
+ (rather than as part of the effect style) precisely so the Light/Dark switch
237
+ reaches them; alias a near-black primitive where the alpha allows, otherwise
238
+ set the rgba per mode directly. **`text/onEmphasis`** is the
239
+ label/icon color that sits **on** `bg/emphasis` (a primary button's text, a
240
+ filled badge) — it must contrast with the emphasis fill in *every* mode, so it's
241
+ a distinct role, **not** `text/inverse` (which flips with the theme and won't
242
+ reliably contrast a brand-colored emphasis fill). Component-builder binds primary/
243
+ filled control labels to this role; without it, builds fall back to literals and
244
+ break the "everything bound" audit.
245
+ - `Spacing/Semantic` — single *Default* mode now, structured so Desktop/Mobile
246
+ can be added later. Roles: `inset/{xs,sm,md,lg,xl}`, `stack/*`, `inline/*`.
247
+ - `Typography/Semantic` — single *Default* mode (room for Desktop/Mobile). Roles:
248
+ `size/{body,bodyLg,heading/sm…xl,caption}` and role line-heights; these feed
249
+ the text styles built in Step 4.
250
+ - `Radius/Semantic` — single mode. Roles: `control`, `card`, `pill`, `field`.
251
+ - `Border/Semantic` — single mode. Roles: `width/{default,focus,emphasis}`
252
+ aliasing `_Border/Primitive` widths, plus **`offset/focus`** (the gap between a
253
+ control's edge and its focus ring, the `outline-offset` equivalent) aliasing the
254
+ `2` width primitive. It's used by the **outline-style** focus recipe (e.g.
255
+ `vanilla-css`) to keep the ring clear of the edge for accessibility; library idioms
256
+ that render focus as a spread-shadow ring don't need it — see "State handling" in the
257
+ component standards for the per-library recipes.
258
+ - `Opacity/Semantic` (when the system needs disabled/overlay states) — single
259
+ mode. Roles: `disabled`, `muted`, `overlay/scrim`, `hover`, aliasing
260
+ `_Opacity/Primitive`. See the **opacity scale rule** below — this is the one
261
+ category where the Figma value scale is a trap.
262
+
263
+ > **⚠️ Opacity is stored 0–100, NOT 0–1 (critical).** When a variable is bound to
264
+ > a node's **`opacity`** field, Figma treats the value as a **0–100 percentage and
265
+ > divides by 100**. So an opacity primitive must be created on the **0–100 scale**:
266
+ > `_Opacity/Primitive` `40` has the value **`40`** (which renders as 0.4), never
267
+ > `0.4` (which would render as 0.004 — effectively invisible). Name and value must
268
+ > match (`40` = `40`), and a bound `disabled`/`overlay` element renders correctly.
269
+ > The sync layer is responsible for the inverse: CSS `opacity` is 0–1, so it
270
+ > **÷100 on emit** — token-builder and token-sync-layer are a **matched pair** here.
271
+
272
+ These dimensional semantic tiers are often passthroughs today — that's expected
273
+ under the structural-consistency rule; keep the role names real (`inset/md`, not
274
+ `space/16`).
275
+
276
+ The **color** semantic tier is where the Light/Dark switch lives:
277
+ `bg/default` → `{gray/50}` in Light and `{gray/900}` in Dark. Primitives
278
+ stay fixed; the semantic aliases differ per mode. This is exactly what lets the
279
+ sync layer emit `:root`/`.dark` for web later.
280
+
281
+ **Mode-application reality (state this to the user):** a frame can now carry up
282
+ to three independent modes — Brand (`_Color/Primitive`), Theme (`Color/Semantic`),
283
+ and later Device (`Spacing`/`Typography`). That's the cost of independent axes.
284
+
285
+ Verify the aliases resolve — use `figma_get_variables` (filtered to the new
286
+ semantic collection, `resolveAliases: true`) to confirm semantic tokens point at
287
+ primitives, not literals; if you read via a `figma_execute` script instead, use
288
+ the async APIs (see the dynamic-page note in Prerequisites). Then checkpoint:
289
+ show the semantic layer and demonstrate the cascade if useful ("change `gray/50`
290
+ and `bg/default` follows").
291
+
292
+ Update the manifest: `tokens.semanticBuilt` = `true`, add every semantic
293
+ collection to `tokens.collections`.
294
+
295
+ ## Step 4 — Build Figma STYLES (the third phase)
296
+
297
+ Figma variables can't express everything a design system needs. Composed
298
+ **styles** must be created as a distinct phase *after* the variables exist, so
299
+ they can **bind to the tokens you just built** rather than duplicating values:
300
+
301
+ - **Text styles** — a full type scale needs one text style per role/size
302
+ (e.g. `Heading/XL`, `Body/Default`, `Caption`), each composing family + size +
303
+ weight + line-height + letter-spacing. Bind size and family to the
304
+ corresponding `font.*` variables where Figma supports it, so changing a font
305
+ primitive updates the text styles. A type scale is NOT just variables — these
306
+ composed text styles are what designers actually apply to text layers.
307
+ - **Effect styles** — drop shadows and elevation levels (e.g. `Elevation/1`
308
+ through `Elevation/5`), driven by the shadow scale from the brainstorm. The
309
+ *composition* (offset, blur, spread) is what makes this a style rather than a
310
+ variable — but the shadow **color must be bound to the `shadow/*`
311
+ `Color/Semantic` variables**, never hardcoded. Figma supports binding the color
312
+ of a drop shadow inside an effect style to a color variable; do that for every
313
+ shadow in every elevation level. This is what makes elevations **mode-aware**:
314
+ because the bound `shadow/*` variable carries different values per mode, toggling
315
+ Light/Dark recolors all elevations automatically. A style with a literal shadow
316
+ color is the bug — it stays the same color in dark mode. (Heavier elevations may
317
+ layer two shadows, e.g. an ambient + a key shadow; bind each to `shadow/ambient`
318
+ and `shadow/key` respectively.)
319
+ - **Grid styles** — layout grids if the user wants them (column/row grids for
320
+ their breakpoints). Optional; only if requested.
321
+
322
+ Generate styles via the active write mechanism (scripted where possible). Bind
323
+ to variables wherever Figma allows so styles consume tokens rather than
324
+ duplicating them. Checkpoint with the user: show the created styles.
325
+
326
+ Update the manifest: set `tokens.stylesBuilt` = `true` and record which style
327
+ groups were created in `tokens.styleGroups`. Append `token-builder` to
328
+ `completedSkills`.
329
+
330
+ ## Step 5 — Hand off
331
+
332
+ Tell the user what's unlocked and offer natural next steps without forcing them:
333
+ generate a visual stylesheet of all tokens (token-sheet-builder), build an icon
334
+ system (icon-system-builder), or start on components (component-builder). If
335
+ they're heading toward code, mention that the token-sync skill will later turn
336
+ these exact variables into code files — but only when they have a repo, which
337
+ comes later.
338
+
339
+ ## Notes that matter
340
+
341
+ - **Never flatten semantic into literals.** Aliases are the whole point.
342
+ - **One collection per category per tier.** Modes belong to the collection, so
343
+ splitting by category is what keeps color's Light/Dark from infecting spacing.
344
+ - **Privacy is the `_` prefix.** Only `Spacing/Primitive` is public by default;
345
+ all other primitives are private.
346
+ - **Primitives can be multi-mode.** Brand lives on `_Color/Primitive`; the sync
347
+ layer must emit brand themes from the primitive tier, not just light/dark from
348
+ semantics.
349
+ - **Lock primitive names before building semantics.** The checkpoint between
350
+ tiers exists precisely to prevent rename cascades.
351
+ - **Think in DTCG terms even though you're writing Figma variables.** Each token
352
+ has a value and a type (color, dimension, fontFamily, etc.). The sync layer
353
+ will later normalize these Figma variables into DTCG-format JSON, so keep
354
+ types clean and consistent — it makes the downstream sync trivial.
355
+ - **Token-efficiency:** scripted loops via `figma_execute`, batched per tier —
356
+ not per-variable tool calls.
357
+ - **Brownfield: never delete-and-recreate to rename (guardrail 3).** On a retrofit,
358
+ rename variables in place to preserve their Figma IDs and every binding. Snapshot the
359
+ binding count before and after each rename and assert it's unchanged — see Step 1.5
360
+ and `.throughline/references/figma-scripting.md`.
361
+