@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,532 @@
1
+ # Figma component standards
2
+
3
+ Best practices for constructing components in Figma so they're consistent,
4
+ maintainable, and translate cleanly to code. The component-builder skill follows
5
+ these; getting them right in Figma directly improves the quality of the code the
6
+ storybook skill generates (auto layout → flex/padding, variants → props).
7
+
8
+ ## Auto layout on everything
9
+
10
+ Every component and meaningful container uses **auto layout**. This is
11
+ non-negotiable:
12
+
13
+ - Components must resize correctly with their content (a button grows with its
14
+ label; a card grows with its body).
15
+ - Auto layout structure maps directly to code: direction → flex-direction,
16
+ spacing → gap, padding → padding, alignment → justify/align. A component built
17
+ with proper auto layout generates clean flex-based code; one built with
18
+ absolute positioning generates brittle, hard-to-maintain code.
19
+ - Bind padding and gap to **spacing tokens**, not hardcoded numbers, so the
20
+ token cascade reaches layout.
21
+
22
+ ## Clip content — off by default
23
+
24
+ Figma creates every new frame with **`clipsContent = true`**, so if you don't set
25
+ it, your layout and component frames silently clip. That clipping cuts off anything
26
+ a child draws **at or past the frame's edge**, which is exactly the stuff that makes
27
+ a component read as polished:
28
+
29
+ - **Outer strokes / borders.** A child with `strokeAlign = "OUTSIDE"` (or a flush-to-
30
+ edge border) gets the outer half of its stroke sliced away — borders look thin,
31
+ uneven, or missing on one side.
32
+ - **Focus rings.** A focus ring extends *beyond* the control's edge by design (see
33
+ *State handling*), so a clipping **ancestor** eats it — keep the doc card, variant
34
+ rows, and component set unclipped. One deliberate exception: a *filled* control that
35
+ casts its focus ring as a **drop-shadow effect** must have `clipsContent = true` on
36
+ that control frame itself — a frame's own effect isn't clipped by its own clip, and
37
+ clipping gives it a clean ring silhouette. The no-clip rule is about *ancestors*, not
38
+ that control frame.
39
+ - **Shadows / elevation.** Drop shadows on a child near the edge get cut at the frame
40
+ boundary instead of feathering out.
41
+
42
+ **The rule:** explicitly set **`clipsContent = false`** on component frames, variant
43
+ rows, component sets, and the layout frames that hold them. Don't rely on the default —
44
+ set it, and read it back.
45
+
46
+ **Turn clipping ON only for a deliberate cutoff** — a frame whose *job* is to crop its
47
+ contents to a fixed window: a scroll container, an image/media crop frame, an
48
+ avatar/thumbnail masked to its bounds, or anything emulating CSS `overflow: hidden`.
49
+ The one other legitimate ON case is a **filled control frame that casts its focus ring
50
+ as a drop-shadow effect** — clip ON gives that ring a clean silhouette (see *State
51
+ handling*), and clipping the frame's *children* doesn't clip the frame's own effect.
52
+ Everywhere else, leave content unclipped so strokes, focus rings, and shadows render in
53
+ full.
54
+
55
+ ## Variants vs. component properties — use the right tool
56
+
57
+ Figma offers variants (a matrix of discrete options) and component properties
58
+ (boolean, text, instance-swap). Use them deliberately:
59
+
60
+ - **Variants** — for discrete, mutually-exclusive style states that change the
61
+ component's appearance: `type` (primary/secondary/...), `size` (sm/md/lg),
62
+ `state` (default/hover/disabled). These become the code component's variant
63
+ props.
64
+ - **Boolean property** — for show/hide of an element. But remember: in code this
65
+ collapses into prop *optionality* (passing the slot shows it), so don't model
66
+ things as booleans that are really "is this slot filled."
67
+ - **Text property** — for editable labels.
68
+ - **Instance-swap property** — for slots that hold another component (icons,
69
+ avatars). These become the typed slot props (icon-set / typed-component).
70
+
71
+ Avoid variant explosion: don't create a variant axis for something that should
72
+ be a property. A button with type × size × state as variants, plus icon slots as
73
+ instance-swap properties, is correct. Making "has icon" a variant axis doubles
74
+ the matrix needlessly.
75
+
76
+ ## Component set arrangement (the variant matrix layout)
77
+
78
+ A `ComponentSet` (the frame holding all variants) must itself be a clean
79
+ **auto-layout** frame, not a scatter of variants at arbitrary coordinates. The
80
+ layout law is fixed: **variants are rows, states are columns.** Lay the matrix out
81
+ as a readable grid so it's scannable in Figma and predictable run-to-run:
82
+
83
+ - **Variants are always their own row; states are always the columns.** Each row is
84
+ a single variant (one `type`, or one `type`+`size` combination) and steps through
85
+ every `state` left-to-right (default → hover → focus → active → disabled →
86
+ loading…) as the columns. The next row is the next variant. Reading down the rows
87
+ enumerates the variants; reading across a row enumerates the states. This holds
88
+ for every component — never put states on rows or variants on columns.
89
+ - **Size variations are variants — each size gets its own row.** A different size is
90
+ a distinct variant, not a state, so treat `type × size` as the row identity (one
91
+ row per `type`+`size` combination, still stepping through states across the
92
+ columns) and **stack the size groups vertically** (all `sm` rows, then all `md`,
93
+ then `lg`), so the layout stays a 2-D grid instead of sprawling sideways. Never
94
+ model size as a column or as anything other than its own row.
95
+ - **Always include the full relevant state set per row** — don't ship a component
96
+ with only `default`. Every component renders its complete, applicable state set
97
+ across the columns (see "State handling" for the per-component checklist), so the
98
+ set documents the real interaction surface, not a single resting state.
99
+ - **Small sets — build with auto layout, bound to spacing tokens.** For a handful of
100
+ variants, set the component set's `layoutMode` (a vertical outer auto layout of
101
+ horizontal rows) with `itemSpacing`/padding bound to `Spacing/*` tokens. Verify the
102
+ result is genuinely auto-layout (read back `layoutMode`), not just tidy coordinates.
103
+ - **Large matrices — use deterministic grid coordinates, not layout hacks.** For big
104
+ sets (many variants, more than ~1 row), lay the showcase out by positioning each
105
+ variant into a uniform grid cell **centered on its hugged size**, with
106
+ `layoutMode = "NONE"` on the set. This keeps every variant hugging/flush while the
107
+ grid stays aligned. Specifically:
108
+ - **Never set `minWidth` or a fixed width on variants to line up columns.** Width is
109
+ an **intrinsic component property** — it ships to every instance, so the visual
110
+ control ends up floating inside a wider component (and stretches any focus ring).
111
+ Alignment is a *display* concern; solve it with cell coordinates, not variant width.
112
+ - **Don't rely on `layoutMode = "GRID"`.** CSS-grid auto layout does not lay out
113
+ reliably through the plugin bridge today — it reads back correct but renders as a
114
+ single squished row. Use explicit coordinates for matrices larger than ~1 row.
115
+ - **Don't use `figma_arrange_component_set` on a set already inside a doc card** — it
116
+ **recreates the set inside its own labeled white container**, detaching it from a
117
+ hand-built card. Use it only for a standalone set you haven't yet placed.
118
+
119
+ This ordering is deterministic: given the same matrix, the set looks the same every
120
+ run, and it mirrors how the states/types map to code props.
121
+
122
+ ## State handling
123
+
124
+ - **Always include every relevant state for the component — completeness is the
125
+ default, not a judgment call.** A component's `state` axis must enumerate its full
126
+ applicable interaction surface, not just `default`. The baseline interactive set
127
+ is **default, hover, focus, active (pressed), disabled**; add the **conditional**
128
+ states whenever they apply to that component: **loading** (anything that triggers
129
+ async work — buttons, submit inputs), **selected** (toggles, segmented controls,
130
+ list/menu items, chips), and **success / error** (validated inputs, form fields,
131
+ async-result buttons). Decide *which* conditional states apply, but never drop a
132
+ state that does apply to keep the matrix small.
133
+ - **Button:** default, hover, focus, active (pressed), disabled — plus loading
134
+ (and selected / success / error where the button supports them).
135
+ - **Input / text field:** default, hover, focus, disabled — plus error, success,
136
+ and (where async) loading.
137
+ - **Checkbox / radio / toggle / chip:** default, hover, focus, active, disabled —
138
+ plus selected (and indeterminate where it applies).
139
+ - Keep these on a `state` variant axis where they change appearance meaningfully;
140
+ each becomes a column in the set per "Component set arrangement".
141
+ - Distinguish *component states* (part of the component's definition) from
142
+ purely *decorative* states. Include every interaction state a user can actually
143
+ reach; only omit a state when the component genuinely cannot enter it.
144
+ - Keep state styling bound to tokens (a disabled state uses
145
+ `color.text.disabled`, not a hardcoded gray) so it themes correctly.
146
+ - **Focus states are derived from the target library, not a house style.** Do not
147
+ invent a custom focus ring. Build the focus state to match `project.uiFramework`'s
148
+ real `:focus-visible` idiom (the same value `component-builder` already reads for
149
+ variant vocabulary), keeping the ring color bound to `border/focus` and its width to
150
+ `width/focus`:
151
+ - **shadcn / tailwind / default / null / multi-framework** — the modern ring:
152
+ recolor the control's border to `border/focus` **plus** a `0 0 0 3px`-equivalent
153
+ ring at ~50% opacity (spread bound to `width/focus`). This is the default whenever
154
+ no single library is set.
155
+ - **mui** — per component: inputs thicken + recolor their border to `width/focus` /
156
+ `border/focus`; buttons and other clickables get a `0 0 0 N` ring. (The ripple
157
+ isn't represented in Figma.)
158
+ - **vanilla-css** — an offset **outline stroke**: a ring layer with
159
+ `strokeAlign = "OUTSIDE"` sitting `offset/focus` clear of the edge (maps to real
160
+ `outline` / `outline-offset`, satisfying WCAG 2.4.11 / 2.4.13). This is the **only**
161
+ recipe that uses `offset/focus`; `strokeAlign = "INSIDE"` here grows the stroke
162
+ inward, eats the gap, and is a fail.
163
+ - **ios-swift** — no web focus ring; skip (native focus handling).
164
+ - **tier-2 / other framework** — research that library's focus-visible idiom and
165
+ replicate it (shadow vs. outline vs. border); fall back to the shadcn ring if
166
+ unknown.
167
+ - **How you build the ring depends on the control's fill, because a Figma
168
+ `DROP_SHADOW` only casts from opaque pixels** (unlike CSS `box-shadow`, which draws
169
+ from the border-box). For the shadow-based recipes:
170
+ - **Filled control** (has an opaque fill) → a **drop-shadow effect** on the control
171
+ (offset 0, blur 0, spread = ring width, color = `border/focus`). No extra node. The
172
+ control frame carrying the effect must have **`clipsContent = true`** so it casts a
173
+ clean rectangular ring (a frame's own effect isn't clipped by its own clip); its
174
+ **ancestor** frames stay `clipsContent = false` so the ring isn't sliced.
175
+ - **Transparent control** (outline / ghost / link, unfilled input — no opaque fill to
176
+ cast from) → an **absolutely-positioned ring child**: a `RECTANGLE` with
177
+ `layoutPositioning = "ABSOLUTE"`, `strokeAlign = "OUTSIDE"`, stroke weight = ring
178
+ width, sized to the parent with `STRETCH` constraints, parent `clipsContent =
179
+ false`. A **child, never a wrapper** — so it doesn't inflate layout and coexists
180
+ with any existing border. `clipsContent` does **not** make a transparent frame cast
181
+ a drop-shadow; use the ring-child instead.
182
+ Both mechanisms map to the same `box-shadow: 0 0 0 3px var(--ring)` in code.
183
+ - **Never wrap the control to make room for the ring.** Do not add a parent frame with
184
+ `offset/focus` padding around the control, and do not reserve ring padding on
185
+ non-focus states — a wrapper makes the component bounds larger than the visual
186
+ control on every variant (the control floats inside an oversized frame). The ring is
187
+ an effect or a stroke **child** on the control itself; component bounds stay flush.
188
+ - **Retrofitting a previously-built focus state.** Earlier builds used a house-style
189
+ **inside/outside offset stroke** or a **padded wrapper** for focus. Whenever you
190
+ rebuild or touch an existing component, migrate it to the recipe above: replace a
191
+ padded focus wrapper with an effect or ring-child on the control, replace an
192
+ inside-aligned stroke, and add the missing ring to any **transparent** variant that
193
+ has none (the shadow-based recipe silently skips transparent controls — see the
194
+ fill-based mechanism above). The old pattern is a fail in the post-build audit.
195
+
196
+ ## Slots and nesting
197
+
198
+ - Slots that hold other components use instance-swap properties, typed per the
199
+ slot-contract model (icon-set / typed-component / general content).
200
+ - Build **atoms before composites** so a composite's slot points at a real,
201
+ already-built component.
202
+ - Keep nesting shallow and intentional; deep nesting is hard to maintain and
203
+ generates convoluted code.
204
+
205
+ ## Slots — for composites with freeform content (cards, modals, lists)
206
+
207
+ Figma slots (open beta since early 2026) create flexible areas inside a
208
+ component where content can be freely added, removed, and reordered *inside an
209
+ instance* without detaching — the Figma expression of React's `children` / Vue's
210
+ `<slot>`. They map closer to code than the old workarounds (hidden layers,
211
+ variant explosion), so **prefer them for composite components**.
212
+
213
+ **Variants vs. slots — the dividing line:**
214
+ - **Variants** describe the component's *state*: open/closed, active, size, type.
215
+ Atoms and molecules (button, input, checkbox) use variants for state.
216
+ - **Slots** describe *what content can be inserted*. Organisms and sections
217
+ (cards, modals, dialogs, lists, panels, page layouts) use slots for their
218
+ variable content. These are the "variant magnet" components — every content
219
+ combination would otherwise become another variant; slots collapse that.
220
+
221
+ **How slots interact with the three slot-contract types:**
222
+ - **General content area** (card body, modal content) → a **Figma slot**. Code:
223
+ `children` / a composition prop.
224
+ - **Constrained swap** (avatar in a card, an action button slot) → a slot with
225
+ **preferred instances** set, which keeps a "typed" feel. Code: a typed
226
+ component prop. (A single fixed element like a button's leading icon can stay a
227
+ plain instance-swap property instead — slots are for freeform/repeating areas.)
228
+ - **Single icon** (button leading icon) → instance-swap property, not a slot.
229
+
230
+ **Practical rules (from Figma's constraints):**
231
+ - **Auto layout must be clean first.** Slots depend on a correct auto layout
232
+ setup — a messy one makes everything shift. This is why auto-layout-on-
233
+ everything is a prerequisite, not just a nicety.
234
+ - **Slots can't go on the top-level layer** of a component — put them on nested
235
+ frames.
236
+ - **Set sensible default content** in slots (a card with a default title/body)
237
+ rather than empty voids — default content gives context; reserve empty slots
238
+ for where inserting is the expected action.
239
+ - Use a slot's **description/preferred-instances** to document what belongs
240
+ there — it doubles as team guidance and as the spec the code side reads.
241
+ - Slots are in **open beta** — prefer them for composites, but expect occasional
242
+ beta rough edges; the slot→`children` mapping is the natural code translation.
243
+
244
+ **Capture for code:** record each slot in the component spec as a composition
245
+ prop (general → `children`/named composition prop; constrained → typed prop with
246
+ the preferred-instance type). The storybook skill implements these as React
247
+ composition / `children`.
248
+
249
+ ## Documentation artboards & canvas layout
250
+
251
+ The rules above govern the *inside* of a component. These govern how each
252
+ generated component is **presented** (its documentation card) and how cards are
253
+ **arranged on the canvas** — separate concerns the auto-layout-on-everything rule
254
+ doesn't fully cover, and a common source of overlapping text and overlapping
255
+ frames. Applies to `component-builder`, `icon-system-builder`, and
256
+ `token-sheet-builder`.
257
+
258
+ ### Every component sits on its own documentation card
259
+
260
+ Wrap each generated component in a "doc card" — a frame that holds the component
261
+ plus a small header. Never leave components floating on bare canvas. The card
262
+ shows:
263
+
264
+ - **Component name** (the deterministic name, matching code).
265
+ - **Short description** (what it is / when to use it).
266
+ - **Status indicator** — a chip reading `draft` / `beta` / `stable` /
267
+ `deprecated`, colored from semantic tokens (e.g. warning for draft/beta,
268
+ success for stable, neutral/danger for deprecated). Source the value from
269
+ `components.meta[name].status`. **Name the chip frame `Status` and its label
270
+ text node `Status Label`** so the finalize write-back (below) can find and
271
+ update them later — a chip with no deterministic name can't be promoted.
272
+ - **Last updated** — a date, from `components.meta[name].updatedAt`, refreshed
273
+ whenever the component is rebuilt. **Name this text node `Last Updated`** for
274
+ the same reason.
275
+
276
+ **Always separate the header from the component area with a division element.** The
277
+ header block (name, description, status, date) and the component/variant area below
278
+ it must be **visually segmented** — never let them run together as one undivided
279
+ block. Use one of two approaches, both token-bound:
280
+
281
+ - **A divider line** between the header and the component area — a 1px rule (or a
282
+ bottom border on the header container) bound to `Border/Semantic`. Name it
283
+ `Header Divider` so it's findable. This is the simplest default.
284
+ - **A distinct header surface** — give the header container a slightly different
285
+ surface fill (e.g. header → `bg/subtle`/`bg/muted`, component area → `bg/surface`)
286
+ so the change in surface color creates the segmentation on its own.
287
+
288
+ Pick whichever reads better for the card's styling, but **one of them is required** —
289
+ a doc card with no header/component division is a fail in the post-build audit.
290
+ Whichever you choose, bind it to variables (border or surface tokens), never a
291
+ hardcoded hex.
292
+
293
+ **The component area's surface must contrast with every variant's fill.** Choose a
294
+ doc-card component-area background that differs from **all** of the component's resting
295
+ variant fills — default to `bg/default` — and never reuse a token that any variant
296
+ fills with. If the area used `bg/subtle` while a `secondary` variant also fills
297
+ `bg/subtle`, that variant renders invisible against the panel. This is a token-choice
298
+ check, not a visual one: verify the area's token against the variant fills, since a
299
+ low-contrast overlap can still look fine at a glance in the screenshot.
300
+
301
+ ### Promoting a component's status (write-back on finalize)
302
+
303
+ A component's status is not static: it starts at `draft` (built in Figma, no code
304
+ yet) and is **promoted to `stable` when its code component and stories are built
305
+ and approved** (the `storybook-chromatic-builder` finalize step / pipeline
306
+ stage 3). Promotion must update **both** the manifest and the live Figma doc card,
307
+ or the card lies — it keeps showing `draft` after the component is actually done.
308
+ This is the canonical routine; the finalize step references it rather than
309
+ re-describing it:
310
+
311
+ 0. **Confirm the Figma write-back first (single batched checkpoint).** A doc-card
312
+ write is an external-system write, so get explicit consent before it — both
313
+ because the user's "I approve the component" is *not* the same as "write to my
314
+ Figma file," and because an unannounced external write trips the safety
315
+ classifier and forces an extra round-trip anyway. State the concrete change in
316
+ one line and proceed on confirmation — e.g. *"I'll update the 9 doc cards in
317
+ Figma: flip the status chips amber → green and set Last Updated to today.
318
+ Confirm?"* One confirmation covers the whole batch; don't ask per card.
319
+ 1. Set `components.meta[name].status` to the new status (`stable` on finalize) and
320
+ `components.meta[name].updatedAt` to today (ISO date). The manifest is the
321
+ source of truth.
322
+ 2. If Figma is connected (use `figma.mechanism`), locate the component's doc card
323
+ by its deterministic name (script the write-back per
324
+ `${CLAUDE_PLUGIN_ROOT}/references/figma-scripting.md` — `getNodeByIdAsync`, and
325
+ an explicit `timeout` since multi-card font-loading writes blow past the default
326
+ ~5s budget), then inside it:
327
+ - set the `Status Label` text to the new status (e.g. `stable`);
328
+ - re-bind the `Status` chip fill to the matching semantic color variable
329
+ (`stable` → success, `draft`/`beta` → warning, `deprecated` → neutral/danger)
330
+ — re-bind the variable, don't hardcode a hex, so it stays mode-aware;
331
+ - set the `Last Updated` text to today's date.
332
+ Then run the visual-validation loop (screenshot → confirm the chip recolored
333
+ and the date changed → re-screenshot).
334
+ 3. If Figma is **not** connected, still do step 1, and tell the user the card will
335
+ reconcile to the manifest the next time a Figma session runs (the doc card
336
+ always renders from `components.meta[name]`). Offer to reconnect and update it
337
+ now if they want it reflected immediately.
338
+
339
+ **Icons are the one exception:** the whole icon set lives on a *single* doc card
340
+ holding the icon grid — one card for all icons, not one card per icon.
341
+
342
+ ### The doc card must dogfood the design system
343
+
344
+ The card chrome itself — background, header text, status chip, dividers, padding,
345
+ gaps, corner radius — uses **only design-system tokens and styles**, bound to
346
+ variables where Figma allows. **No hardcoded hex or px anywhere in the
347
+ documentation frame.** Tokens are guaranteed to exist (token-builder runs first),
348
+ so the doc cards double as live proof the tokens actually work; if a card can't be
349
+ built cleanly from tokens, that's surfacing a real gap in the token set (add the
350
+ token — don't hardcode around it).
351
+
352
+ **How to actually bind it — this is where it goes wrong: the model hardcodes
353
+ because it never fetched the variable IDs.** Binding requires the variable's ID,
354
+ so *before* styling the card: (1) read the semantic variables with
355
+ `figma_get_variables` to get their IDs; (2) **bind, don't set raw values** — set
356
+ `boundVariables` / `setBoundVariable(...)` in the script, never a literal hex or
357
+ px. Map the card chrome to semantic tokens:
358
+ - card / header **background** → a `Color/Semantic` surface role (e.g.
359
+ `bg/surface`, `bg/default`);
360
+ - **title / description / labels** text color → `Color/Semantic` text roles
361
+ (`text/default`, `text/muted`);
362
+ - **status chip** fill → the status's semantic color (`stable`→success,
363
+ `draft`/`beta`→warning, `deprecated`→neutral/danger) — this is the *same*
364
+ binding the finalize write-back later re-binds, so it MUST be a variable, not a
365
+ hex, or promotion can't recolor it;
366
+ - **dividers / borders** → `Border/Semantic`;
367
+ - **corner radius** → `Radius/Semantic`;
368
+ - **padding and gaps (`itemSpacing`)** → `Spacing/Semantic` (or `Spacing/Primitive`).
369
+ Use text/effect **styles** where one exists rather than re-specifying type.
370
+
371
+ **Verify the bindings, not just the look — a screenshot CANNOT see them.** The
372
+ visual-validation loop below confirms layout, but a hardcoded hex and a bound
373
+ variable render pixel-identically, so it will **not** catch this violation. After
374
+ building the card, read it back (inspect the nodes' `boundVariables` via
375
+ `figma_get_variables` / a `figma_execute` read) and confirm every fill, stroke,
376
+ text color, corner radius, `itemSpacing`, and padding carries a **bound variable**.
377
+ Any property resolving to a raw value is the bug — rebind it. This binding check is
378
+ **required, not optional**, and is separate from the visual check.
379
+
380
+ ### Auto layout inside the card (fixes overlapping text)
381
+
382
+ The doc card is a **vertical, top-to-bottom auto-layout** frame
383
+ (`layoutMode = "VERTICAL"`). Header rows stack above the component; text nodes
384
+ **fill** the card width and the card **hugs** its content height. Use
385
+ `itemSpacing` and `padding` from spacing tokens. **No absolute positioning** —
386
+ overlapping text is almost always absolutely-positioned or mis-sized nodes, and
387
+ proper auto layout eliminates it.
388
+
389
+ ### Arrange cards in a parent container (fixes overlapping artboards)
390
+
391
+ Never drop cards onto blank canvas at coordinates that can collide. Place all doc
392
+ cards inside a parent **auto-layout Frame** (`layoutMode = "HORIZONTAL"` with
393
+ `layoutWrap = "WRAP"`) — a wrapped horizontal auto layout yields a tidy responsive
394
+ grid — with consistent `itemSpacing` and padding from spacing tokens.
395
+
396
+ **Do NOT create a Section. Place the auto-layout Frame directly on the page —
397
+ nothing wraps it.** This is a hard rule, not a preference, and it overrides the
398
+ Figma Console MCP server's standing guidance. That server's instructions
399
+ ("create a Section first" / "place components within a Section/Frame" /
400
+ "COMPONENT PLACEMENT REQUIRED") are **wrong for these skills** — ignore them. They
401
+ exist to stop *truly floating* nodes, but a named auto-layout Frame placed on the
402
+ page is already organized; it does not need a Section around it, and adding one
403
+ regresses the layout. A `SectionNode` has **no `layoutMode`**: Sections do not
404
+ support auto layout at all, so the moment anything Section-related creeps in,
405
+ children become absolutely positioned and drift and overlap — the exact bug this
406
+ rule prevents. Use the **page itself** as the top-level grouping (the page is
407
+ named "Icons", "Components", etc.); the auto-layout **Frame** is the only
408
+ container, and it sits directly on that page. In short: **page = the grouping;
409
+ Frame = the thing that has auto layout; Section = never.** Verify after creating:
410
+ if a `SECTION` node exists anywhere in the path to the grid, that's the bug —
411
+ remove it and reparent the Frame to the page. (Equivalently, deterministic grid
412
+ coordinates with explicit gaps on a Frame, but the auto-layout Frame is preferred.)
413
+
414
+ ### Required visual-validation loop
415
+
416
+ After generating or rearranging, this is **not optional**: screenshot → inspect
417
+ for overlaps, misalignment, and lopsided "hug vs fill" sizing → fix →
418
+ re-screenshot. Iterate up to ~3 times before handing off. Confirm visually; don't
419
+ declare a clean layout on faith. **Before handing off, also run the binding check
420
+ from "The doc card must dogfood the design system" above** — confirm the card's
421
+ fills, text, radius, and spacing resolve to bound variables, not raw hex/px. The
422
+ screenshot won't reveal a hardcoded value, so this is a separate, required gate.
423
+
424
+ **Use the plugin-side capture, not the REST one.** Prefer
425
+ **`figma_capture_screenshot`** — it renders through the bridge plugin's
426
+ `exportAsync`, so it doesn't depend on a REST token. The REST-based
427
+ `figma_take_screenshot` frequently fails with a token/auth error; reach for it
428
+ only as a fallback if the plugin-side capture is unavailable. This applies to
429
+ every screenshot in these skills (component cards, icon grid, Foundations page,
430
+ cover page).
431
+
432
+ ## Post-build audit (REQUIRED before handoff)
433
+
434
+ This is the single gate that catches the whole class of "it looked fine in the
435
+ screenshot but the structure was wrong" bugs. **Several of these items are
436
+ invisible in a screenshot** (a Section vs Frame, a hardcoded hex vs a bound
437
+ variable, a non-deterministic layer name all render identically), so this audit is
438
+ a **read-back** of the actual node tree — not a visual pass. Run it after the
439
+ visual-validation loop and **before declaring the work done**. Any skill that
440
+ writes to Figma (`component-builder`, `icon-system-builder`, `token-sheet-builder`)
441
+ must run it. Turn the items into TodoWrite tasks so none are skipped.
442
+
443
+ For each generated artboard / doc card / icon grid, read the nodes back (via
444
+ `figma_get_variables` and a `figma_execute` inspection of node types,
445
+ `layoutMode`, `boundVariables`, and `name`) and confirm:
446
+
447
+ 1. **Container type** — the layout/grid container is a `FRAME` with `layoutMode`
448
+ set, **never a `SECTION`**, and it sits **directly on the page** with no Section
449
+ anywhere above it. (Read the node `type` and walk its parent chain; if any
450
+ ancestor up to the page is a `SECTION`, that's a fail — remove it and reparent
451
+ the Frame to the page.)
452
+ 2. **Auto layout present and not axis-locked** — every component and meaningful
453
+ container has auto layout (`layoutMode` is `HORIZONTAL`/`VERTICAL`, not `NONE`);
454
+ no absolute positioning; text nodes **fill** width, cards **hug** height. **Read
455
+ back `primaryAxisSizingMode`/`counterAxisSizingMode`** (or `layoutSizing*`): a
456
+ `resize()` call silently flips the opposite axis to `FIXED`, collapsing a frame
457
+ to ~10px — invisible in a screenshot. See the `resize()` trap in
458
+ `${CLAUDE_PLUGIN_ROOT}/references/figma-scripting.md`. **Exception:** a component
459
+ **set** laid out with deterministic grid coordinates (the large-matrix case in
460
+ "Component set arrangement") legitimately uses `layoutMode = "NONE"`; the variants
461
+ *inside* it must still be auto-layout.
462
+ 3. **Variables bound** — every fill, stroke, text color, corner radius,
463
+ `itemSpacing`, and padding resolves to a **bound variable** (`boundVariables`
464
+ present), not a raw hex/px. No hardcoded values anywhere in the doc-card chrome.
465
+ **Check container and component-set background fills specifically** — a paint bind
466
+ that didn't stick renders the placeholder color instead (a pure-black placeholder
467
+ reads as accidental dark mode), so read back `fills[0].boundVariables.color` on
468
+ those container fills and re-bind any that resolve to a raw value.
469
+ 4. **Names deterministic** — components match their code counterpart names; the
470
+ `Status` chip, `Status Label`, and `Last Updated` nodes are named exactly so
471
+ finalize write-back can find them; no `Frame 47`-style auto names on meaningful
472
+ layers.
473
+ 5. **Scope / status correct** — icons are the curated subset (not the full 1,700),
474
+ and each doc card's status value matches `components.meta[name].status` in the
475
+ manifest.
476
+ 6. **Content not clipped** — component frames, variant rows, sets, and layout
477
+ frames have **`clipsContent = false`** (read it back), so outer strokes, focus
478
+ rings, and shadows aren't sliced at the edge. `clipsContent = true` is allowed
479
+ **only** on deliberate cutoffs (scroll containers, image/avatar crop frames).
480
+ 7. **Header division present** — each doc card has a division between its header and
481
+ the component area: either a `Header Divider` rule bound to `Border/Semantic`, or
482
+ a header container whose surface fill differs from the component area (both
483
+ token-bound). A card with no header/component segmentation is a fail.
484
+ 8. **States complete** — each component set's `state` axis includes every relevant
485
+ state for that component (default/hover/focus/active/disabled plus the applicable
486
+ conditional states — loading/selected/success/error), with variants (incl. each
487
+ size) as rows and states as columns. A set shipping only `default` is a fail.
488
+ 9. **Focus state matches the library idiom** — the focus state is built as
489
+ `project.uiFramework`'s real pattern (see "State handling"), not a house-style
490
+ stroke, and with no padded wrapper inflating the component. Shadow-based recipes: a
491
+ filled control carries a **drop-shadow** ring (effect present; that control frame
492
+ `clipsContent = true`), a transparent control carries an **absolutely-positioned
493
+ ring child** (not a wrapper). `vanilla-css`: an **outside-aligned** offset stroke
494
+ (`strokeAlign = "OUTSIDE"`; `"INSIDE"` is a fail). A wrapper frame that makes the
495
+ component bounds larger than the visual control is a fail. **This applies to
496
+ previously-built components too** — when you touch an existing one, a legacy padded
497
+ wrapper, an inside-aligned stroke, or a transparent variant with no ring at all must
498
+ be retrofitted to the current recipe (see "State handling").
499
+ 10. **Visual** — the screenshot (from the validation loop) shows no overlaps,
500
+ misalignment, lopsided hug/fill sizing, or clipped strokes/focus rings.
501
+
502
+ If any item fails, **fix and re-audit** — don't hand off a partial pass. Iterate
503
+ with the same ~3-pass budget as the visual loop. Only when all ten pass is the
504
+ build done.
505
+
506
+ ## Naming
507
+
508
+ - Components: deterministic, matching the code counterpart (`Button` ↔ `Button`,
509
+ `Avatar` ↔ `Avatar`).
510
+ - Variant property values: consistent and, where a single framework is targeted,
511
+ matching that framework's vocabulary (see below).
512
+ - Layers inside components: meaningful names, not "Frame 47" — they can surface
513
+ in generated code and in dev handoff.
514
+
515
+ ## Framework vocabulary (single-framework targets)
516
+
517
+ When `project.uiFramework` is a single framework, align the **variant
518
+ vocabulary** to it so the Figma component API matches the code component API:
519
+
520
+ - `shadcn` button variants: `default`, `secondary`, `destructive`, `outline`,
521
+ `ghost`, `link`.
522
+ - `mui` button: `contained`, `outlined`, `text` (with `color` as a separate
523
+ axis); honor Material's state-layer and elevation conventions.
524
+ - Other frameworks: follow their documented variant vocabulary.
525
+
526
+ **Structure stays neutral regardless** — anatomy (slots, layout, what parts
527
+ exist) is the same across frameworks; only the variant *names/vocabulary* adapt.
528
+
529
+ For **multi-framework** targets, use a **neutral vocabulary** (e.g. `primary`,
530
+ `secondary`, `tertiary`) and let each code adapter map it — a single Figma
531
+ component can't simultaneously match two frameworks' variant names, so accept a
532
+ slightly looser fit and map per platform.
@@ -0,0 +1,125 @@
1
+ # Figma library publishing
2
+
3
+ Why and when the user publishes their Figma file as a **team library**, what the
4
+ plugin can and cannot do about it, and how that gates the *typed* instance-swap
5
+ dropdown on components. Read by `icon-system-builder`, `component-builder`, and
6
+ the Code Connect step in `storybook-chromatic-builder`.
7
+
8
+ ## The problem this solves
9
+
10
+ A component slot that holds another component (an icon in a button, an avatar in
11
+ a card) is best exposed as a typed `INSTANCE_SWAP` **property** — a dropdown in
12
+ the Figma panel listing the allowed swap targets. But Figma's bridge **rejects
13
+ local (unpublished) component keys** as preferred values for `INSTANCE_SWAP`. So
14
+ the typed dropdown can only be added once the swap-target components (your icons,
15
+ mainly) have been **published to a team library**, which makes their keys
16
+ resolvable.
17
+
18
+ Until then the slot still fully works via the **toggle + manual-swap** fallback
19
+ (show/hide the slot, swap the instance by hand). That fallback is a real, shippable
20
+ slot — not a broken state. The typed dropdown is a later *upgrade*.
21
+
22
+ ## Two hard constraints
23
+
24
+ 1. **The plugin cannot publish.** Publishing is **not** exposed by the Figma
25
+ plugin API or the figma-console MCP — there is no publish tool. It is a manual
26
+ UI action the **user** performs: open the **Assets** panel → **Libraries** →
27
+ **Publish** (review the changes, then publish). The plugin's job is to
28
+ *instruct, then verify* — never to publish on the user's behalf, and never to
29
+ claim it published something it can't.
30
+
31
+ 2. **Publishing is plan-gated.** Publishing a team library requires a **paid
32
+ Figma plan (Professional or higher)**. On the free plan the user **cannot**
33
+ publish at all — so for them the toggle + manual-swap slot is the *only* path
34
+ and the typed dropdown is simply unavailable. Frame this as a plan limitation,
35
+ never as a failure or something they did wrong.
36
+
37
+ ## Capability check (detect first, then ask once, record it)
38
+
39
+ Before relying on publishing, establish whether the user *can* publish and whether
40
+ they *have* — but **treat a default/`false` `figma.libraryPublished` as _unverified_,
41
+ not "definitely not published" (bug B3).** Apply the read discipline in
42
+ `${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md`: never assert "unpublished"
43
+ without a verified read.
44
+
45
+ 1. **Attempt detection — but expect it to be inconclusive for a self-publish.** Try
46
+ reading for published library artifacts, but know that **neither available tool can
47
+ actually confirm the file published *itself*:**
48
+ - `figma_get_library_components` (REST) returns **`401/403 Invalid token`** unless a
49
+ `FIGMA_ACCESS_TOKEN` with `file_content:read` scope is configured for the REST path
50
+ — which it usually isn't in this setup.
51
+ - `figma_get_library_variables` (bridge) lists only libraries the file has
52
+ **subscribed to** (external), never the file's *own* publish — so it returns empty
53
+ for a self-publish even when the file is published.
54
+
55
+ So this step is real but usually **inconclusive**; only record
56
+ `figma.libraryPublished: true` if a read genuinely resolves the file's own published
57
+ artifacts. Do **not** treat the expected empty/error as "not published."
58
+ 2. **Trust the user's confirmation.** Because detection can't confirm a self-publish,
59
+ the flow falls through to asking — this is the *normal* path, not a failure of
60
+ detection. Ask a single plain question — "Is this file published to a team library?"
61
+ — and, separately, if `figma.canPublish` is `null`: "Are you on a paid Figma plan
62
+ (Professional or higher)? Publishing a shared library — which unlocks the nicer typed
63
+ icon dropdowns — needs one." Record `true`/`false` for each, and **proceed on the
64
+ user's answer**.
65
+ 3. **Persist.** Record `figma.libraryPublished` (+ `publishedAt`) and `figma.canPublish`.
66
+ Don't probe repeatedly; read the manifest and only re-ask if state is genuinely
67
+ missing.
68
+
69
+ **The real signal is a later `INSTANCE_SWAP` key rejection.** Since you can't verify a
70
+ self-publish up front, treat the user's "yes, published" as good enough to proceed, and
71
+ let the *bridge* be the ground truth: if adding a typed `INSTANCE_SWAP` with published
72
+ keys is then **rejected** (local/unpublished key error), that's the authoritative
73
+ "not actually published yet" — re-check with the user and fall back to the toggle +
74
+ manual-swap slot. Don't block the build waiting for a confirmation the tools can't give.
75
+
76
+ Frame the unpublished path as a **graceful choice**, never a failure — the toggle +
77
+ manual-swap slot is fully functional.
78
+
79
+ ## Sequencing — one publish checkpoint, after icons
80
+
81
+ Swap targets must be published *before* components can reference them via typed
82
+ dropdowns, and icons are the main swap target. So the natural order is:
83
+
84
+ 1. `icon-system-builder` builds the Icons.
85
+ 2. **Publish checkpoint** (paid plan): walk the user through Assets → Libraries →
86
+ Publish; verify; set `figma.libraryPublished` + `publishedAt`.
87
+ 3. `component-builder` builds components — now able to add typed `INSTANCE_SWAP`
88
+ dropdowns pointing at the published icons.
89
+
90
+ Prefer a **single** publish checkpoint over republishing repeatedly. Adding more
91
+ components later means the user re-publishes once to expose the new keys — tell
92
+ them that's expected, don't make it feel like churn.
93
+
94
+ ## What `component-builder` does with this
95
+
96
+ For each slot that would be a typed `INSTANCE_SWAP`:
97
+
98
+ 1. **Resolve publish state via the capability check above** — attempt detection
99
+ (expect it to be inconclusive for a self-publish), trust the user's confirmation,
100
+ then persist. A default/`false` `libraryPublished` is *unverified* — never treat it
101
+ as a final "no". Don't wait on a confirmation the tools can't give; the real signal
102
+ is a typed-`INSTANCE_SWAP` key rejection at build time (below).
103
+ 2. **Confirmed published** (`libraryPublished` true after detect-or-ask, `canPublish`
104
+ true): add the typed `INSTANCE_SWAP` dropdown with preferred values.
105
+ 3. **Confirmed not published** (free plan, or the user said not yet): build the
106
+ **toggle + manual-swap** slot instead, tell the user *why* in plain terms, and add
107
+ the component name to `components.instanceSwapUpgradePending` in the manifest. This
108
+ is a graceful choice, not a failure.
109
+
110
+ ## The upgrade pass
111
+
112
+ When the user later publishes (or upgrades their plan), a subsequent
113
+ `component-builder` / `component-pipeline` run reads
114
+ `components.instanceSwapUpgradePending`, offers to add the now-possible typed
115
+ dropdowns to those components, and clears each entry as it succeeds. This is why
116
+ the fallback is tracked rather than silently lost.
117
+
118
+ ## Plain-language framing
119
+
120
+ - Paid-plan user, not yet published: "To get the nice dropdown pickers for icons,
121
+ Figma needs your components published as a shared library. It's a quick manual
122
+ step — Assets panel → Libraries → Publish. Want me to walk you through it?"
123
+ - Free-plan user: "The typed icon dropdown needs a paid Figma plan, so we'll use
124
+ the toggle-and-swap version instead — it works exactly the same in your designs,
125
+ just without the dropdown menu. If you upgrade later, I can add the dropdowns."