@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,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."
|