@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.
- package/LICENSE +21 -0
- package/README.md +214 -0
- package/adapters/codex/AGENTS.md +29 -0
- package/adapters/codex/codex-mcp.toml +4 -0
- package/adapters/codex/prompts/component-builder.md +272 -0
- package/adapters/codex/prompts/component-pipeline.md +81 -0
- package/adapters/codex/prompts/design-system-audit.md +122 -0
- package/adapters/codex/prompts/design-system-status.md +41 -0
- package/adapters/codex/prompts/figma-environment-setup.md +456 -0
- package/adapters/codex/prompts/icon-system-builder.md +268 -0
- package/adapters/codex/prompts/new-component.md +13 -0
- package/adapters/codex/prompts/repository-builder.md +163 -0
- package/adapters/codex/prompts/retrofit-planner.md +136 -0
- package/adapters/codex/prompts/start.md +22 -0
- package/adapters/codex/prompts/storybook-chromatic-builder.md +261 -0
- package/adapters/codex/prompts/sync-figma-tokens.md +22 -0
- package/adapters/codex/prompts/token-builder.md +361 -0
- package/adapters/codex/prompts/token-crosswalk-builder.md +120 -0
- package/adapters/codex/prompts/token-sheet-builder.md +162 -0
- package/adapters/codex/prompts/token-sync-layer.md +232 -0
- package/adapters/cursor/.cursor/commands/design-system-status.md +41 -0
- package/adapters/cursor/.cursor/commands/new-component.md +13 -0
- package/adapters/cursor/.cursor/commands/start.md +22 -0
- package/adapters/cursor/.cursor/commands/sync-figma-tokens.md +22 -0
- package/adapters/cursor/.cursor/mcp.json +14 -0
- package/adapters/cursor/.cursor/rules/component-builder.mdc +276 -0
- package/adapters/cursor/.cursor/rules/component-pipeline.mdc +85 -0
- package/adapters/cursor/.cursor/rules/design-system-audit.mdc +126 -0
- package/adapters/cursor/.cursor/rules/figma-environment-setup.mdc +460 -0
- package/adapters/cursor/.cursor/rules/icon-system-builder.mdc +272 -0
- package/adapters/cursor/.cursor/rules/repository-builder.mdc +167 -0
- package/adapters/cursor/.cursor/rules/retrofit-planner.mdc +140 -0
- package/adapters/cursor/.cursor/rules/storybook-chromatic-builder.mdc +265 -0
- package/adapters/cursor/.cursor/rules/token-builder.mdc +365 -0
- package/adapters/cursor/.cursor/rules/token-crosswalk-builder.mdc +124 -0
- package/adapters/cursor/.cursor/rules/token-sheet-builder.mdc +166 -0
- package/adapters/cursor/.cursor/rules/token-sync-layer.mdc +236 -0
- package/adapters/generic/AGENTS.md +46 -0
- package/adapters/generic/commands/design-system-status.md +41 -0
- package/adapters/generic/commands/new-component.md +13 -0
- package/adapters/generic/commands/start.md +22 -0
- package/adapters/generic/commands/sync-figma-tokens.md +22 -0
- package/adapters/generic/skills/component-builder/SKILL.md +272 -0
- package/adapters/generic/skills/component-pipeline/SKILL.md +81 -0
- package/adapters/generic/skills/design-system-audit/SKILL.md +122 -0
- package/adapters/generic/skills/figma-environment-setup/SKILL.md +456 -0
- package/adapters/generic/skills/icon-system-builder/SKILL.md +268 -0
- package/adapters/generic/skills/repository-builder/SKILL.md +163 -0
- package/adapters/generic/skills/retrofit-planner/SKILL.md +136 -0
- package/adapters/generic/skills/storybook-chromatic-builder/SKILL.md +261 -0
- package/adapters/generic/skills/token-builder/SKILL.md +361 -0
- package/adapters/generic/skills/token-crosswalk-builder/SKILL.md +120 -0
- package/adapters/generic/skills/token-sheet-builder/SKILL.md +162 -0
- package/adapters/generic/skills/token-sync-layer/SKILL.md +232 -0
- package/package.json +29 -0
- package/references/brainstorm-before-build.md +139 -0
- package/references/brownfield-retrofit.md +93 -0
- package/references/coding-level.md +70 -0
- package/references/crosswalk-schema.md +113 -0
- package/references/figma-component-standards.md +532 -0
- package/references/figma-publishing.md +125 -0
- package/references/figma-scripting.md +278 -0
- package/references/manifest-schema.md +341 -0
- package/references/scaling-up-handoff.md +61 -0
- package/references/sync-adapters.md +141 -0
- package/scripts/README.md +78 -0
- package/scripts/adapters/emit-codex.mjs +58 -0
- package/scripts/adapters/emit-cursor.mjs +26 -0
- package/scripts/adapters/emit-generic.mjs +41 -0
- package/scripts/adapters/generate.mjs +73 -0
- package/scripts/adapters/read-sources.mjs +54 -0
- package/scripts/adapters/translate.mjs +51 -0
- package/scripts/build-reverse-index.mjs +56 -0
- package/scripts/crosswalk.schema.json +33 -0
- package/scripts/grep-color-usage.mjs +143 -0
- package/scripts/guard-token-removal.mjs +95 -0
- package/scripts/install.mjs +143 -0
- package/scripts/lib/crosswalk.mjs +91 -0
- 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,22 @@
|
|
|
1
|
+
Re-run the token sync pipeline to catch any updates made in Figma.
|
|
2
|
+
|
|
3
|
+
Follow the `token-sync-layer` prompt's pipeline using the settings already
|
|
4
|
+
recorded in `design-system.json` (`sync.platforms`, `figma.mechanism`,
|
|
5
|
+
`figma.fileKey`) — don't re-ask configuration that's already set. Specifically:
|
|
6
|
+
|
|
7
|
+
1. Verify Figma is connected (cheap liveness read); if not, offer
|
|
8
|
+
`figma-environment-setup`.
|
|
9
|
+
2. Re-extract Figma variables to DTCG JSON.
|
|
10
|
+
3. Rebuild outputs through Style Dictionary for every platform in
|
|
11
|
+
`sync.platforms`.
|
|
12
|
+
4. Run full-regeneration with **rename detection** — diff against the previous
|
|
13
|
+
output, flag probable renames (vanished + reappeared with identical
|
|
14
|
+
`$value`/`$type`) and plain deletions.
|
|
15
|
+
5. Open a **reviewable PR** (or a reviewed branch diff if the repo is only
|
|
16
|
+
`local-git`) summarizing added / changed / deleted / probable-renames. Never
|
|
17
|
+
write silently.
|
|
18
|
+
6. Update `sync.lastRun` and `tokens.lastSync` in the manifest.
|
|
19
|
+
|
|
20
|
+
Scale explanation to `user.codingLevel`. This is the recurring loop of the whole
|
|
21
|
+
system: tokens change in Figma → `/sync-figma-tokens` → review PR → merge.
|
|
22
|
+
|
|
@@ -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
|
+
|