Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
@@ -0,0 +1,260 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Code Connect Setup Reference
This reference covers all Code Connect tooling available to the figma-generate-library agent: the `add_code_connect_map` tool, `get_code_connect_map` for verification, `send_code_connect_mappings` for bulk application, variable code syntax, framework labels, and the decision of when to map per-component vs. in a final pass.
---
## 1. What Code Connect Does
Code Connect links a Figma component node to its code implementation so that:
- **Dev Mode** shows a real code snippet (from your codebase) instead of an auto-generated approximation when a developer inspects a component.
- **MCP `get_design_context`** returns `componentName`, `source`, and a rendered snippet alongside design tokens, enabling accurate AI-assisted code generation.
- **`search_design_system`** can return code references alongside Figma component metadata.
---
## 2. The Three MCP Tools
### 2a. add_code_connect_map — single mapping
Maps one Figma node to one code component.
**Parameters:**
| Parameter | Type | Required | Notes |
|-----------|------|----------|-------|
| `nodeId` | string | Yes (remote) / Optional (desktop) | Format `123:456`. Must be a published component or component set. |
| `fileKey` | string | Yes (remote) | The Figma file key. |
| `source` | string | Yes | Path in the codebase (e.g. `src/components/Button.tsx`) or a URL. |
| `componentName` | string | Yes | The code component name (e.g. `Button`). |
| `label` | enum | Yes | Framework label — see Section 4 for valid values. |
| `template` | string | Optional | Executable JS template code. Providing this creates a **figmadoc** (template) mapping instead of a simple **component_browser** mapping. Requires the `pixie_mcp_enable_writing_code_connect_templates` feature flag. |
| `templateDataJson` | string | Optional | JSON string with optional fields: `isParserless`, `imports`, `nestable`, `props`. |
**Two mapping tiers:**
1. **Simple mapping (component_browser):** Only `source`, `componentName`, and `label` provided. Associates the Figma component with a code path + name. Dev Mode generates a basic JSX snippet from Figma prop names. This is the default — use it first.
2. **Template mapping (figmadoc):** `template` is also provided. The template is executed in a sandboxed QuickJS environment and dynamically renders the snippet based on the actual instance's property values. Use this when precise prop-level Code Connect is required by the user.
**Common error codes:**
| Error | Meaning | Fix |
|-------|---------|-----|
| `CODE_CONNECT_MAPPING_ALREADY_EXISTS` | Component is already mapped | Disconnect existing mapping in Figma UI first |
| `CODE_CONNECT_ASSET_NOT_FOUND` | Published component not found | Ensure the component is published to the library |
| `CODE_CONNECT_INSUFFICIENT_PERMISSIONS` | No edit access | Request edit permission on the file |
| `CODE_CONNECT_NO_LIBRARY_FOUND` | File is not published as a library | Publish the file as a Figma library first |
**Usage example:**
```
Tool: add_code_connect_map
Args: {
nodeId: "123:456",
fileKey: "abc123",
source: "src/components/Button.tsx",
componentName: "Button",
label: "React"
}
```
---
### 2b. get_code_connect_map — verification
Retrieves the current Code Connect mapping for a node. Use this immediately after `add_code_connect_map` to confirm the mapping was saved, and before `send_code_connect_mappings` to audit existing state.
**Parameters:**
| Parameter | Type | Required | Notes |
|-----------|------|----------|-------|
| `nodeId` | string | Optional | The node to check. Omit to get all mappings in the file. |
| `fileKey` | string | Yes (remote) | The Figma file key. |
| `codeConnectLabel` | string | Optional | Filter results to a specific framework label. |
**Returns:** A map of `nodeId -> { componentName, source, label, snippet, snippetImports }`.
**How to verify:**
```
1. Call add_code_connect_map with the node.
2. Immediately call get_code_connect_map(nodeId, fileKey).
3. Confirm the returned object has the expected componentName and source.
4. If the mapping is missing, check for error codes from step 1.
```
---
### 2c. send_code_connect_mappings — bulk application
Applies multiple Code Connect mappings in one call. Use after `get_code_connect_suggestions` returns a batch of unmapped components, or when doing a final-pass bulk mapping at the end of Phase 4.
**Parameters:**
| Parameter | Type | Required | Notes |
|-----------|------|----------|-------|
| `nodeId` | string | Optional | Context node for design fallback if mappings array is empty. |
| `fileKey` | string | Yes (remote) | The Figma file key. |
| `mappings` | array | Yes | Array of mapping objects. |
**Each mapping object:**
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| `nodeId` | string | Yes | The Figma node identifier. |
| `componentName` | string | Yes | Code component name. |
| `source` | string | Yes | Path in the codebase. |
| `label` | enum | Yes | Framework label. |
| `template` | string | Optional | JS template code for figmadoc mapping. |
| `templateDataJson` | string | Optional | JSON template metadata. |
**Behavior:**
- All mappings are processed in parallel via POSTs to the backend.
- If any mapping fails, errors are reported per mapping — the rest succeed.
- On full success, `get_design_context` is called for the nodes and fresh design context is returned.
**Bulk workflow:**
```
1. Collect all {nodeId, componentName, source, label} pairs.
2. Call send_code_connect_mappings({ fileKey, mappings: [...all pairs...] }).
3. Review reported errors and call add_code_connect_map individually for any failures.
4. Call get_code_connect_map on a sample of nodes to spot-check.
```
---
## 3. Variable Code Syntax (Token Round-Tripping)
Setting code syntax on variables creates the bidirectional link between Figma tokens and the codebase token system. This is what enables Dev Mode to show `var(--color-bg-primary)` next to a design value instead of a raw hex.
**The three platforms:**
```javascript
// In use_figma:
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
variable.setVariableCodeSyntax('ANDROID', 'Theme.colorBgPrimary');
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
```
- `WEB` — used for CSS custom properties, design token JSON, and any web framework.
- `ANDROID` — used for Jetpack Compose theme references and Android resource names.
- `iOS` — used for SwiftUI Color extensions and UIKit color methods.
**Derivation rules (in priority order):**
1. **Best:** Use the exact token name from the codebase. Search the codebase for CSS custom properties (`--`), Swift color extensions, or Kotlin theme references and use those exact strings.
2. **Good:** Derive from the Figma variable name with a consistent transformation: replace `/` and spaces with `-`, prefix with `var(--` and suffix with `)`.
- Example: `color/bg/primary` → `var(--color-bg-primary)`
3. **Avoid:** Guessing or inventing names that don't exist in the codebase.
**Consistency rule:** The transformation must be uniform. If you use `var(--color-bg-primary)` for one variable, use the same `var(--{path-with-hyphens})` pattern for all variables in that collection.
**WEB syntax bulk example:**
```javascript
// In use_figma — set WEB code syntax on all variables in a collection
const collections = await figma.variables.getLocalVariableCollectionsAsync();
for (const coll of collections) {
if (coll.name !== 'Color') continue;
for (const varId of coll.variableIds) {
const v = await figma.variables.getVariableByIdAsync(varId);
if (!v) continue;
// Derive: "color/bg/primary" → "var(--color-bg-primary)"
const cssName = 'var(--' + v.name.toLowerCase().replace(/\//g, '-').replace(/\s+/g, '-') + ')';
v.setVariableCodeSyntax('WEB', cssName);
}
}
```
---
## 4. Framework Labels
The following labels are valid for all Code Connect MCP operations. Use the label that matches your codebase framework.
| Label | Use for |
|-------|---------|
| `React` | React / JSX / TSX components |
| `Web Components` | Native Web Components, Lit, FAST |
| `Vue` | Vue 2 and Vue 3 SFCs |
| `Svelte` | Svelte components |
| `Storybook` | Storybook stories with Code Connect integration |
| `Javascript` | Plain JavaScript, framework-agnostic |
| `Swift` | Swift / UIKit |
| `Swift UIKit` | UIKit specifically |
| `Objective-C UIKit` | Objective-C with UIKit |
| `SwiftUI` | SwiftUI view components |
| `Compose` | Jetpack Compose (Android) |
| `Java` | Java Android components |
| `Kotlin` | Kotlin Android (non-Compose) |
| `Android XML Layout` | Android XML layout files |
| `Flutter` | Flutter / Dart widgets |
| `Markdown` | Documentation or MDX components |
**HTML note:** The label `HTML` is used by the Code Connect CLI's HTML parser (for Angular, Vue, and Web Components without a framework-specific parser), but the MCP tools use `Web Components` or `Vue` directly. Check the codebase framework before selecting.
---
## 5. Per-Component vs. Final-Pass Strategy
### Per-component (preferred for new builds)
Map Code Connect immediately after creating a component, while the context is fresh (Phase 3, step 3h in the SKILL.md workflow):
**Advantages:**
- The node ID is already in hand from the creation script.
- You know exactly which code component this Figma component corresponds to (you just designed it to match).
- Errors surface early, before building dependent components.
**When to use:** Any time you create a Figma component that has a clear 1:1 match with an existing code component.
### Final pass (for bulk mapping at Phase 4)
Collect all unmapped components and map them in one `send_code_connect_mappings` call:
**Advantages:**
- One bulk call instead of N individual calls.
- Can use `get_code_connect_suggestions` to discover unmapped components automatically.
- Better for importing existing Figma files where you didn't control creation.
**When to use:** Retrofitting Code Connect onto an existing file, or when the codebase mapping requires research that is better done after all components are created.
### Hybrid (recommended for large systems)
- Map atoms (Button, Input, Badge, Avatar) **per-component** during Phase 3.
- Map molecules and organisms in a **final pass** during Phase 4 after all atoms are mapped, since molecule snippets reference atom Code Connect IDs.
---
## 6. Verification in Dev Mode
After mapping:
1. Open the Figma file in the browser or desktop app.
2. Switch to Dev Mode (the `</>` icon in the toolbar).
3. Select a component instance (not the main component — an instance placed on a page).
4. In the Inspect panel, the code snippet should show the Code Connect output instead of auto-generated code.
5. If the snippet is missing or shows `[auto-generated]`, run `get_code_connect_map` via MCP to confirm the mapping exists, then check that the component is published.
**Via MCP (faster during agent workflows):**
```
get_code_connect_map(nodeId: "<the component set node ID>", fileKey: "<file key>")
```
The response should include `componentName`, `source`, `label`, and a non-empty `snippet`.
---
## 7. Important Constraints
- **Published components only:** `add_code_connect_map` requires the component to be published to a library. If the file is not yet published, the mapping will fail with `CODE_CONNECT_NO_LIBRARY_FOUND`.
- **One mapping per label per node:** A node can have multiple mappings (one per framework label), but only one per label. Attempting to add a second React mapping to the same node returns `CODE_CONNECT_MAPPING_ALREADY_EXISTS`.
- **Template mappings are gated:** The `template` parameter requires the `pixie_mcp_enable_writing_code_connect_templates` feature flag. Use simple mappings unless the user explicitly requests template-level Code Connect.
- **Start simple, escalate:** Always begin with simple mappings (`source` + `componentName` + `label`). Add `template` only if the user needs precise prop-level snippet rendering.
@@ -0,0 +1,972 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Component Creation Reference
Complete guide for Phase 3: building components with variant matrices, variable bindings, component properties, and documentation.
---
## 1. Component Architecture
### Dependency Ordering: Atoms Before Molecules
Always build in dependency order. A molecule that contains an atom instance cannot exist until the atom is published. Suggested ordering:
```
Tier 0 (atoms): Icon, Avatar, Badge, Spinner
Tier 1 (molecules): Button, Checkbox, Toggle, Input, Select
Tier 2 (organisms): Card, Dialog, Menu, Navigation, Form
```
If a component embeds an instance of another component, the embedded component must be created first. Build your dependency graph during Phase 0 and encode the creation order in the plan.
### Building Blocks Sub-Components (M3 Pattern)
For complex components with independent sub-element state machines, extract the sub-element into its own component set prefixed with `Building Blocks/` (public) or `.Building Blocks/` (hidden from assets panel). The dot-prefix is a Figma convention for suppressing a component from the public assets panel.
**When to use Building Blocks:**
- The sub-element has its own variant axes (state, selection) that would cause combinatorial explosion in the parent
- The sub-element repeats (nav items, table cells, calendar cells, segmented button segments)
- The sub-element has different variant axes than the parent
**Example (M3 Segmented Button):**
```
Building Blocks/Segmented button/Button segment (start) [27 variants: Config × State × Selected]
Building Blocks/Segmented button/Button segment (middle) [27 variants]
Building Blocks/Segmented button/Button segment (end) [27 variants]
Segmented button [16 variants: Segments=2-5 × Density=0/-1/-2/-3]
Each variant contains instances of the appropriate Building Block segment components.
```
The parent manages composition and configuration; the Building Block manages its own interaction states.
### Private Components (`__` Prefix)
Use the `__` prefix for internal helper components that should not appear in the team library (Shop Minis pattern). Use `_` for documentation-only components (UI3 pattern).
```
__asset // private icon/asset holder
_Label/Direction // documentation annotation helper
```
---
## 2. Creating the Component Page
Each component lives on its own dedicated page (one page per component is the default). The page contains: a documentation frame at top-left and the component set positioned to its right or below.
```javascript
// Create or find the component page
let page = figma.root.children.find(p => p.name === 'Button');
if (!page) {
page = figma.createPage();
page.name = 'Button';
}
await figma.setCurrentPageAsync(page);
// Documentation frame — positioned at (40, 40)
const docFrame = figma.createFrame();
docFrame.name = 'Button / Documentation';
docFrame.x = 40;
docFrame.y = 40;
docFrame.resize(600, 400);
docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
docFrame.layoutMode = 'VERTICAL';
docFrame.primaryAxisSizingMode = 'AUTO';
docFrame.counterAxisSizingMode = 'FIXED';
docFrame.paddingTop = 40;
docFrame.paddingBottom = 40;
docFrame.paddingLeft = 40;
docFrame.paddingRight = 40;
docFrame.itemSpacing = 16;
// Title text node
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.fontSize = 32;
title.characters = 'Button';
docFrame.appendChild(title);
// Description text node
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const desc = figma.createText();
desc.fontName = { family: 'Inter', style: 'Regular' };
desc.fontSize = 14;
desc.characters = 'Buttons allow users to take actions and make choices with a single tap.';
docFrame.appendChild(desc);
// Tag docFrame with sharedPluginData for idempotency
docFrame.setSharedPluginData('dsb', 'run_id', RUN_ID);
docFrame.setSharedPluginData('dsb', 'key', 'doc/button');
return { docFrameId: docFrame.id, pageId: page.id };
```
---
## 3. Base Component: Auto-Layout, Child Nodes, Variable Bindings
The base component is the template from which all variants are cloned. It must have:
1. Auto-layout (not manual positioning)
2. All child nodes present
3. ALL visual properties bound to variables (no hardcoded values)
### Complete Button Base Component Example
```javascript
const RUN_ID = 'ds-build-2024-001'; // replace with your actual run ID
await figma.setCurrentPageAsync(
figma.root.children.find(p => p.name === 'Button')
);
// Rehydrate variables from IDs stored in state ledger
const bgVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary');
const textVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary');
const paddingVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md');
const radiusVar = await figma.variables.getVariableByIdAsync('VAR_ID_radius_md');
const gapVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm');
// --- Base component frame ---
const comp = figma.createComponent();
comp.name = 'Size=Medium, Style=Primary, State=Default';
comp.layoutMode = 'HORIZONTAL';
comp.primaryAxisSizingMode = 'AUTO';
comp.counterAxisSizingMode = 'AUTO';
comp.counterAxisAlignItems = 'CENTER';
comp.primaryAxisAlignItems = 'CENTER';
// Padding — bound to spacing variables
comp.setBoundVariable('paddingTop', paddingVar);
comp.setBoundVariable('paddingBottom', paddingVar);
comp.setBoundVariable('paddingLeft', paddingVar);
comp.setBoundVariable('paddingRight', paddingVar);
comp.setBoundVariable('itemSpacing', gapVar);
// Corner radius — bound to radius variable
comp.setBoundVariable('topLeftRadius', radiusVar);
comp.setBoundVariable('topRightRadius', radiusVar);
comp.setBoundVariable('bottomLeftRadius', radiusVar);
comp.setBoundVariable('bottomRightRadius', radiusVar);
// Background fill — bound to color variable
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } },
'color',
bgVar
);
comp.fills = [bgPaint];
// --- Label text node ---
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
const label = figma.createText();
label.name = 'label';
label.fontName = { family: 'Inter', style: 'Medium' };
label.fontSize = 14;
label.characters = 'Button';
label.layoutSizingHorizontal = 'HUG';
label.layoutSizingVertical = 'HUG';
// Text fill — bound to color variable
const textPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } },
'color',
textVar
);
label.fills = [textPaint];
comp.appendChild(label);
// --- Icon placeholder (Rectangle for now — will be INSTANCE_SWAP) ---
const iconBox = figma.createFrame();
iconBox.name = 'icon';
iconBox.resize(16, 16);
iconBox.fills = [];
iconBox.layoutSizingHorizontal = 'FIXED';
iconBox.layoutSizingVertical = 'FIXED';
comp.appendChild(iconBox);
// Tag for idempotency
comp.setSharedPluginData('dsb', 'run_id', RUN_ID);
comp.setSharedPluginData('dsb', 'phase', 'phase3');
comp.setSharedPluginData('dsb', 'key', 'component/button/base');
return { baseCompId: comp.id };
```
**ALL of these must be variable-bound (never hardcoded):**
| Property | Variable type | API method |
|---|---|---|
| Fill color | COLOR | `setBoundVariableForPaint(..., 'color', var)` |
| Stroke color | COLOR | `setBoundVariableForPaint(..., 'color', var)` |
| Text fill | COLOR | `setBoundVariableForPaint(..., 'color', var)` |
| Padding (all 4 sides) | FLOAT | `comp.setBoundVariable('paddingTop', var)` |
| Gap / itemSpacing | FLOAT | `comp.setBoundVariable('itemSpacing', var)` |
| Corner radius (all 4) | FLOAT | `comp.setBoundVariable('topLeftRadius', var)` etc. |
| Stroke weight | FLOAT | `comp.setBoundVariable('strokeWeight', var)` |
---
## 4. Variant Matrix
### Defining Axes
For each component, identify its variant axes before writing any code. Standard axes:
```
Button:
Size → [Small, Medium, Large]
Style → [Primary, Secondary, Outline, Ghost]
State → [Default, Hover, Focused, Pressed, Disabled]
Total = 3 × 4 × 5 = 60 combinations — exceeds 30 limit → split by Style
```
### The 30-Combination Cap and Split Strategy
When the product of all variant axes exceeds 30 combinations, split the matrix. Options:
1. **Split by a primary axis**: Create separate component sets, one per Style (Primary Button, Secondary Button, etc.)
2. **Use INSTANCE_SWAP**: Remove a visual axis (like Icon) from the variant matrix entirely and expose it as an INSTANCE_SWAP property instead
3. **Use Building Blocks**: Extract sub-elements with their own state axes into Building Block component sets
For Button with Size × State = 15 combinations, add Style as a variant axis only if Style ≤ 2 options (15 × 2 = 30). For more Styles, split.
### Creating All Variants with use_figma
Build each variant by cloning the base component and adjusting the variable bindings that differ per variant. Pass in the base component ID from the previous call's state.
```javascript
const RUN_ID = 'ds-build-2024-001';
const BASE_COMP_ID = 'BASE_ID_FROM_STATE'; // from state ledger
await figma.setCurrentPageAsync(
figma.root.children.find(p => p.name === 'Button')
);
const base = await figma.getNodeByIdAsync(BASE_COMP_ID);
// Variable IDs from state ledger
const vars = {
// Primary style
bg_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary'),
text_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary'),
// Secondary style
bg_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_secondary'),
text_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_secondary'),
// Disabled
bg_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_disabled'),
text_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_disabled'),
// Sizes
padding_sm: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm'),
padding_md: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md'),
padding_lg: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_lg'),
};
const axes = {
Size: ['Small', 'Medium', 'Large'],
Style: ['Primary', 'Secondary'],
State: ['Default', 'Hover', 'Disabled'],
};
const paddingBySize = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg };
const components = [];
for (const size of axes.Size) {
for (const style of axes.Style) {
for (const state of axes.State) {
const clone = base.clone();
clone.name = `Size=${size}, Style=${style}, State=${state}`;
// Bind padding by size
clone.setBoundVariable('paddingTop', paddingBySize[size]);
clone.setBoundVariable('paddingBottom', paddingBySize[size]);
clone.setBoundVariable('paddingLeft', paddingBySize[size]);
clone.setBoundVariable('paddingRight', paddingBySize[size]);
// Bind fill by style + state
const isDisabled = state === 'Disabled';
const bgVar = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary);
const txtVar = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary);
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgVar
);
clone.fills = [bgPaint];
const labelNode = clone.findOne(n => n.name === 'label');
const textPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txtVar
);
labelNode.fills = [textPaint];
clone.setSharedPluginData('dsb', 'run_id', RUN_ID);
clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`);
components.push(clone);
}
}
}
return { variantIds: components.map(c => c.id) };
```
---
## 5. `combineAsVariants` + Grid Layout
After all variant components exist, combine them into a ComponentSet and position them in a grid. This MUST be a separate `use_figma` call — you must pass in all variant IDs from the previous call's return value.
### Grid Design Conventions
Professional design systems lay out variants in a readable grid where:
- **Columns** = the property users interact with most (typically **State**: Default, Hover, Focused, Pressed, Disabled)
- **Rows** = structural axes grouped together (typically **Size × Style**, where Size varies fastest)
- **Gap** = 16–40px between variants (20px is a safe default; match existing file if one exists)
- **Padding** = 40px around the grid inside the ComponentSet frame
```
Visual structure:
Default Hover Focused Pressed Disabled
┌──────────────────────────────────────────────────────────────────┐
│ Small/Primary [comp] [comp] [comp] [comp] [comp] │
│ Small/Secondary [comp] [comp] [comp] [comp] [comp] │
│ Medium/Primary [comp] [comp] [comp] [comp] [comp] │
│ Medium/Secondary[comp] [comp] [comp] [comp] [comp] │
│ Large/Primary [comp] [comp] [comp] [comp] [comp] │
│ Large/Secondary [comp] [comp] [comp] [comp] [comp] │
└──────────────────────────────────────────────────────────────────┘
```
**Why State on columns?** State is the axis designers scan horizontally to verify interaction consistency. Size/Style define the "identity" of each row. This matches how professional design systems (M3, Polaris, Simple DS) organize their grids.
### Adding Row/Column Header Labels
After laying out the grid, add text labels OUTSIDE the ComponentSet to help navigation. These are siblings of the ComponentSet on the page — not children of it:
```javascript
// Add column headers above the component set
const colLabels = ['Default', 'Hover', 'Focused', 'Pressed', 'Disabled'];
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
for (let i = 0; i < colLabels.length; i++) {
const label = figma.createText();
label.fontName = { family: 'Inter', style: 'Medium' };
label.characters = colLabels[i];
label.fontSize = 11;
label.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
label.x = cs.x + padding + i * (childWidth + gap);
label.y = cs.y - 20;
}
// Add row headers to the left of the component set
const rowLabels = ['Small / Primary', 'Small / Secondary', 'Med / Primary', ...];
for (let i = 0; i < rowLabels.length; i++) {
const label = figma.createText();
label.fontName = { family: 'Inter', style: 'Medium' };
label.characters = rowLabels[i];
label.fontSize = 11;
label.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
label.x = cs.x - 120;
label.y = cs.y + padding + i * (childHeight + gap) + childHeight / 2 - 6;
}
```
**Note:** These labels are documentation aids, not part of the component itself. They help designers navigate the variant grid.
### Grid layout code
```javascript
const VARIANT_IDS = ['ID1', 'ID2', '...']; // from state ledger
const PAGE_ID = 'PAGE_ID'; // from state ledger
await figma.setCurrentPageAsync(await figma.getNodeByIdAsync(PAGE_ID));
// Collect component nodes
const components = await Promise.all(
VARIANT_IDS.map(id => figma.getNodeByIdAsync(id))
);
// Combine as variants
const cs = figma.combineAsVariants(components, figma.currentPage);
cs.name = 'Button';
// Grid layout: position each variant based on its property values
// Determine column axis (State) and row axes (Size × Style)
const axes = {
Size: ['Small', 'Medium', 'Large'],
Style: ['Primary', 'Secondary'],
State: ['Default', 'Hover', 'Disabled'],
};
const COL_AXIS = 'State'; // columns
const ROW_AXES = ['Size', 'Style']; // rows (Size changes fastest)
const gap = 16;
const padding = 40;
// Measure child dimensions (all should be same height within Size tier)
// Use the first child as reference for column width
const childWidth = 120; // approximate; refine after first screenshot
const childHeight = 40;
cs.children.forEach(child => {
const props = {};
child.name.split(', ').forEach(part => {
const [k, v] = part.split('=');
props[k] = v;
});
const colIdx = axes[COL_AXIS].indexOf(props[COL_AXIS]);
// Row = Size index * number of styles + Style index
const rowIdx = axes.Size.indexOf(props.Size) * axes.Style.length
+ axes.Style.indexOf(props.Style);
child.x = padding + colIdx * (childWidth + gap);
child.y = padding + rowIdx * (childHeight + gap);
});
// Resize component set to fit all children + padding
let maxX = 0, maxY = 0;
for (const child of cs.children) {
maxX = Math.max(maxX, child.x + child.width);
maxY = Math.max(maxY, child.y + child.height);
}
cs.resizeWithoutConstraints(maxX + padding, maxY + padding);
// Style the component set frame
cs.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.98 } }];
cs.cornerRadius = 8;
// Position component set on page (to the right of doc frame)
cs.x = 680;
cs.y = 40;
cs.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
cs.setSharedPluginData('dsb', 'key', 'componentset/button');
return { componentSetId: cs.id };
```
**Critical rules for combineAsVariants:**
- `components` must be a non-empty array containing ONLY `ComponentNode` objects (not frames, not groups)
- After combining, children are placed at (0,0) and overlap — you MUST manually position them
- `resizeWithoutConstraints` is required after positioning to make the component set frame fit its contents
- There is no `figma.createComponentSet()` — you cannot create an empty component set
---
## 6. Component Properties
Add TEXT, BOOLEAN, and INSTANCE_SWAP properties to the ComponentSet (not to individual variants). The return value of `addComponentProperty` is the actual property key (it gets a `#id:id` suffix appended) — save this key and use it immediately when setting `componentPropertyReferences`.
### TEXT Properties
Expose editable text in instances:
```javascript
// On the ComponentSetNode (cs):
const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button');
// labelKey is now something like "Label#0:1"
// Wire to the label child in each variant:
for (const child of cs.children) {
const labelNode = child.findOne(n => n.name === 'label');
if (labelNode) {
labelNode.componentPropertyReferences = { characters: labelKey };
}
}
```
### BOOLEAN Properties
Toggle child node visibility:
```javascript
const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true);
for (const child of cs.children) {
const iconNode = child.findOne(n => n.name === 'icon');
if (iconNode) {
iconNode.componentPropertyReferences = { visible: showIconKey };
}
}
```
### INSTANCE_SWAP Properties
Allow swapping a nested component instance (e.g., swap the icon):
```javascript
// defaultIconCompId is the ID of the default icon component (from state ledger)
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_COMP_ID);
for (const child of cs.children) {
const iconSlot = child.findOne(n => n.name === 'icon');
if (iconSlot && iconSlot.type === 'INSTANCE') {
iconSlot.componentPropertyReferences = { mainComponent: iconKey };
}
}
```
**Use INSTANCE_SWAP instead of creating a variant per icon.** Never add "Icon=ChevronRight, Icon=ChevronLeft, ..." as VARIANT axes — that causes combinatorial explosion. One INSTANCE_SWAP property covers all icons.
### Creating Icon Components for INSTANCE_SWAP
INSTANCE_SWAP needs a real Component ID as its default value. Before wiring INSTANCE_SWAP, you need at least one icon component. Here's how to create icons from SVG:
```javascript
// Create a simple icon component from SVG
const svgNode = figma.createNodeFromSvg(
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' +
'<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
'</svg>'
);
// Wrap in a component
const iconComp = figma.createComponent();
iconComp.name = 'Icon/ChevronRight';
iconComp.resize(24, 24);
iconComp.clipsContent = true;
// Move SVG children into the component
for (const child of [...svgNode.children]) {
iconComp.appendChild(child);
}
svgNode.remove();
// Bind the icon fill to a color variable (so it respects themes)
// Find vector children and bind their fills
iconComp.findAll(n => n.type === 'VECTOR').forEach(vec => {
// For stroke-based icons:
if (vec.strokes.length > 0) {
const strokePaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', iconColorVar
);
vec.strokes = [strokePaint];
}
});
iconComp.setSharedPluginData('dsb', 'run_id', RUN_ID);
iconComp.setSharedPluginData('dsb', 'key', 'icon/chevron-right');
return { iconCompId: iconComp.id };
```
**Then use the returned `iconCompId` as the default value for INSTANCE_SWAP:**
```javascript
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', ICON_COMP_ID);
```
**Constraining swap options with `preferredValues`:**
After adding the INSTANCE_SWAP property, you can optionally limit which components appear in the swap picker:
```javascript
// Get the property definitions to find the exact key
const props = cs.componentPropertyDefinitions;
const iconPropKey = Object.keys(props).find(k => k.startsWith('Icon'));
// Set preferred values (array of component keys or instance IDs)
cs.editComponentProperty(iconPropKey, {
preferredValues: [
{ type: 'COMPONENT', key: chevronRightComp.key },
{ type: 'COMPONENT', key: chevronLeftComp.key },
{ type: 'COMPONENT', key: closeComp.key },
],
});
```
**Icon library tip:** Create all icon components on a dedicated `Icons` page before building any UI components. Then reference their IDs when wiring INSTANCE_SWAP properties.
### `componentPropertyReferences` mapping
The `componentPropertyReferences` object maps a node's own property to a component property key:
| Node property | Component property type | Used for |
|---|---|---|
| `characters` | TEXT | Editable text content |
| `visible` | BOOLEAN | Show/hide toggle |
| `mainComponent` | INSTANCE_SWAP | Swap nested instances |
---
## 7. `sharedPluginData` Tagging for Idempotency
Tag EVERY created node immediately after creation. This enables safe cleanup, resumability, and idempotency checks.
```javascript
// After creating any node:
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // identifies the build run
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created it
node.setSharedPluginData('dsb', 'key', KEY); // unique logical key for this entity
// Reading back:
const runId = node.getSharedPluginData('dsb', 'run_id'); // '' if not set
const key = node.getSharedPluginData('dsb', 'key');
```
**Key naming convention:** use `/`-separated logical paths that mirror the entity hierarchy:
```
'component/button/base'
'component/button/variant/Medium/Primary/Default'
'componentset/button'
'doc/button'
'page/button'
```
**Idempotency check before creating:** before creating a node, scan the current page for an existing node with the same `key`:
```javascript
const existing = figma.currentPage.findAll(n =>
n.getSharedPluginData('dsb', 'key') === 'componentset/button'
);
if (existing.length > 0) {
// Skip creation — already done. Return existing node's ID.
return { componentSetId: existing[0].id };
}
```
---
## 8. Documentation
### Page title + description frame
The documentation frame (see Section 2) should contain:
1. Component name as a large title (32px+ Bold)
2. 1–3 sentence description of what the component is and when to use it
3. Spec notes (sizes, spacing values, accessibility notes)
### Component `description` property
Set the description on the ComponentSet — it appears in the Figma properties panel and is exported as documentation:
```javascript
cs.description = 'Buttons allow users to take actions and make choices. Use Primary for the highest-emphasis action on a page.';
```
### `documentationLinks`
Link to external documentation (Storybook, design spec, tokens reference):
```javascript
cs.documentationLinks = [
{ uri: 'https://your-storybook.com/button' }
];
```
### Node names and organization
- ComponentSet: plain component name — `'Button'`
- Individual variants: `'Property=Value, Property=Value'` format (match the file's existing casing)
- Child nodes: semantic names — `'label'`, `'icon'`, `'container'`, `'state-layer'`
- Documentation frames: `'ComponentName / Documentation'`
---
## 9. Validation
Always validate after creating or modifying a component before proceeding to the next one.
### `get_metadata` structural checks
After creating the component set, call `get_metadata` on the ComponentSet node and verify:
- `variantGroupProperties` lists the expected axes with the correct value arrays
- `componentPropertyDefinitions` contains the expected TEXT/BOOLEAN/INSTANCE_SWAP properties
- `children.length` equals the expected variant count (e.g., 18 for 3×2×3)
- No children are named `'Component 1'` (unnamed components are a sign of a bug)
### `get_screenshot` — Visual Validation (Critical)
`get_screenshot` returns an **image** of the specified node. Call it on the **component page node** (not the component set) to see the full page including documentation and grid labels.
```
Tool: get_screenshot
Args: { nodeId: "PAGE_NODE_ID", fileKey: "FILE_KEY" }
```
**How to use the screenshot:**
1. **Display it to the user** — this is the primary purpose. Show the screenshot as part of the user checkpoint: "Here's the Button component. Does it look right?"
2. **Analyze it yourself** — if you have vision capabilities, check the visual checklist below. If you don't (text-only agent), fall back to structural validation only via `get_metadata` and describe what you created textually.
**Visual validation checklist** (check each item when viewing the screenshot):
| # | Check | What "good" looks like | What "broken" looks like |
|---|-------|----------------------|------------------------|
| 1 | **Grid layout** | Variants in neat rows and columns with consistent spacing | All variants piled at top-left (0,0 stacking bug) |
| 2 | **Color fills** | Components show distinct, correct colors per style variant | All components are black or same color (variable binding failed) |
| 3 | **Size differentiation** | Small variants are visibly smaller than Large variants | All variants are the same size (height/padding not bound to variables) |
| 4 | **Text readability** | Labels are visible with correct font and color | Text is invisible (white on white), missing, or shows "undefined" |
| 5 | **Spacing/padding** | Interior padding visible, components aren't "shrink-wrapped" | Components look cramped or have no visible internal space |
| 6 | **State differentiation** | Hover/Pressed variants have visible color differences from Default | All states look identical (state-specific fills not applied) |
| 7 | **Disabled state** | Lower opacity or muted colors compared to active states | Disabled looks identical to Default |
| 8 | **Documentation frame** | Title + description text visible above or beside the component grid | No documentation, or it overlaps the component set |
| 9 | **Grid labels** | Row/column headers visible around the component set (if added) | Labels overlap the grid or are missing |
| 10 | **Component set boundary** | Gray background frame wraps all variants with even padding | Frame is too small (variants clipped) or way too large |
**Screenshot → diagnosis → fix mapping:**
| Screenshot shows | Diagnosis | Fix script |
|-----------------|-----------|------------|
| All variants stacked top-left | Grid layout wasn't applied after `combineAsVariants` | Re-run the grid layout script (§5) |
| Everything black/same color | Variable bindings failed or variables don't have values for the active mode | Re-run variable binding, check mode values |
| No text visible | Font wasn't loaded, or text fill is same color as background | Check `loadFontAsync` was called; bind text fill to `color/text/*` variable |
| Variants all same size | Padding/height not bound to size variables | Re-run `bindVariablesToComponent` with size-specific tokens |
| Component set frame tiny | `resizeWithoutConstraints` wasn't called or used wrong dimensions | Re-calculate bounds from children and resize |
| Doc frame overlaps components | Component set positioned at same x,y as doc frame | Move component set: `cs.x = docFrame.x + docFrame.width + 60` |
**When visual analysis isn't available:**
If your model can't process images (text-only mode), validate structurally instead:
1. Call `get_metadata` on the component set — verify child count, property definitions, variant names
2. Run an `use_figma` that samples key properties:
```javascript
const cs = await figma.getNodeByIdAsync(CS_ID);
const sample = cs.children.slice(0, 3).map(c => ({
name: c.name,
width: c.width, height: c.height,
x: c.x, y: c.y,
fills: c.fills?.map(f => f.type === 'SOLID' ?
{ r: f.color.r.toFixed(2), g: f.color.g.toFixed(2), b: f.color.b.toFixed(2), boundVar: f.boundVariables?.color?.id } : f.type
),
}));
return { sampleVariants: sample, totalChildren: cs.children.length };
```
This gives you positions (grid working?), dimensions (size differentiation?), and fill info (bindings working?) without needing vision.
**When to take a screenshot:**
- After EVERY completed component (mandatory — part of the user checkpoint)
- After creating the foundations documentation page
- After final QA (screenshot every page)
- Do NOT screenshot after every intermediate step (wastes tool calls)
### Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
| All variants stacked at (0,0) | `combineAsVariants` was called but children were never repositioned | Re-run grid layout script |
| Variants show wrong colors | Variable bindings applied after `combineAsVariants` instead of before | Rebind on component set children |
| Variant count wrong | Clone loop indexing error | Print `components.map(c => c.name)` before combining |
| BOOLEAN property has no effect | `componentPropertyReferences` was set on the component set frame, not on the child node | Find the actual child node and set references there |
| INSTANCE_SWAP shows no swap option | Default value was not a valid component ID | Pass a real existing component ID as `defaultValue` |
| `combineAsVariants` throws | At least one node in the array is not a `ComponentNode` | Filter array: `nodes.filter(n => n.type === 'COMPONENT')` |
| `addComponentProperty` returns unexpected key | Expected — the key gets a `#id:id` suffix | Save the returned value immediately: `const key = cs.addComponentProperty(...)` |
---
## 10. Complete Worked Example: Button Component
This shows the full sequence of `use_figma` calls for a Button component, including state passing between calls. Replace `RUN_ID` and variable IDs with your actual values from the state ledger.
### Call 1: Create the component page
**Goal:** Create (or find) the Button page.
**State input:** None
**State output:** `{ pageId }`
```javascript
let page = figma.root.children.find(p => p.name === 'Button');
if (!page) { page = figma.createPage(); page.name = 'Button'; }
page.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
page.setSharedPluginData('dsb', 'key', 'page/button');
return { pageId: page.id };
```
### Call 2: Create documentation frame
**Goal:** Add title + description frame.
**State input:** `{ pageId }`
**State output:** `{ docFrameId }`
```javascript
const PAGE_ID = 'PAGE_ID_FROM_STATE';
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
// Idempotency check
const existing = page.findAll(n => n.getSharedPluginData('dsb', 'key') === 'doc/button');
if (existing.length > 0) {
return { docFrameId: existing[0].id };
}
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const docFrame = figma.createFrame();
docFrame.name = 'Button / Documentation';
docFrame.x = 40; docFrame.y = 40;
docFrame.layoutMode = 'VERTICAL';
docFrame.primaryAxisSizingMode = 'AUTO';
docFrame.counterAxisSizingMode = 'FIXED';
docFrame.resize(560, 100);
docFrame.paddingTop = 40; docFrame.paddingBottom = 40;
docFrame.paddingLeft = 40; docFrame.paddingRight = 40;
docFrame.itemSpacing = 16;
docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.fontSize = 32;
title.characters = 'Button';
docFrame.appendChild(title);
const desc = figma.createText();
desc.fontName = { family: 'Inter', style: 'Regular' };
desc.fontSize = 14;
desc.characters = 'Buttons allow users to take actions with a single tap. Use Primary for the highest-emphasis action on a page, Secondary for supporting actions.';
desc.layoutSizingHorizontal = 'FILL';
docFrame.appendChild(desc);
docFrame.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
docFrame.setSharedPluginData('dsb', 'key', 'doc/button');
return { docFrameId: docFrame.id };
```
### Call 3: Create base component
**Goal:** Create the base component with auto-layout and all variable bindings.
**State input:** `{ pageId }` + variable IDs from Phase 1
**State output:** `{ baseCompId }`
*(See Section 3 for full code — substituting the actual variable IDs from the state ledger.)*
### Call 4: Create all variants
**Goal:** Clone base and produce all 18 variants (3 Size × 2 Style × 3 State).
**State input:** `{ pageId, baseCompId }` + variable IDs
**State output:** `{ variantIds: ['id1', 'id2', ..., 'id18'] }`
```javascript
const RUN_ID = 'ds-build-2024-001';
const BASE_ID = 'BASE_COMP_ID_FROM_STATE';
const PAGE_ID = 'PAGE_ID_FROM_STATE';
// Variable IDs from state ledger:
const VAR = {
bg_primary: 'VAR_ID_1',
text_primary: 'VAR_ID_2',
bg_secondary: 'VAR_ID_3',
text_secondary: 'VAR_ID_4',
bg_disabled: 'VAR_ID_5',
text_disabled: 'VAR_ID_6',
padding_sm: 'VAR_ID_7',
padding_md: 'VAR_ID_8',
padding_lg: 'VAR_ID_9',
};
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
const base = await figma.getNodeByIdAsync(BASE_ID);
// Load all variables
const vars = {};
for (const [k, v] of Object.entries(VAR)) {
vars[k] = await figma.variables.getVariableByIdAsync(v);
}
const axes = {
Size: ['Small', 'Medium', 'Large'],
Style: ['Primary', 'Secondary'],
State: ['Default', 'Hover', 'Disabled'],
};
const paddingMap = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg };
const components = [];
for (const size of axes.Size) {
for (const style of axes.Style) {
for (const state of axes.State) {
const clone = base.clone();
clone.name = `Size=${size}, Style=${style}, State=${state}`;
clone.setBoundVariable('paddingTop', paddingMap[size]);
clone.setBoundVariable('paddingBottom', paddingMap[size]);
clone.setBoundVariable('paddingLeft', paddingMap[size]);
clone.setBoundVariable('paddingRight', paddingMap[size]);
const isDisabled = state === 'Disabled';
const bgV = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary);
const txV = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary);
clone.fills = [figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgV
)];
const labelNode = clone.findOne(n => n.name === 'label');
labelNode.fills = [figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txV
)];
clone.setSharedPluginData('dsb', 'run_id', RUN_ID);
clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`);
components.push(clone);
}
}
}
return { variantIds: components.map(c => c.id) };
```
### Call 5: combineAsVariants + grid layout
**Goal:** Combine all 18 variants into a ComponentSet and lay them out in a grid.
**State input:** `{ pageId, variantIds }` (18 IDs)
**State output:** `{ componentSetId }`
*(See Section 5 for full code.)*
### Call 6: Add component properties
**Goal:** Add TEXT, BOOLEAN, INSTANCE_SWAP properties and wire them to child nodes.
**State input:** `{ pageId, componentSetId }`
**State output:** `{ componentSetId, properties: { labelKey, showIconKey, iconKey } }`
```javascript
const CS_ID = 'CS_ID_FROM_STATE';
const DEFAULT_ICON_ID = 'ICON_COMP_ID_FROM_STATE';
const page = figma.root.children.find(p => p.name === 'Button');
await figma.setCurrentPageAsync(page);
const cs = await figma.getNodeByIdAsync(CS_ID);
cs.description = 'Buttons allow users to take actions and make choices with a single tap.';
cs.documentationLinks = [{ uri: 'https://your-storybook.com/button' }];
// Add properties — save returned keys
const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button');
const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true);
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_ID);
// Wire to children
for (const child of cs.children) {
const labelNode = child.findOne(n => n.name === 'label');
if (labelNode) labelNode.componentPropertyReferences = { characters: labelKey };
const iconNode = child.findOne(n => n.name === 'icon');
if (iconNode) {
iconNode.componentPropertyReferences = {
visible: showIconKey,
...(iconNode.type === 'INSTANCE' ? { mainComponent: iconKey } : {}),
};
}
}
return {
componentSetId: cs.id,
properties: { labelKey, showIconKey, iconKey },
};
```
### Call 7: Validate with get_metadata
**Goal:** Structural check — variant count, properties, axes.
**Action:** Call `get_metadata` on the ComponentSet node ID (from state). Verify in the result:
- `children.length === 18`
- `variantGroupProperties` has `Size`, `Style`, `State` keys with correct value arrays
- `componentPropertyDefinitions` has `Label`, `Show Icon`, `Icon` entries
### Call 8: Validate with get_screenshot
**Goal:** Visual check — layout, colors, text.
**Action:** Call `get_screenshot` on the Button page. Inspect the screenshot. If variants are stacked, re-run Call 5. If colors look wrong, inspect variable bindings.
### Checkpoint
After Call 8: show the screenshot to the user. Ask: "Here's the Button component with 18 variants. Does this look correct?" Do not proceed to the next component until the user approves.
@@ -0,0 +1,494 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Discovery Phase Reference
This document covers everything needed for Phase 0 of a design system build: analyzing the codebase for tokens, inspecting the Figma file for existing conventions, searching subscribed libraries, building the plan, and resolving conflicts before any write operations begin.
---
## 1. Codebase Analysis — Finding Token Sources
### Search Priority Order
Look for token sources in this order. Stop as soon as you find a definitive source; multiple formats can coexist:
1. Design token files: `*.tokens.json`, `tokens/*.json`, `src/tokens/**`
2. CSS variable files: `variables.css`, `tokens.css`, `theme.css`, `global.css`
3. Tailwind config: `tailwind.config.js`, `tailwind.config.ts`
4. CSS-in-JS theme objects: `theme.ts`, `createTheme`, `ThemeProvider`
5. Platform-specific: iOS Asset catalogs (`.xcassets`), Android `themes.xml`, `colors.xml`
### CSS Custom Properties (Most Common for Web)
**What to search for:**
```
:root { ... }
@theme { ... } ← Tailwind v4
--color-*, --spacing-*, --radius-*, --shadow-*, --font-*
```
**Pattern:** `/--[\w-]+:\s*[^;]+/g`
**Common file locations:** `src/styles/tokens.css`, `src/styles/variables.css`, `src/theme/*.css`
**Extraction and naming translation:**
| CSS Property | Figma Variable Name | Figma Type | WEB Code Syntax |
|---|---|---|---|
| `--color-bg-primary: #fff` | `color/bg/primary` | COLOR | `var(--color-bg-primary)` |
| `--color-text-secondary: #757575` | `color/text/secondary` | COLOR | `var(--color-text-secondary)` |
| `--spacing-sm: 8px` | `spacing/sm` | FLOAT | `var(--spacing-sm)` |
| `--radius-md: 8px` | `radius/md` | FLOAT | `var(--radius-md)` |
| `--font-body: "Inter"` | `typography/body/font-family` | STRING | `var(--font-body)` |
**Naming rule:** Replace hyphens with slashes at category boundaries. Keep hyphens within the final path segment: `--color-bg-primary` → `color/bg/primary`, but `--color-bg-primary-hover` → `color/bg/primary-hover`.
**Always store the original CSS variable name** as the code syntax value — never derive it from the Figma variable name. If the codebase uses `--sds-color-background-brand-default`, use exactly that string in `setVariableCodeSyntax('WEB', '--sds-color-background-brand-default')`.
### Tailwind Configuration
**What to look for in `tailwind.config.js` or `tailwind.config.ts`:**
```javascript
// theme.extend.colors → Figma color variables
{ primary: { DEFAULT: '#3366FF', light: '#6699FF', dark: '#0033CC' } }
// → color/primary/default, color/primary/light, color/primary/dark
// theme.extend.spacing → Figma FLOAT variables
{ 'xs': '4px', 'sm': '8px', 'md': '16px' }
// → spacing/xs = 4, spacing/sm = 8, spacing/md = 16
// theme.extend.borderRadius → Figma FLOAT variables
{ 'sm': '4px', 'md': '8px', 'lg': '16px' }
// → radius/sm = 4, radius/md = 8, radius/lg = 16
```
Tailwind utility class names (`bg-blue-500`, `p-4`) are not tokens — extract values from the config object, not the class names.
### Design Token Community Group (DTCG) Format
**Pattern:** `*.tokens.json` or `tokens/*.json`. Find source files, not generated outputs from Style Dictionary or Tokens Studio.
```json
{
"color": {
"bg": {
"primary": { "$type": "color", "$value": "#ffffff" },
"secondary": { "$type": "color", "$value": "#f5f5f5" }
}
},
"spacing": {
"sm": { "$type": "dimension", "$value": "8px" }
}
}
```
Nested keys map to slash-separated Figma names: `color.bg.primary` → `color/bg/primary`.
### CSS-in-JS / Theme Objects
**What to search for:** `createTheme`, `ThemeProvider`, `theme = {}`, styled-components, Emotion, Stitches, vanilla-extract
```typescript
// theme.colors.bg.primary → Figma variable: color/bg/primary
// theme.spacing.sm → Figma variable: spacing/sm
// Multiple theme objects (lightTheme, darkTheme) → modes in the same collection
```
### iOS Token Sources
```swift
// Asset catalog colors in .xcassets/Colors.xcassets
// extension Color { static let bgPrimary = Color("bg-primary") }
// Look for traitCollection.userInterfaceStyle for dark mode detection
```
### Android Token Sources
```kotlin
// res/values/colors.xml <color name="primary">#3366FF</color>
// res/values-night/colors.xml (dark mode overrides)
// MaterialTheme.colorScheme.primary in Compose
// val Primary = Color(0xFF3366FF)
```
### Detecting Dark Mode
| Platform | Signal |
|---|---|
| Web (CSS) | `@media (prefers-color-scheme: dark)`, `.dark { }`, `[data-theme="dark"]` |
| Web (Tailwind) | `darkMode: 'class'` or `darkMode: 'media'` in config |
| Web (JS) | Separate `darkTheme` object alongside `lightTheme` |
| iOS | `Color(uiColor:)` with `traitCollection.userInterfaceStyle`, dual-appearance asset catalog |
| Android | `themes.xml` with `Theme.*.Night`, `isSystemInDarkTheme()` in Compose, `values-night/` folder |
**Figma mapping:** If dark mode exists → minimum 2 modes (Light/Dark) in the semantic color collection. Primitive collections stay single-mode.
### Shadow/Elevation Extraction
Shadows cannot be Figma variables — they become **Effect Styles**.
```css
/* Look for: box-shadow, --shadow-* */
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.10);
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.10);
```
CSS `0 4px 6px -1px rgba(0,0,0,0.1)` → Figma:
```
{ type: "DROP_SHADOW", offset: {x:0, y:4}, radius: 6, spread: -1, color: {r:0, g:0, b:0, a:0.1} }
```
### Typography Extraction
| Code token | Maps to |
|---|---|
| `font-size: 16px` | FLOAT variable (scope `FONT_SIZE`) or Text Style `fontSize` |
| `line-height: 1.5` | Text Style `lineHeight: {value: 24, unit: "PIXELS"}` |
| `font-weight: 600` | Text Style `fontName: {family: "Inter", style: "Semi Bold"}` |
| `letter-spacing: -0.02em` | Text Style `letterSpacing: {value: -2, unit: "PERCENT"}` |
| `font-family: "Inter"` | STRING variable (scope `FONT_FAMILY`) or Text Style `fontName.family` |
Composite text styles (all properties bundled) → Figma Text Styles. Individual properties → Figma variables with appropriate scopes.
### Component Extraction
For each component, extract:
1. **Name** → Figma component set name
2. **Union-type props** → VARIANT properties
3. **String content props** → TEXT properties
4. **Boolean props** → BOOLEAN properties (and VARIANT State when combined with interaction states)
5. **Child/slot props** → INSTANCE_SWAP properties
```typescript
// React example:
interface ButtonProps {
size: 'sm' | 'md' | 'lg'; // → VARIANT: Size = sm|md|lg
variant: 'primary' | 'secondary'; // → VARIANT: Style = primary|secondary
disabled?: boolean; // → VARIANT: State (combine: default|hover|pressed|disabled)
label: string; // → TEXT: Label
icon?: ReactNode; // → INSTANCE_SWAP: Icon + BOOLEAN: Show Icon
}
// → Component Set "Button", variant count: 3 sizes × 2 styles × 4 states = 24
```
---
## 2. Figma File Inspection
Run these `use_figma` snippets at the start of every build. All are read-only and safe to run before any user checkpoint.
### List All Pages
```javascript
const pages = figma.root.children.map((p, i) => ({
index: i,
name: p.name,
id: p.id,
childCount: p.children.length
}));
return { pages };
```
Interpret: note page names for naming convention (are they PascalCase? sentence case?), count separator pages (`---`), identify existing component pages vs foundations pages.
### List Variable Collections With Modes
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const result = collections.map(c => ({
id: c.id,
name: c.name,
modes: c.modes, // [{modeId, name}, ...]
variableCount: c.variableIds.length,
defaultModeId: c.defaultModeId
}));
return { collections: result };
```
Interpret: identify existing primitive/semantic split, note mode names (do they use "Light/Dark" or "SDS Light/SDS Dark"?), count variables to understand scope.
### List Variables in a Collection (with names, types, scopes, and sample values)
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const targetName = "Color"; // change to the collection you want to inspect
const coll = collections.find(c => c.name === targetName);
if (!coll) { return { error: `Collection "${targetName}" not found` }; }
const allVars = await figma.variables.getLocalVariablesAsync();
const vars = allVars.filter(v => v.variableCollectionId === coll.id);
const result = vars.map(v => ({
id: v.id,
name: v.name,
resolvedType: v.resolvedType,
scopes: v.scopes,
codeSyntax: v.codeSyntax,
// First mode value only, for a sample
sampleValue: v.valuesByMode[coll.defaultModeId]
}));
return { collection: coll.name, variableCount: result.length, variables: result };
```
Interpret: check if variables use `ALL_SCOPES` (bad), check naming convention (slash-separated hierarchy?), check if code syntax is set, identify alias chains.
### List Component Sets with Properties
```javascript
await figma.setCurrentPageAsync(figma.currentPage); // ensures page context
const componentSets = figma.currentPage.findAll(n => n.type === 'COMPONENT_SET');
const result = componentSets.map(cs => ({
id: cs.id,
name: cs.name,
variantCount: cs.children.length,
properties: Object.entries(cs.componentPropertyDefinitions).map(([key, def]) => ({
name: key,
type: def.type,
variantOptions: def.variantOptions || null,
defaultValue: def.defaultValue
}))
}));
return { componentSets: result, count: result.length };
```
Note: to search ALL pages, iterate `figma.root.children` and `setCurrentPageAsync` for each.
### List All Styles
```javascript
const [textStyles, effectStyles, paintStyles] = await Promise.all([
figma.getLocalTextStylesAsync(),
figma.getLocalEffectStylesAsync(),
figma.getLocalPaintStylesAsync()
]);
return {
textStyles: textStyles.map(s => ({ id: s.id, name: s.name, fontSize: s.fontSize, fontName: s.fontName })),
effectStyles: effectStyles.map(s => ({ id: s.id, name: s.name, effectCount: s.effects.length })),
paintStyles: paintStyles.map(s => ({ id: s.id, name: s.name })),
counts: { text: textStyles.length, effect: effectStyles.length, paint: paintStyles.length }
};
```
### Check Naming Conventions on an Existing Component
```javascript
// Replace with the node ID of an existing component to analyze
const node = await figma.getNodeByIdAsync("YOUR_NODE_ID");
if (!node) { return { error: "Node not found" }; }
// Check fills for variable bindings
const fillInfo = [];
if ('fills' in node && Array.isArray(node.fills)) {
for (const fill of node.fills) {
if (fill.type === 'SOLID' && fill.boundVariables?.color) {
fillInfo.push({ type: 'variable_alias', id: fill.boundVariables.color.id });
} else if (fill.type === 'SOLID') {
fillInfo.push({ type: 'hardcoded', r: fill.color.r, g: fill.color.g, b: fill.color.b });
}
}
}
return {
name: node.name,
type: node.type,
fills: fillInfo,
sharedPluginData: node.getSharedPluginData('dsb', 'key') || null
};
```
---
## 3. Using search_design_system
### What It Searches
`search_design_system` runs three parallel searches against **subscribed design libraries** for the given file:
1. **Components** — published library components, searched by name/description via a recommendation engine (relevance-ranked, not exact match)
2. **Variables** — design tokens (colors, spacing, etc.) across subscribed libraries
3. **Styles** — paint styles, text styles, and effect styles
Only libraries the file has subscribed to are searched. If results are empty, the file may not be subscribed to any design system libraries.
### Input
```
search_design_system({
query: "button", // required — text query
fileKey: "abc123", // required — your file key
includeComponents: true, // default true
includeVariables: true, // default true
includeStyles: true // default true
})
```
### What It Returns
```json
{
"components": [
{
"name": "Button",
"libraryName": "Design System",
"assetType": "component_set",
"componentKey": "abc123def",
"description": "Primary action button"
}
],
"variables": [
{
"name": "colors/primary/500",
"variableType": "COLOR",
"variableSetKey": "set1key",
"key": "var1key",
"scopes": ["FRAME_FILL", "SHAPE_FILL"],
"variableCollectionName": "Colors"
}
],
"styles": [
{
"name": "Heading/H1",
"styleType": "TEXT",
"key": "style1key"
}
]
}
```
### How to Interpret Results
**Components:** The `componentKey` can be used in `use_figma` to import the component:
```javascript
const component = await figma.importComponentByKeyAsync("abc123def");
// or for component sets:
const componentSet = await figma.importComponentSetByKeyAsync("abc123def");
```
**Variables:** The `variableSetKey` is the collection key. The `key` is the variable key. Use these to understand what naming conventions are in use, and what tokens are available to alias from.
**Styles:** The `key` is usable with `figma.importStyleByKeyAsync(key)` to import into the current file.
### When to Search
- **Phase 0, step 0c**: Search broadly (`query: "button"`, `query: "color"`, `query: "spacing"`) before planning anything. This establishes the reuse baseline.
- **Immediately before each component creation**: Search for the specific component name before writing any `use_figma` creation code.
**Reuse decision:**
| Condition | Decision |
|---|---|
| Found component with matching variant API, same token model | Import and reuse |
| Found component but wrong variant properties or hardcoded values | Rebuild |
| Found component that matches visually but API is incompatible | Wrap: nest as instance inside a new wrapper component |
---
## 4. Building the Plan
After codebase analysis and Figma inspection, produce a mapping table and present it to the user.
### Token → Variable Mapping Table
For each token found in code, record:
| Code Token | CSS Name | Raw Value | Figma Collection | Figma Variable Name | Figma Type | Mode(s) |
|---|---|---|---|---|---|---|
| `theme.colors.blue[500]` | `--color-blue-500` | `#3B82F6` | Primitives | `blue/500` | COLOR | Value |
| `theme.colors.bg.primary` | `--color-bg-primary` | (light: blue/50, dark: gray/900) | Color | `color/bg/primary` | COLOR | Light, Dark |
| `theme.spacing.sm` | `--spacing-sm` | `8px` | Spacing | `spacing/sm` | FLOAT | Value |
| `theme.radii.md` | `--radius-md` | `8px` | Spacing | `radius/md` | FLOAT | Value |
| `theme.shadows.md` | `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | — | — | Effect Style | — |
### Component → Component Set Mapping Table
| Code Component | Props → Variant Axes | Variant Count | Figma Page | Reuse? |
|---|---|---|---|---|
| `Button` | size (sm/md/lg) × variant (primary/secondary) × state (default/hover/disabled) | 18 | Buttons | Search first |
| `Avatar` | size (sm/md/lg) × type (image/initials/icon) | 9 | Avatars | Search first |
### Gap Identification
Compare what was found in code vs what already exists in Figma:
- **New:** tokens or components that exist in code but not in Figma → create
- **Existing:** tokens or components already in Figma with matching names → verify scope/code-syntax, skip or update
- **Conflict:** same name, different value → escalate to user (see section 5)
- **Figma-only:** exists in Figma but not in code → flag for user, likely skip
### User-Facing Checkpoint Message Template
Present this message before proceeding. Never begin Phase 1 without explicit user approval.
```
Here's what I found and what I plan to build:
CODEBASE ANALYSIS
Colors: {N} primitives ({families}), {M} semantic tokens ({light/dark if applicable})
Spacing: {N} tokens ({range})
Typography: {N} text styles, {M} individual scale tokens
Shadows: {N} levels → will become Effect Styles
Components: {list of component names}
EXISTING FIGMA FILE
Collections: {N} existing collections
Variables: {M} existing variables
Styles: {K} text, {L} effect, {J} paint styles
Components: {list}
PLAN
New collections: {list with mode counts}
New variables: ~{N} ({breakdown by collection})
New styles: {N} text, {M} effect
New components: {list}
Libraries to search before each component: {list}
GAPS / CONFLICTS NEEDING DECISIONS
⚠ {conflict description} — Code says X, Figma already has Y. Which wins?
WHAT I WON'T BUILD (and why)
- {item}: already exists in Figma with matching conventions
- {item}: not supported as a Figma variable (e.g. z-index, animation timing)
Shall I proceed?
```
---
## 5. Conflict Resolution — When Code and Figma Disagree
When the same token/component exists in both code and Figma but with different values, names, or structures, **always ask the user**. Never silently pick one.
### Decision Framework
| Scenario | Ask the user |
|---|---|
| Same CSS name, different hex value (e.g., `--color-accent` is `#3366FF` in code but `#5B7FFF` in Figma) | "Code says `#3366FF`, Figma currently has `#5B7FFF` for `color/accent/default`. Which is correct?" |
| Same component name, different variant axes (code has `size: sm/md/lg`, Figma has `Size: Small/Large`) | "Code uses 3 sizes (sm/md/lg) but Figma has 2 (Small/Large). Should I add Medium, or rename to match code?" |
| Code has a semantic token with no primitive layer; Figma already has a fully-layered system | "The codebase uses a flat single-layer token model. The Figma file uses a primitive/semantic split. Should I match the Figma architecture or the code architecture?" |
| Figma variable exists but has `ALL_SCOPES` (incorrect per best practice) | "I found `color/bg/primary` already exists but it uses ALL_SCOPES. I recommend changing it to `FRAME_FILL, SHAPE_FILL`. May I update the scope?" |
| Code uses camelCase (`backgroundColor`), Figma uses slash-separated (`color/bg/default`) | "The codebase uses camelCase naming. The Figma file uses slash-separated hierarchy. For new variables, should I use slash-separated (Figma standard) and map via code syntax?" |
### Code Wins
Default to code as the source of truth for:
- Hex values (code is the live production value)
- Token naming (the CSS variable names become code syntax)
- Mode values (light/dark split comes from code)
### Figma Wins
Default to Figma as the source of truth for:
- Collection architecture (if a well-structured system already exists, extend it rather than replace it)
- Variable naming hierarchy (if designers are already using the system with specific names)
- Page structure (match the existing page organization pattern)
### Neither: Negotiate
When neither is clearly correct, propose a resolution and ask:
> "I'd suggest [option]. This way both the code token name and the Figma naming convention are preserved. Does that work?"
@@ -0,0 +1,834 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Documentation Creation Reference
This reference covers Phase 2 of the design system build: the cover page, foundations documentation page (color swatches, type specimens, spacing bars, shadow cards, radius demo), page layout dimensions, and inline component documentation. Every code block is complete `use_figma`-ready JavaScript (helper-function form — meant to be embedded in a larger script that uses `return` to send results back).
---
## 1. Cover Page
The cover page is always the first page in the file. It is a branded title card that sets context for anyone opening the file.
### What to include
- File/system name as a large heading (48–72px)
- Version string or date
- Brief tagline (1 sentence)
- Optional: color block background using the primary brand color variable
### Cover page dimensions
The cover frame should be **1440 × 900px** — this matches the default Figma canvas and looks correct in the page thumbnail.
### use_figma for cover page
```javascript
async function createCoverPage(systemName, tagline, version, primaryColorVar) {
// primaryColorVar: a Figma Variable object for the brand primary fill
const page = figma.createPage();
page.name = 'Cover';
await figma.setCurrentPageAsync(page);
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
const frame = figma.createFrame();
frame.name = 'Cover';
frame.resize(1440, 900);
frame.x = 0;
frame.y = 0;
frame.layoutMode = 'VERTICAL';
frame.primaryAxisAlignItems = 'CENTER';
frame.counterAxisAlignItems = 'CENTER';
frame.itemSpacing = 16;
frame.paddingTop = 0;
frame.paddingBottom = 0;
frame.paddingLeft = 0;
frame.paddingRight = 0;
// Background: bind to primary variable if provided, else solid dark
if (primaryColorVar) {
const bgPaint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.05, g: 0.05, b: 0.05 } },
'color',
primaryColorVar
);
frame.fills = [bgPaint];
} else {
frame.fills = [{ type: 'SOLID', color: { r: 0.06, g: 0.06, b: 0.07 } }];
}
page.appendChild(frame);
// System name heading
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.characters = systemName;
title.fontSize = 64;
title.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
title.textAlignHorizontal = 'CENTER';
frame.appendChild(title);
// Tagline
const tag = figma.createText();
tag.fontName = { family: 'Inter', style: 'Regular' };
tag.characters = tagline;
tag.fontSize = 20;
tag.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.7 } }];
tag.textAlignHorizontal = 'CENTER';
frame.appendChild(tag);
// Version
const ver = figma.createText();
ver.fontName = { family: 'Inter', style: 'Medium' };
ver.characters = version;
ver.fontSize = 13;
ver.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.45 } }];
ver.textAlignHorizontal = 'CENTER';
frame.appendChild(ver);
return { page, frameId: frame.id };
}
```
---
## 2. Foundations Page
The Foundations page is always placed **before any component pages**. It visually documents the design tokens — colors, typography, spacing, shadows, and border radii — so designers and engineers can see available primitives at a glance.
### Page layout dimensions
The outer documentation frame should be **1440px wide**. Sections stack vertically with **64–100px gaps** between them. Each section frame fills the full 1440px width and hugs its content vertically.
### Full Foundations page skeleton
```javascript
async function createFoundationsPage() {
const page = figma.createPage();
page.name = 'Foundations';
await figma.setCurrentPageAsync(page);
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
// Root scroll frame
const root = figma.createFrame();
root.name = 'Foundations';
root.layoutMode = 'VERTICAL';
root.primaryAxisAlignItems = 'MIN';
root.counterAxisAlignItems = 'MIN';
root.itemSpacing = 80;
root.paddingTop = 80;
root.paddingBottom = 120;
root.paddingLeft = 80;
root.paddingRight = 80;
root.layoutSizingHorizontal = 'FIXED';
root.layoutSizingVertical = 'HUG';
root.resize(1440, 1);
root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
page.appendChild(root);
return { page, root };
}
```
---
## 3. Color Swatches (bound to variables)
Color swatches must be **bound to actual Figma variables** — never hardcode hex values in swatch fills. This keeps documentation in sync automatically when variable values change.
### Single color swatch
```javascript
/**
* Creates a single color swatch card (rectangle + variable name label).
* The swatch rectangle fill is bound to the provided variable.
*
* @param {FrameNode} parent - The auto-layout row to append to.
* @param {string} varName - Display name (e.g. "color/bg/primary").
* @param {Variable} variable - The Figma Variable object to bind to.
* @returns {FrameNode} The swatch frame.
*/
async function createColorSwatch(parent, varName, variable) {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const swatchFrame = figma.createFrame();
swatchFrame.name = `Swatch/${varName}`;
swatchFrame.layoutMode = 'VERTICAL';
swatchFrame.primaryAxisAlignItems = 'MIN';
swatchFrame.counterAxisAlignItems = 'MIN';
swatchFrame.itemSpacing = 6;
swatchFrame.layoutSizingHorizontal = 'FIXED';
swatchFrame.layoutSizingVertical = 'HUG';
swatchFrame.resize(88, 1);
swatchFrame.fills = [];
// Color rectangle — bound to variable
const rect = figma.createRectangle();
rect.resize(88, 88);
rect.cornerRadius = 8;
const paint = figma.variables.setBoundVariableForPaint(
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
'color',
variable
);
rect.fills = [paint];
swatchFrame.appendChild(rect);
// Name label
const label = figma.createText();
label.fontName = { family: 'Inter', style: 'Regular' };
label.characters = varName.split('/').pop(); // show leaf name only
label.fontSize = 10;
label.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
label.layoutSizingHorizontal = 'FILL';
swatchFrame.appendChild(label);
// Full path tooltip label (smaller, lighter)
const pathLabel = figma.createText();
pathLabel.fontName = { family: 'Inter', style: 'Regular' };
pathLabel.characters = varName;
pathLabel.fontSize = 9;
pathLabel.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }];
pathLabel.layoutSizingHorizontal = 'FILL';
swatchFrame.appendChild(pathLabel);
parent.appendChild(swatchFrame);
return swatchFrame;
}
```
### Color section builder (primitives row + semantic grid)
```javascript
/**
* Creates a complete color documentation section with a section heading,
* a row of primitive swatches, and a grid of semantic swatches.
*
* @param {FrameNode} root - The root vertical stack frame.
* @param {Variable[]} primitiveVars - Variables from the Primitives collection.
* @param {Variable[]} semanticVars - Variables from the semantic Color collection.
*/
async function createColorSection(root, primitiveVars, semanticVars) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
// Section container
const section = figma.createFrame();
section.name = 'Section/Colors';
section.layoutMode = 'VERTICAL';
section.itemSpacing = 24;
section.layoutSizingHorizontal = 'FILL';
section.layoutSizingVertical = 'HUG';
section.fills = [];
root.appendChild(section);
// Section heading
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Colors';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
// Description
const desc = figma.createText();
desc.fontName = { family: 'Inter', style: 'Regular' };
desc.characters = 'Primitive color palette and semantic color tokens. Semantic tokens reference primitives — always use semantic tokens in components.';
desc.fontSize = 14;
desc.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }];
desc.layoutSizingHorizontal = 'FILL';
section.appendChild(desc);
// Primitive swatches row
const primLabel = figma.createText();
primLabel.fontName = { family: 'Inter', style: 'Bold' };
primLabel.characters = 'Primitives';
primLabel.fontSize = 13;
primLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
section.appendChild(primLabel);
const primRow = figma.createFrame();
primRow.name = 'Primitives/Row';
primRow.layoutMode = 'HORIZONTAL';
primRow.itemSpacing = 12;
primRow.layoutSizingHorizontal = 'FILL';
primRow.layoutSizingVertical = 'HUG';
primRow.fills = [];
primRow.layoutWrap = 'WRAP';
section.appendChild(primRow);
for (const v of primitiveVars) {
await createColorSwatch(primRow, v.name, v);
}
// Semantic swatches grid
if (semanticVars.length > 0) {
const semLabel = figma.createText();
semLabel.fontName = { family: 'Inter', style: 'Bold' };
semLabel.characters = 'Semantic';
semLabel.fontSize = 13;
semLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
section.appendChild(semLabel);
const semRow = figma.createFrame();
semRow.name = 'Semantic/Row';
semRow.layoutMode = 'HORIZONTAL';
semRow.itemSpacing = 12;
semRow.layoutSizingHorizontal = 'FILL';
semRow.layoutSizingVertical = 'HUG';
semRow.fills = [];
semRow.layoutWrap = 'WRAP';
section.appendChild(semRow);
for (const v of semanticVars) {
await createColorSwatch(semRow, v.name, v);
}
}
return section;
}
```
---
## 4. Type Specimens
Typography specimens show each text style rendered at its actual size with a sample string, the style name, and its specifications.
### Single type specimen row
```javascript
/**
* Creates a single type specimen row: style name (small label) + sample text +
* specification line (family · style · size · line-height).
*
* @param {FrameNode} parent - The parent vertical stack.
* @param {string} styleName - The text style name (e.g. "Display Large").
* @param {string} fontFamily - Font family (e.g. "Inter").
* @param {string} fontStyle - Font style (e.g. "Bold").
* @param {number} fontSize - Font size in pixels.
* @param {number} lineHeight - Line height in pixels.
* @returns {FrameNode} The specimen row frame.
*/
async function createTypeSpecimen(parent, styleName, fontFamily, fontStyle, fontSize, lineHeight) {
await figma.loadFontAsync({ family: fontFamily, style: fontStyle });
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const row = figma.createFrame();
row.name = `Type/${styleName}`;
row.layoutMode = 'VERTICAL';
row.itemSpacing = 6;
row.paddingTop = 16;
row.paddingBottom = 16;
row.layoutSizingHorizontal = 'FILL';
row.layoutSizingVertical = 'HUG';
row.fills = [];
parent.appendChild(row);
// Style name label (small, muted)
const nameText = figma.createText();
nameText.fontName = { family: 'Inter', style: 'Medium' };
nameText.characters = styleName;
nameText.fontSize = 11;
nameText.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
nameText.layoutSizingHorizontal = 'FILL';
row.appendChild(nameText);
// Sample text rendered in the actual style
const specimen = figma.createText();
specimen.fontName = { family: fontFamily, style: fontStyle };
specimen.characters = 'The quick brown fox jumps over the lazy dog';
specimen.fontSize = fontSize;
specimen.lineHeight = { value: lineHeight, unit: 'PIXELS' };
specimen.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
specimen.layoutSizingHorizontal = 'FILL';
row.appendChild(specimen);
// Specification line
const specs = figma.createText();
specs.fontName = { family: 'Inter', style: 'Regular' };
specs.characters = `${fontFamily} ${fontStyle} · ${fontSize}px · ${lineHeight}px line height`;
specs.fontSize = 11;
specs.fills = [{ type: 'SOLID', color: { r: 0.65, g: 0.65, b: 0.65 } }];
specs.layoutSizingHorizontal = 'FILL';
row.appendChild(specs);
// Divider line
const divider = figma.createRectangle();
divider.resize(1280, 1);
divider.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
divider.layoutSizingHorizontal = 'FILL';
row.appendChild(divider);
return row;
}
```
### Typography section builder
```javascript
/**
* Creates a complete typography documentation section.
* Pass an array of style definitions; the function renders one specimen per entry.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, family, style, size, lineHeight}>} typeStyles - Style definitions.
*/
async function createTypographySection(root, typeStyles) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createFrame();
section.name = 'Section/Typography';
section.layoutMode = 'VERTICAL';
section.itemSpacing = 0;
section.layoutSizingHorizontal = 'FILL';
section.layoutSizingVertical = 'HUG';
section.fills = [];
root.appendChild(section);
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Typography';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
for (const ts of typeStyles) {
await createTypeSpecimen(section, ts.name, ts.family, ts.style, ts.size, ts.lineHeight);
}
return section;
}
```
---
## 5. Spacing Bars
Spacing bars show each spacing token as a filled rectangle whose width equals the spacing value. Shorter bars for small values, longer bars for large values — the visual encoding is immediate.
### Spacing bar row
```javascript
/**
* Creates a single spacing bar: a colored rectangle sized to the spacing value,
* with a label showing name + pixel value + code syntax.
*
* @param {FrameNode} parent - Parent vertical stack.
* @param {string} name - Token name (e.g. "spacing/sm").
* @param {number} value - Spacing value in pixels.
* @param {Variable} variable - Figma Variable to bind the width to.
* @param {string} codeSyntax - CSS variable string (e.g. "var(--spacing-sm)").
*/
async function createSpacingBar(parent, name, value, variable, codeSyntax) {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const row = figma.createFrame();
row.name = `Spacing/${name}`;
row.layoutMode = 'HORIZONTAL';
row.counterAxisAlignItems = 'CENTER';
row.itemSpacing = 16;
row.layoutSizingHorizontal = 'FILL';
row.layoutSizingVertical = 'HUG';
row.fills = [];
parent.appendChild(row);
// The bar rectangle — width bound to spacing variable
const bar = figma.createRectangle();
bar.resize(value, 16);
bar.cornerRadius = 3;
bar.fills = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98 } }];
// Bind width to the spacing variable so it reflects the actual token value
if (variable) {
bar.setBoundVariable('width', variable);
}
row.appendChild(bar);
// Label: "spacing/sm 8px var(--spacing-sm)"
const label = figma.createText();
label.fontName = { family: 'Inter', style: 'Regular' };
label.characters = `${name} ${value}px ${codeSyntax}`;
label.fontSize = 12;
label.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
row.appendChild(label);
return row;
}
```
### Spacing section builder
```javascript
/**
* Creates the full spacing documentation section.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, value, variable, codeSyntax}>} spacingTokens - Token definitions.
*/
async function createSpacingSection(root, spacingTokens) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createFrame();
section.name = 'Section/Spacing';
section.layoutMode = 'VERTICAL';
section.itemSpacing = 12;
section.layoutSizingHorizontal = 'FILL';
section.layoutSizingVertical = 'HUG';
section.fills = [];
root.appendChild(section);
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Spacing';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
for (const tok of spacingTokens) {
await createSpacingBar(section, tok.name, tok.value, tok.variable, tok.codeSyntax);
}
return section;
}
```
---
## 6. Shadow Cards (Elevation)
Elevation documentation shows cards with progressively stronger drop shadows, labeled with name and effect parameters.
### Single shadow card
```javascript
/**
* Creates a shadow card: a white rectangle with a drop shadow effect,
* labeled with the elevation name and shadow parameters.
*
* @param {FrameNode} parent - The horizontal row to append to.
* @param {string} name - Elevation name (e.g. "Shadow/Medium").
* @param {DropShadowEffect[]} effects - Array of Figma effect objects.
*/
async function createShadowCard(parent, name, effects) {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
const card = figma.createFrame();
card.name = `ShadowCard/${name}`;
card.layoutMode = 'VERTICAL';
card.primaryAxisAlignItems = 'CENTER';
card.counterAxisAlignItems = 'CENTER';
card.itemSpacing = 8;
card.paddingTop = 16;
card.paddingBottom = 16;
card.resize(120, 120);
card.cornerRadius = 8;
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
card.effects = effects;
parent.appendChild(card);
// Elevation name
const nameLabel = figma.createText();
nameLabel.fontName = { family: 'Inter', style: 'Medium' };
nameLabel.characters = name.split('/').pop();
nameLabel.fontSize = 12;
nameLabel.textAlignHorizontal = 'CENTER';
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.2, b: 0.2 } }];
card.appendChild(nameLabel);
// Effect parameters as small text
if (effects.length > 0) {
const e = effects[0];
if (e.type === 'DROP_SHADOW') {
const params = figma.createText();
params.fontName = { family: 'Inter', style: 'Regular' };
params.characters = `x:${e.offset.x} y:${e.offset.y}\nblur:${e.radius}`;
params.fontSize = 10;
params.textAlignHorizontal = 'CENTER';
params.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
card.appendChild(params);
}
}
return card;
}
```
### Shadow section builder
```javascript
/**
* Creates the full elevation/shadow documentation section.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, effects}>} shadowTokens - Shadow definitions.
*/
async function createShadowSection(root, shadowTokens) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createFrame();
section.name = 'Section/Elevation';
section.layoutMode = 'VERTICAL';
section.itemSpacing = 24;
section.layoutSizingHorizontal = 'FILL';
section.layoutSizingVertical = 'HUG';
section.fills = [];
root.appendChild(section);
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Elevation';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
// Cards row — extra top padding so shadows are visible
const row = figma.createFrame();
row.name = 'Elevation/Row';
row.layoutMode = 'HORIZONTAL';
row.itemSpacing = 32;
row.paddingTop = 24;
row.paddingBottom = 40;
row.layoutSizingHorizontal = 'FILL';
row.layoutSizingVertical = 'HUG';
row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }];
row.cornerRadius = 8;
row.paddingLeft = 24;
row.paddingRight = 24;
section.appendChild(row);
for (const tok of shadowTokens) {
await createShadowCard(row, tok.name, tok.effects);
}
return section;
}
```
---
## 7. Border Radius Demo
Border radius documentation shows rectangles at each corner radius value, labeled with the token name and pixel value.
### Single radius card
```javascript
/**
* Creates a single border radius card: a square with corner radius applied,
* labeled with the token name and pixel value.
*
* @param {FrameNode} parent - The horizontal row to append to.
* @param {string} name - Token name (e.g. "radius/md").
* @param {number} value - Corner radius in pixels (0 for none, 9999 for full).
* @param {Variable} [variable] - Optional Figma Variable to bind corner radius.
*/
async function createRadiusCard(parent, name, value, variable) {
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
const wrapper = figma.createFrame();
wrapper.name = `Radius/${name}`;
wrapper.layoutMode = 'VERTICAL';
wrapper.primaryAxisAlignItems = 'CENTER';
wrapper.counterAxisAlignItems = 'CENTER';
wrapper.itemSpacing = 8;
wrapper.fills = [];
wrapper.layoutSizingHorizontal = 'FIXED';
wrapper.layoutSizingVertical = 'HUG';
wrapper.resize(96, 1);
parent.appendChild(wrapper);
const rect = figma.createRectangle();
rect.resize(72, 72);
rect.fills = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98, a: 0.15 } }];
rect.strokes = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98 } }];
rect.strokeWeight = 1.5;
// Cap display value — 9999 is how Figma represents "full/pill"
const displayRadius = Math.min(value, 36);
rect.cornerRadius = displayRadius;
// Bind to variable if provided
if (variable) {
rect.setBoundVariable('cornerRadius', variable);
}
wrapper.appendChild(rect);
const nameLabel = figma.createText();
nameLabel.fontName = { family: 'Inter', style: 'Medium' };
nameLabel.characters = name.split('/').pop();
nameLabel.fontSize = 11;
nameLabel.textAlignHorizontal = 'CENTER';
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.2, b: 0.2 } }];
wrapper.appendChild(nameLabel);
const valueLabel = figma.createText();
valueLabel.fontName = { family: 'Inter', style: 'Regular' };
valueLabel.characters = value >= 9999 ? 'full' : `${value}px`;
valueLabel.fontSize = 10;
valueLabel.textAlignHorizontal = 'CENTER';
valueLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
wrapper.appendChild(valueLabel);
return wrapper;
}
```
### Radius section builder
```javascript
/**
* Creates the full border radius documentation section.
*
* @param {FrameNode} root - Root vertical stack.
* @param {Array<{name, value, variable}>} radiusTokens - Radius token definitions.
*/
async function createRadiusSection(root, radiusTokens) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
const section = figma.createFrame();
section.name = 'Section/Radius';
section.layoutMode = 'VERTICAL';
section.itemSpacing = 24;
section.layoutSizingHorizontal = 'FILL';
section.layoutSizingVertical = 'HUG';
section.fills = [];
root.appendChild(section);
const heading = figma.createText();
heading.fontName = { family: 'Inter', style: 'Bold' };
heading.characters = 'Border Radius';
heading.fontSize = 32;
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
section.appendChild(heading);
const row = figma.createFrame();
row.name = 'Radius/Row';
row.layoutMode = 'HORIZONTAL';
row.itemSpacing = 24;
row.paddingTop = 24;
row.paddingBottom = 24;
row.paddingLeft = 24;
row.paddingRight = 24;
row.layoutSizingHorizontal = 'FILL';
row.layoutSizingVertical = 'HUG';
row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }];
row.cornerRadius = 8;
section.appendChild(row);
for (const tok of radiusTokens) {
await createRadiusCard(row, tok.name, tok.value, tok.variable);
}
return section;
}
```
---
## 8. Documentation Alongside Components
Each component page should include a documentation frame directly on the canvas, placed to the left of the component set. This keeps docs and the component in sync without requiring a separate file.
### Component page documentation frame
```javascript
/**
* Creates the documentation frame for a component page: title, description,
* and usage notes, positioned at x=0 with the component set to its right.
*
* @param {PageNode} page - The component page (must already be current).
* @param {string} componentName - The component name.
* @param {string} description - What the component does and when to use it.
* @param {string[]} usageNotes - Bullet points for usage guidance.
* @returns {FrameNode} The documentation frame.
*/
async function createComponentDocFrame(page, componentName, description, usageNotes) {
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
const doc = figma.createFrame();
doc.name = '_Doc';
doc.layoutMode = 'VERTICAL';
doc.itemSpacing = 16;
doc.paddingTop = 40;
doc.paddingBottom = 40;
doc.paddingLeft = 40;
doc.paddingRight = 40;
doc.layoutSizingHorizontal = 'FIXED';
doc.layoutSizingVertical = 'HUG';
doc.resize(360, 1);
doc.fills = [];
doc.x = 0;
doc.y = 0;
page.appendChild(doc);
// Component name — large heading
const title = figma.createText();
title.fontName = { family: 'Inter', style: 'Bold' };
title.characters = componentName;
title.fontSize = 28;
title.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
title.layoutSizingHorizontal = 'FILL';
doc.appendChild(title);
// Description
const descText = figma.createText();
descText.fontName = { family: 'Inter', style: 'Regular' };
descText.characters = description;
descText.fontSize = 13;
descText.lineHeight = { value: 20, unit: 'PIXELS' };
descText.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
descText.layoutSizingHorizontal = 'FILL';
doc.appendChild(descText);
// Divider
const divider = figma.createRectangle();
divider.resize(280, 1);
divider.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
divider.layoutSizingHorizontal = 'FILL';
doc.appendChild(divider);
// Usage notes
if (usageNotes.length > 0) {
const usageHeading = figma.createText();
usageHeading.fontName = { family: 'Inter', style: 'Bold' };
usageHeading.characters = 'Usage';
usageHeading.fontSize = 13;
usageHeading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
doc.appendChild(usageHeading);
for (const note of usageNotes) {
const noteText = figma.createText();
noteText.fontName = { family: 'Inter', style: 'Regular' };
noteText.characters = `• ${note}`;
noteText.fontSize = 12;
noteText.lineHeight = { value: 18, unit: 'PIXELS' };
noteText.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }];
noteText.layoutSizingHorizontal = 'FILL';
doc.appendChild(noteText);
}
}
return doc;
}
```
---
## 9. Critical Rules
1. **Bind swatches to variables** — use `setBoundVariableForPaint` for color fills, `setBoundVariable('width', ...)` for spacing bars, and `setBoundVariable('cornerRadius', ...)` for radius cards. Never hardcode values that have corresponding variables.
2. **Foundations page comes before component pages** — always insert it between the file structure separators and the first component page.
3. **Show both primitive and semantic layers** — if the system has a Primitives collection and a semantic Color collection, document both on the Foundations page with clear section labels.
4. **Page frame width = 1440px** — this is the convention across Simple DS, Polaris, and Material 3. Use it unless you detect a different existing convention via `get_metadata`.
5. **Section spacing = 64–80px** — the gap between color / typography / spacing / shadow / radius sections should be at minimum 64px so the page is scannable.
6. **Match existing page style** — if the target file uses emoji page name prefixes or a decorative separator style, carry that through to the Foundations page name.
7. **Include code syntax in labels** — where variables have code syntax set, display the CSS variable name in the swatch/bar label so developers can copy it directly.
@@ -0,0 +1,514 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Error Recovery Reference
Protocol for handling failures and incomplete runs across a 20–100+ call design system build.
---
## 1. Core Protocol: STOP → Inspect → Fix → Retry
**`use_figma` is atomic — a failed script does not execute.** If a script errors, no changes are made to the file. There are no partial nodes or half-built state from the failed call itself. Retrying after a fix is safe.
However, in multi-step workflows (20–100+ calls), **previously successful calls** will have created state that persists. If a workflow is abandoned mid-way, nodes from earlier successful calls remain in the file. The cleanup and idempotency patterns in this document handle that scenario.
The recovery sequence for a failed script:
```
1. STOP — Do not run any more use_figma writes.
2. INSPECT — Read the error message carefully. Optionally call get_metadata or get_screenshot to understand the current file state.
3. FIX — Correct the script that failed.
4. RETRY — Re-run the corrected script.
5. PERSIST — Update the state ledger with the outcome.
```
For **abandoned multi-step workflows** (where you need to roll back nodes from previous *successful* calls), use the cleanup protocol in Section 2.
---
## 2. `sharedPluginData`-Based Cleanup: Why Name Matching is Dangerous
### Why name-prefix matching fails
A cleanup script that deletes "all nodes whose name starts with `Button`" will also delete nodes the user may have created manually with that name, or nodes from a previous approved phase. Name-based cleanup has no way to distinguish "orphan from a failed attempt" from "intentional user node."
Furthermore, variant names (`Size=Medium, Style=Primary, State=Default`) do not have consistent prefixes that are safe to target without also hitting legitimate nodes.
### How `setSharedPluginData` / `getSharedPluginData` works
`sharedPluginData` is a key-value store attached to individual nodes. It persists across sessions and is invisible to the user in the Figma UI. Data is scoped by namespace — we use `'dsb'`. Use three keys:
```javascript
node.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001'); // identifies the build run
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created this node
node.setSharedPluginData('dsb', 'key', 'componentset/button');// unique logical key
// Reading:
const runId = node.getSharedPluginData('dsb', 'run_id'); // returns '' if never set
const key = node.getSharedPluginData('dsb', 'key');
```
`getSharedPluginData` returns `''` (empty string, not null) for unset keys. Always check for `!== ''`.
**Tag every created node immediately after creation** — this enables safe cleanup if the multi-step workflow is abandoned later. Tag in the same statement sequence as creation:
```javascript
const comp = figma.createComponent();
comp.setSharedPluginData('dsb', 'run_id', RUN_ID); // tag immediately
comp.setSharedPluginData('dsb', 'key', key); // tag immediately
// ... then do the rest of the setup
```
### Complete `cleanupOrphans` script using `run_id`
This script finds all nodes tagged with a given `run_id` and optionally a `phase` filter, then removes them. Run it on the specific page where the failure occurred.
```javascript
const TARGET_RUN_ID = 'ds-build-2024-001'; // run ID to clean
const TARGET_PHASE = 'phase3'; // optionally filter by phase ('' = all phases)
const PAGE_NAME = 'Button'; // page to clean (or null for all pages)
const pagesToSearch = PAGE_NAME
? [figma.root.children.find(p => p.name === PAGE_NAME)].filter(Boolean)
: figma.root.children;
const removed = [];
const skipped = [];
for (const page of pagesToSearch) {
await figma.setCurrentPageAsync(page);
const orphans = page.findAll(node => {
const runId = node.getSharedPluginData('dsb', 'run_id');
if (runId !== TARGET_RUN_ID) return false;
if (TARGET_PHASE && node.getSharedPluginData('dsb', 'phase') !== TARGET_PHASE) return false;
return true;
});
// Remove leaf-first to avoid removing parents before children
// Sort by depth (deepest first) to avoid double-remove errors
const sorted = orphans.slice().sort((a, b) => {
let depthA = 0, depthB = 0;
let n = a; while (n.parent) { depthA++; n = n.parent; }
n = b; while (n.parent) { depthB++; n = n.parent; }
return depthB - depthA;
});
for (const node of sorted) {
try {
if (node.removed) continue; // already removed (was a child of removed parent)
node.remove();
removed.push({ id: node.id, name: node.name, key: node.getSharedPluginData('dsb', 'key') });
} catch (e) {
skipped.push({ id: node.id, name: node.name, error: e.message });
}
}
}
return { removed: removed.length, skipped: skipped.length, details: removed };
```
After running cleanup, call `get_metadata` on the target page to confirm the orphaned nodes are gone before retrying.
---
## 3. Idempotency Patterns: Check-Before-Create
Run an idempotency check at the start of every create operation. If the entity already exists (tagged with the expected `key`), skip creation and return the existing ID.
### Check-before-create for a variable collection
```javascript
const KEY = 'collection/color';
const RUN_ID = 'ds-build-2024-001';
const COLLECTION_NAME = 'Color';
// Check: does a collection tagged with this key already exist?
const allCollections = await figma.variables.getLocalVariableCollectionsAsync();
// Variables/collections support sharedPluginData too — check by name as fallback
// Note: VariableCollection sharedPluginData is set via collection.setSharedPluginData(...)
const existing = allCollections.find(c =>
c.getSharedPluginData('dsb', 'key') === KEY
);
if (existing) {
return {
collectionId: existing.id,
modeIds: existing.modes.map(m => ({ name: m.name, id: m.modeId })),
alreadyExisted: true,
};
}
// Create fresh
const collection = figma.variables.createVariableCollection(COLLECTION_NAME);
collection.setSharedPluginData('dsb', 'run_id', RUN_ID);
collection.setSharedPluginData('dsb', 'key', KEY);
// Rename default mode, add second mode
collection.renameMode(collection.modes[0].modeId, 'Light');
const darkModeId = collection.addMode('Dark');
return {
collectionId: collection.id,
modeIds: [
{ name: 'Light', id: collection.modes[0].modeId },
{ name: 'Dark', id: darkModeId },
],
};
```
### Check-before-create for a page
```javascript
const KEY = 'page/button';
const PAGE_NAME = 'Button';
const RUN_ID = 'ds-build-2024-001';
// Check by sharedPluginData key first, then by name as fallback
let page = figma.root.children.find(p => p.getSharedPluginData('dsb', 'key') === KEY);
if (!page) {
page = figma.root.children.find(p => p.name === PAGE_NAME);
}
if (page) {
// Ensure it's tagged if it was found by name only
if (!page.getSharedPluginData('dsb', 'key')) {
page.setSharedPluginData('dsb', 'run_id', RUN_ID);
page.setSharedPluginData('dsb', 'key', KEY);
}
return { pageId: page.id, alreadyExisted: true };
}
page = figma.createPage();
page.name = PAGE_NAME;
page.setSharedPluginData('dsb', 'run_id', RUN_ID);
page.setSharedPluginData('dsb', 'key', KEY);
return { pageId: page.id, alreadyExisted: false };
```
### Check-before-create for a component set
```javascript
const KEY = 'componentset/button';
const PAGE_ID = 'PAGE_ID_FROM_STATE';
const RUN_ID = 'ds-build-2024-001';
const page = await figma.getNodeByIdAsync(PAGE_ID);
await figma.setCurrentPageAsync(page);
const existing = page.findAll(n =>
n.type === 'COMPONENT_SET' && n.getSharedPluginData('dsb', 'key') === KEY
);
if (existing.length > 0) {
return {
componentSetId: existing[0].id,
alreadyExisted: true,
};
}
// ... proceed with creation
return { componentSetId: null, alreadyExisted: false };
```
---
## 4. State Ledger
### JSON Schema
Maintain a state ledger in your context (not in the Figma file) across calls. This is your source of truth for node IDs, completed steps, and pending validations.
```json
{
"runId": "ds-build-2024-001",
"phase": "phase3",
"step": "component-button/combine-variants",
"completedSteps": [
"phase0",
"phase1/collections",
"phase1/primitives",
"phase1/semantics",
"phase2/pages",
"phase2/foundations-docs",
"phase3/component-avatar",
"phase3/component-icon"
],
"entities": {
"collections": {
"primitives": "VariableCollectionId:1234:5678",
"color": "VariableCollectionId:1234:5679",
"spacing": "VariableCollectionId:1234:5680"
},
"variables": {
"color/bg/primary": "VariableId:2345:1",
"color/bg/secondary": "VariableId:2345:2",
"color/bg/disabled": "VariableId:2345:3",
"color/text/on-primary": "VariableId:2345:4",
"color/text/on-secondary": "VariableId:2345:5",
"color/text/disabled": "VariableId:2345:6",
"spacing/sm": "VariableId:2345:7",
"spacing/md": "VariableId:2345:8",
"spacing/lg": "VariableId:2345:9",
"radius/md": "VariableId:2345:10"
},
"modes": {
"color/light": "2345:1",
"color/dark": "2345:2"
},
"pages": {
"Cover": "0:1",
"Foundations": "0:2",
"Button": "0:3"
},
"components": {
"Icon": "3456:1",
"Avatar": "3456:2",
"Button": "3456:3"
},
"componentSets": {
"Button": "4567:1"
}
},
"pendingValidations": [
"Button:metadata",
"Button:screenshot"
],
"userCheckpoints": {
"phase0": "approved-2024-01-15",
"phase1": "approved-2024-01-15",
"phase2": "approved-2024-01-15",
"component-avatar": "approved-2024-01-15"
}
}
```
### Persisting between calls
After every successful `use_figma` call:
1. Extract all IDs from the return value
2. Add them to the appropriate `entities` section of the ledger
3. Add the completed step to `completedSteps`
4. Remove from `pendingValidations` if this call validated something
5. Update `phase` and `step` to the current position
### Rehydrating at session start
If a conversation is interrupted and resumed, read the state ledger and verify key entities still exist:
```javascript
// Verify that critical nodes from the ledger still exist
const toVerify = {
'color-collection': 'VariableCollectionId:1234:5679',
'button-page': '0:3',
'button-componentset': '4567:1',
};
const results = {};
for (const [label, id] of Object.entries(toVerify)) {
const node = await figma.getNodeByIdAsync(id)
.catch(() => null);
results[label] = node ? { found: true, name: node.name } : { found: false };
}
return results;
```
If any entity is missing, treat the phase that created it as incomplete and re-run from that checkpoint.
---
## 5. Resume Protocol
### Step 1: Inspect the file for `run_id` tags
```javascript
const TARGET_RUN_ID = 'ds-build-2024-001';
const inventory = { pages: [], variables: [], componentSets: [], frames: [] };
// Scan pages
for (const page of figma.root.children) {
if (page.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
inventory.pages.push({ id: page.id, name: page.name, key: page.getSharedPluginData('dsb', 'key') });
}
}
// Scan variables
const allVars = await figma.variables.getLocalVariablesAsync();
for (const v of allVars) {
if (v.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
inventory.variables.push({ id: v.id, name: v.name, key: v.getSharedPluginData('dsb', 'key') });
}
}
// Scan all component sets and frames on each page
for (const page of figma.root.children) {
await figma.setCurrentPageAsync(page);
const nodes = page.findAll(n => n.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID);
for (const n of nodes) {
if (n.type === 'COMPONENT_SET') {
inventory.componentSets.push({ id: n.id, name: n.name, key: n.getSharedPluginData('dsb', 'key') });
} else if (n.type === 'FRAME') {
inventory.frames.push({ id: n.id, name: n.name, key: n.getSharedPluginData('dsb', 'key') });
}
}
}
return inventory;
```
### Step 2: Reconstruct state from inventory
Map the inventory keys back to the state ledger schema. For each entity found with a `key`, add its ID to the appropriate section. Mark the corresponding step as `completedSteps`.
Example mapping:
```
key: 'collection/color' → entities.collections.color
key: 'variable/color/bg/primary' → entities.variables['color/bg/primary']
key: 'page/button' → entities.pages.Button
key: 'componentset/button' → entities.componentSets.Button
```
### Step 3: Identify the resume point
The resume point is the first step in the workflow that is NOT in `completedSteps`. If the inventory shows the Button component set exists but the pending validations list shows `'Button:screenshot'`, the resume point is the screenshot validation call, not re-creation.
Use the checkpoint table from the workflow to determine which phase to continue from:
```
Phase 0 complete: all planned pages listed in entities.pages
Phase 1 complete: all planned variables listed in entities.variables with correct scopes
Phase 2 complete: all structural pages + foundations doc frames present
Phase 3 complete (per component): componentSet exists + no pending validations + user checkpoint recorded
```
---
## 6. Failure Taxonomy
### Recoverable Errors
These can be fixed and retried without affecting already-created entities:
| Category | Examples | Recovery |
|---|---|---|
| Layout errors | Variants stacked at (0,0), wrong padding values | Re-run the positioning step only |
| Naming issues | Typo in variant name, wrong casing | Find nodes by `dsb_key`, update `name` property |
| Missing property wiring | `componentPropertyReferences` not set | Find component set by ID, re-run the property wiring step |
| Variable binding omission | A fill was hardcoded instead of bound | Find nodes by `dsb_key`, re-bind the fill |
| Wrong variable bound | Bound to wrong variable ID | Re-bind with correct variable ID |
| Text not visible | Font not loaded before text write | Re-run text creation with `loadFontAsync` first |
| Script timeout | Script exceeded time limit before completing | Script is atomic — nothing was created. Reduce scope (fewer nodes per call) and retry |
### Structural Corruption (Requires Rollback or Restart)
These errors leave the file in a state where continuing forward is unreliable:
| Category | Examples | Recovery |
|---|---|---|
| Component cycle | A component instance was accidentally nested inside itself | Full cleanup of the affected component, restart that component from Call 1 |
| combineAsVariants with non-components | Mixed node types passed to combineAsVariants, causing unexpected merges | Remove the malformed component set, re-run from variant creation |
| Variable collection ID drift | Collection was deleted and re-created, old IDs in state ledger are stale | Re-run Phase 1 completely; update all IDs in state ledger |
| Page deletion | A page was deleted after component sets were created on it | Treat as Phase 2 incomplete; re-create the page + re-run affected component creations |
| Mode limit exceeded | `addMode` threw because the plan is Starter or Professional | Redesign variable collection architecture to fit mode limits, restart Phase 1 |
**Recovery from structural corruption**: run `cleanupOrphans` for the entire run ID, then restart from the affected phase. Do NOT attempt to patch corrupted structure in-place.
---
## 7. Common Error Table
| Error message | Likely cause | Fix |
|---|---|---|
| `"Cannot create component from node"` | Tried to call `createComponentFromNode` on a node inside a component | Create a fresh component instead: `figma.createComponent()` |
| `"in addMode: Limited to N modes only"` | Plan mode limit hit (Starter=1, Professional=4) | Redesign to use fewer modes or upgrade plan |
| `"setCurrentPageAsync: page does not exist"` | Page was deleted or wrong ID | Re-create the page using the idempotency pattern |
| `"Cannot read properties of null"` | `getNodeByIdAsync` returned null — node was deleted | Run the resume protocol to find what exists, update state ledger |
| `"Expected nodes to be component nodes"` | Passed a non-ComponentNode to `combineAsVariants` | Filter the array: `nodes.filter(n => n.type === 'COMPONENT')` |
| `"in createVariable: Cannot create variable"` | Collection was deleted or ID is wrong | Verify collection exists with `getVariableCollectionByIdAsync` |
| `"font not loaded"` | Called a text property setter without `loadFontAsync` first | Add `await figma.loadFontAsync({ family, style })` before the text operation |
| `"Cannot set properties of a read-only array"` | Tried to mutate fills/strokes in-place | Clone first: `const fills = JSON.parse(JSON.stringify(node.fills))` |
| `"Expected RGBA color"` | Color value out of 0–1 range | Divide RGB 0–255 values by 255: `{ r: 65/255, g: 85/255, b: 143/255 }` |
| `"Cannot add children to a non-parent node"` | Tried to append a child to a leaf node (text, rect) | Ensure the parent is a FrameNode, ComponentNode, or GroupNode |
| `"in combineAsVariants: nodes must be in the same parent"` | Components are on different pages | Move all components to the same page before combining |
| `"Script exceeded time limit"` | Loop creating too many nodes in one call | Split the work: create N/2 variants per call |
| Component set deletes itself | Tried to create a component set with no children | `combineAsVariants` requires at least 1 node — always pass 1+ |
| `addComponentProperty` returns unexpected name | This is normal — `BOOLEAN`/`TEXT`/`INSTANCE_SWAP` get `#id:id` suffix | Save the returned key immediately and use that, not the input name |
---
## 8. Per-Phase Recovery Guidance
### Phase 1 fails (variable creation)
Since `use_figma` is atomic, a failed call creates nothing. The most common scenario is that some calls in Phase 1 succeeded (creating some variables) while a later call failed.
Recovery steps:
1. Run inspection script to find all variables tagged with your `run_id`
2. Compare against the plan to identify which variables were successfully created and which are still missing
3. If a successfully created variable has wrong values, call `variable.remove()` and recreate it
4. Fix the failed script and retry — it's safe since the failed call created nothing
5. Do NOT proceed to Phase 2 until ALL planned variables exist with correct scopes and code syntax
**The most common Phase 1 failure:** script timeout when creating many variables. Fix: batch variable creation — create at most 20–30 variables per call.
### Phase 2 fails mid-execution (page/file structure)
Symptoms: some pages exist, others are missing; foundations doc frames are incomplete.
Recovery steps:
1. Identify which pages were successfully created (check for `key` tags)
2. Mark remaining pages as pending and create them in subsequent calls
3. If a foundations doc frame is malformed, run `cleanupOrphans` for `dsb_phase: 'phase2'` on that page, then recreate
Phase 2 failures rarely require Phase 1 rollback unless the page structure itself is corrupted (which is unusual).
### Phase 3 fails (component creation)
This is the most common failure mode in long builds. Since `use_figma` is atomic, a failed call creates nothing — but previous successful calls in the component creation sequence will have created state. Handle by which call in the sequence failed:
```
If failure in Call 1 (page creation):
→ Nothing was created. Fix the script and retry.
If failure in Call 2 (doc frame):
→ Call 1's page exists. Fix Call 2 and retry — idempotency check handles it.
If failure in Call 3 (base component):
→ Calls 1-2 succeeded. Fix Call 3 and retry.
If failure in Call 4 (variant creation):
→ Call 3's base component exists. Fix Call 4 and retry.
→ If you need to restart from Call 3, clean up Call 3's nodes first
using cleanupOrphans scoped to the component page.
If failure in Call 5 (combineAsVariants + layout):
→ Variant ComponentNodes from Call 4 exist but aren't combined yet.
→ Fix Call 5 and retry.
→ If the component set was already created by a prior attempt of Call 5
that succeeded, remove it first, then re-run.
If failure in Call 6 (component properties):
→ The component set already exists and is structurally sound.
→ Fix Call 6 and retry — addComponentProperty is safe to retry if
you first check componentPropertyDefinitions for existing properties.
→ Idempotency check: if 'Label' property already exists, skip addComponentProperty.
```
**Idempotency for component properties (Call 6 retry):**
```javascript
const existingDefs = cs.componentPropertyDefinitions;
const labelKey = existingDefs['Label']
? Object.keys(existingDefs).find(k => k.startsWith('Label'))
: cs.addComponentProperty('Label', 'TEXT', 'Button');
```
### Phase 4 fails mid-execution (QA / Code Connect)
Phase 4 is non-destructive. Failures here do not corrupt Phase 3 work. Common failures:
- **Accessibility audit finds contrast failures:** do not attempt auto-fix. Report the specific variable IDs and token names that fail, then ask the user which value to update.
- **Naming audit finds duplicates:** list all duplicates with their `key` values, ask user which to keep, then remove the duplicates.
- **Code Connect mapping fails:** treat as incomplete, not broken. Continue and leave as pending.
@@ -0,0 +1,527 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Naming Conventions Reference
This reference documents every naming convention used in the figma-generate-library workflow. Cover all naming decisions in order: variables, components, pages, variants, styles, separators, status indicators. The last section explains when to match an existing file's conventions vs. using the defaults here.
---
## 1. Variable Naming
### Slash hierarchy (the universal pattern)
All Figma variables use slash-separated paths. The slash creates visual grouping in the Variables panel and maps directly to the token hierarchy in code.
```
{category}/{subcategory}/{role}
```
Real examples from Simple DS and Material 3:
```
color/bg/primary
color/bg/secondary
color/text/primary
color/text/muted
color/border/default
color/border/focus
color/feedback/error
color/feedback/success
spacing/xs
spacing/sm
spacing/md
spacing/lg
spacing/xl
spacing/2xl
radius/none
radius/sm
radius/md
radius/lg
radius/full
typography/body/font-size
typography/body/line-height
typography/heading/font-size
typography/heading/font-weight
```
### Primitives collection
Primitive variables hold raw values and are **not** exposed to consumers (scope = `[]`). They use a flat `{family}/{step}` format matching the color scale convention from Simple DS:
```
blue/50
blue/100
blue/200
...
blue/900
gray/50
gray/100
...
gray/900
red/500
green/500
```
Step numbers follow the convention of the target codebase. If the codebase uses `100–900`, use that. If it uses `50–950`, use that. If there is no codebase convention, use `100–900` in increments of 100.
### Semantic collection
Semantic variables alias primitives. They use the role-based `{category}/{role}` or `{category}/{subcategory}/{role}` pattern:
```
color/bg/primary → alias: primitives/white (light), primitives/gray/900 (dark)
color/bg/secondary → alias: primitives/gray/100 (light), primitives/gray/800 (dark)
color/text/primary → alias: primitives/gray/900 (light), primitives/white (dark)
color/text/secondary → alias: primitives/gray/600 (light), primitives/gray/400 (dark)
color/border/default → alias: primitives/gray/200 (light), primitives/gray/700 (dark)
```
**Rule:** Semantic variables must never hold raw hex values — they always alias a primitive. If you need a new color value, create the primitive first, then create the semantic alias.
### Casing
**Default:** Use **lowercase** with forward slashes: `color/bg/primary`, `spacing/2xl`.
**When to deviate:**
- If the existing file uses PascalCase (e.g., Material 3 uses `Schemes/Primary`) — match it.
- If the design team prefers PascalCase for readability in the Variables panel — acceptable as long as the code syntax is separately defined and uses the platform-correct case.
- Mode names can use spaces and mixed case (e.g., `SDS Light`, `Mode 1 → Light`) — these are labels, not identifiers.
**Never:** camelCase inside variable names (`colorBgPrimary` as a Figma name is wrong — that belongs in Android code syntax only). Never use spaces inside a path segment: `color/bg primary` is wrong; `color/bg/primary` is correct.
**Key distinction:** The casing rule applies to *Figma variable names*. Code syntax names follow *platform conventions* regardless of the Figma name case — see §9 for the full picture.
---
## 2. Component Naming
### Main components: PascalCase, no prefix
Published components intended for library consumers use plain PascalCase names:
```
Button
Input
Checkbox
Toggle
Avatar
Badge
Card
Dialog
Tooltip
Banner
```
Do not use a namespace prefix for public components (e.g., do not name them `DS/Button` or `sds-Button`). Slashes in component names create nested grouping in the Assets panel, which is correct for sub-components but not for top-level public components.
### Sub-components: underscore prefix + slash namespace
Internal sub-components that are NOT meant for library consumers use the `_` prefix. This hides them from the Assets panel by default and signals to other designers that they should not be used directly.
```
_Button/Slot (internal icon slot for Button)
_Input/Indicator (internal state indicator for Input)
_Badge/Dot (internal dot sub-component of Badge)
_Parts/Avatar.Status (UI3 pattern: _Parts/{ParentName}.{SubPart})
_Slider/Handle (UI3 pattern: _{ParentName}/{SubPart})
```
Pattern rules:
- Use `_` prefix for ALL internal sub-components — no exceptions.
- Use slash namespacing to group sub-components under their parent: `_Button/IconSlot`.
- For sub-components shared by multiple parents, use `_Parts/{ComponentName}.{SubPart}`.
### Private documentation components
Components used only for internal documentation (not for production use) use the `.` prefix:
```
.ExampleCard
.GuidelineHeader
.DemoFrame
```
This hides them from consumers while keeping them accessible on the canvas.
---
## 3. Page Naming
Five reference design systems use three distinct naming patterns. Choose one pattern and apply it consistently across all pages in the file.
### Pattern 1: Plain names (Simple DS, Material 3, Polaris)
The most common pattern. Clean, readable, no decoration.
```
Cover
---
Foundations
Icons
---
Accordion
Avatars
Buttons
Cards
Dialog
Inputs
Menu
---
Utilities
Component Playground
```
Use this pattern when starting from scratch or when the target file already uses this style.
### Pattern 2: Emoji prefix + status (UI3 Library)
The most expressive pattern. The page name encodes asset type, design status, and code readiness.
Anatomy: `[Asset Type Emoji] [Optional FPL Label] [Status Circle] Component Name [Code Status Bracket]`
| Segment | Values |
|---------|--------|
| Asset type | Component pages use the C-flag emoji; pattern pages use the P-flag emoji |
| Design status | Green circle = Ready, Yellow circle = WIP, Red circle = Do not use |
| Code status | (none) = Ready in code, `[beta]` = Beta, `[future]` = Not yet built |
Examples:
```
Overview
Status Key
---
FPL COMPONENTS (go/fpl)
[C-flag] FPL [Green] Buttons
[C-flag] FPL [Green] Inputs
[C-flag] FPL [Yellow] Popovers [future]
---
UI3 COMPONENTS
[C-flag] [Green] Comments
---
PATTERNS
[P-flag] [Green] Editor / Layers
---
[Book] Cover
[Headstone] Deprecated
```
Use this pattern only when building a large, multi-team design system where lifecycle tracking is needed, or when the target file already uses it.
### Pattern 3: Emoji prefix (Shop Minis)
A lighter version of the UI3 pattern without status circles.
```
📔 Cover
ℹ️ About
🚀 Getting started
——— THEME ———
Color
Typography
Spacing
——— COMPONENTS ———
Button
Input
Card
```
Use this pattern when the target file already uses emoji prefixes but does not need lifecycle tracking.
### Universal rules (all patterns)
- **Cover** is always first.
- **Separator pages** come before and after each logical section.
- **Foundation/token pages** always come before component pages.
- **Utility and internal pages** always come last.
- Pick one convention and do not mix patterns within a file.
---
## 4. Variant Naming
### Property=Value format
All component variant properties and their values use `Property=Value` format in the Figma component set:
```
Size=Small, Style=Primary, State=Default
Size=Medium, Style=Secondary, State=Hover
Size=Large, Style=Ghost, State=Disabled
```
Actual property names match code prop names where possible:
| Figma Property | Code Prop Equivalent |
|---------------|---------------------|
| `Size` | `size` |
| `Style` / `Variant` | `variant` |
| `State` | Typically controlled by `:hover`, `:focus`, `:disabled` in CSS, but `state` in some systems |
| `Type` | `type` |
| `Disabled` | `disabled` (boolean) |
| `Icon` | `icon` (boolean or instance swap) |
### Property value casing
Property values use **Title Case** in Figma (to be readable in the Variants panel), mapping to lowercase in code:
| Figma value | Code value |
|-------------|-----------|
| `Small` | `"small"` / `"sm"` |
| `Medium` | `"medium"` / `"md"` |
| `Large` | `"large"` / `"lg"` |
| `Primary` | `"primary"` |
| `Disabled` | `disabled` (boolean prop) |
| `Default` | *(typically the absent/unset case)* |
### Boolean properties
Boolean component properties in Figma use `true` / `false` as values (Figma's native boolean), not `Yes` / `No` or `On` / `Off`.
---
## 5. Style Naming (Text and Effect Styles)
### Text styles: category/name
```
Display/Large
Display/Medium
Display/Small
Heading/1
Heading/2
Heading/3
Body/Large
Body/Medium
Body/Small
Label/Large
Label/Small
Code/Inline
```
The category segment maps to the typographic role. Use the same category names as the codebase's typography scale where possible.
### Effect styles (shadows): category/name
```
Shadow/None
Shadow/Subtle
Shadow/Medium
Shadow/Strong
Shadow/Overlay
Elevation/0
Elevation/1
Elevation/2
Elevation/3
Elevation/4
Elevation/5
```
Use `Shadow/` for named semantic shadows. Use `Elevation/N` for Material Design-style numbered elevation levels.
---
## 6. Separator Pages
Separator pages are empty pages whose sole purpose is to create visual breaks in the Figma page panel. Two conventions:
| Convention | Example | Used by |
|------------|---------|---------|
| Three dashes | `---` | Simple DS, UI3, Polaris, Material 3 |
| Decorated text | `——— COMPONENTS ———` | Shop Minis |
The three-dash convention (`---`) is the most common and the default for new files. Use it unless the target file uses the decorated-text style.
**Where to place separators:**
```
Cover
--- ← after cover
Foundations
Icons
--- ← before components
[component pages]
--- ← before utilities
Utilities
```
---
## 7. Status Indicators (UI3 Emoji System)
The UI3 Library uses colored circle emojis in page names to communicate design readiness at a glance. This system is optional but powerful for large teams.
| Emoji | Meaning | When to use |
|-------|---------|-------------|
| Green circle | Ready / Approved | Design is stable, reviewed, and safe to use |
| Yellow circle | WIP / In Progress | Design is being actively worked on, may change |
| Red circle | Do not use | Not ready, do not reference; may be deprecated |
Code readiness is communicated via brackets appended to the component name:
| Bracket | Meaning |
|---------|---------|
| (none) | Component is implemented in code and stable |
| `[beta]` | Component is in code but not yet stable (~3 weeks from ready) |
| `[future]` | Not yet implemented in code |
**Documentation status (within component pages):**
If building a UI3-style system, each documentation frame gets a status banner with one of these labels:
- `APPROVED` — fully vetted
- `READY FOR REVIEW` — awaiting sign-off
- `WORK IN PROGRESS` — actively being designed
- `NEEDS UPDATE` — outdated, requires revision
- `DO NOT REFERENCE` — should not be used
This system is only recommended for large, multi-team systems where lifecycle tracking provides real value. For smaller systems, skip the emoji status indicators and use plain page names.
---
## 8. When to Match Existing vs. Use Defaults
**Always inspect before naming anything.** Run `get_metadata` or `inspectFileStructure` to discover existing conventions before creating any pages or variables.
### Match the existing file when:
- The file already has pages with a consistent naming pattern (emoji prefixes, separator style, casing).
- The file already has variable collections with an established naming scheme.
- The file was started by a design team and carries intentional decisions.
- Any existing component names use a specific pattern (PascalCase, kebab-case, namespace prefixes).
### Use the defaults from this document when:
- Starting a brand-new Figma file with no existing content.
- The existing conventions are inconsistent (mix of styles = no convention to match).
- The user explicitly asks for a fresh design system following best practices.
### When code and Figma disagree:
If the codebase uses `button-primary` but Figma has a component named `Button`, do not rename the Figma component. Instead:
- Keep the Figma name as `Button` (PascalCase, human-readable).
- Set variable code syntax to match the exact CSS token name from the codebase.
- Set Code Connect source path to the actual code file and use the exact code component name.
**The rule:** Figma names are for designers; code syntax and Code Connect source paths carry the exact code identifiers. These two identity systems operate in parallel.
---
## 9. Figma Variable Names vs Code Names — The Full Picture
This is one of the most misunderstood areas. Figma names and code names follow **different conventions on purpose** — they serve different audiences and live in different environments.
### Why they differ
| | Figma variable name | Code syntax (WEB) |
|---|---|---|
| **Audience** | Designers in the Variables panel | Developers in CSS/Swift/Kotlin |
| **Separator** | `/` (slash) — creates visual grouping in Figma UI | `-` (hyphen) — required by CSS custom property syntax |
| **Case** | lowercase (or PascalCase for display — see below) | kebab-case for CSS; camelCase for JS/Android |
| **Depth** | 2–4 levels | Flat for CSS; dot-notation for JS |
| **Namespace** | Implicit (by collection) | Explicit prefix (`--p-`, `--md-`, `--cds-`) |
### The transformation
```
Figma variable name Code syntax (WEB)
────────────────── ─────────────────
color/bg/primary → var(--color-bg-primary)
spacing/xs → var(--spacing-xs)
radius/md → var(--radius-md)
typography/body/font-size → var(--typography-body-font-size)
Pattern: replace "/" with "-", wrap in var(--)
**CRITICAL: The `var()` wrapper is REQUIRED for WEB code syntax.** Figma expects the full CSS function syntax — not just the property name. If you set `--color-bg-primary` (without `var()`), Dev Mode will show raw hex values instead of the variable reference. Always set `var(--color-bg-primary)`.
```
```
Figma variable name Code syntax (ANDROID)
────────────────── ─────────────────────
color/bg/primary → colorBgPrimary
spacing/xs → spacingXs
radius/md → radiusMd
Pattern: replace "/" with "", capitalize each word after first
```
```
Figma variable name Code syntax (iOS)
────────────────── ─────────────────
color/bg/primary → Color.bgPrimary
spacing/xs → Spacing.xs
radius/md → Radius.md
Pattern: first segment becomes class name, remainder becomes property (camelCase)
```
### Real-world examples from the 5 reference files
| File | Figma variable name | WEB code syntax | ANDROID code syntax |
|------|--------------------|-----------------|--------------------|
| Simple DS | `color/bg/primary` | `var(--color-bg-primary)` | `colorBgPrimary` |
| Simple DS | `spacing/sm` | `var(--spacing-sm)` | `spacingSm` |
| Material 3 | `Schemes/Primary` | `var(--md-sys-color-primary)` | `colorPrimary` |
| Material 3 | `Corner/Extra-small` | `var(--md-sys-shape-corner-extra-small)` | `shapeCornerExtraSmall` |
| Polaris | `color/bg/surface` | `var(--p-color-bg-surface)` | — |
**Key observation from Material 3:** The Figma name `Schemes/Primary` uses PascalCase with a space, but the WEB code syntax is `var(--md-sys-color-primary)` — entirely kebab-case with a vendor prefix `md-sys-`. The Figma name and the code syntax bear almost no resemblance. This is intentional and common in mature design systems.
### Casing in Figma: lowercase is default, PascalCase is valid for display
The guideline to use lowercase is a default, not a universal rule. Evidence from real files:
| File | Figma case | Code output case | Why |
|------|-----------|------------------|-----|
| Simple DS | `color/bg/primary` (lowercase) | `var(--color-bg-primary)` | Direct mapping — simple |
| Material 3 | `Schemes/Primary` (PascalCase) | `var(--md-sys-color-primary)` | PascalCase reads better in Variables panel; code name is independently defined |
| Polaris | `color/bg/surface` (lowercase) | `var(--p-color-bg-surface)` | Direct mapping with vendor prefix |
**Rule:** Use lowercase when the Figma name will map directly to the CSS name. Use PascalCase (or match existing file) when the design system has human-readable variable names that are distinct from the technical code names.
### When the codebase doesn't use CSS custom properties
Some JavaScript-first systems (Chakra, Ant Design, MUI) don't use CSS `var(--...)` at all. Their tokens live in JS theme objects:
```
Chakra: colors.gray[500] → JS: theme.colors.gray[500]
Ant: colorPrimary → JS: token.colorPrimary
MUI: palette.primary.main → JS: theme.palette.primary.main
```
In these cases, set WEB code syntax to the JS property path rather than a CSS variable:
```javascript
// For a JS-object-based system like Chakra:
v.setVariableCodeSyntax('WEB', 'colors.gray.500');
// For Ant Design:
v.setVariableCodeSyntax('WEB', 'colorPrimary');
```
### Hierarchy depth: match the codebase
The number of slash levels should mirror the codebase's nesting depth:
| Codebase pattern | Figma depth | Example |
|-----------------|------------|---------|
| `--primary` (flat) | 1–2 levels | `color/primary` |
| `--color-bg-surface` (3-part) | 3 levels | `color/bg/surface` |
| `--md-sys-color-primary` (vendor + 3-part) | 3 levels (vendor prefix goes in code syntax only) | `color/primary` |
| `theme.palette.primary.main` (4-part) | 3–4 levels | `color/palette/primary/main` |
**Important:** Vendor prefixes (`--p-`, `--md-sys-`, `--cds-`) belong in the **code syntax**, not the Figma variable name. The Figma name `color/bg/surface` + code syntax `var(--p-color-bg-surface)` is the correct pattern.
### Action at discovery time
During Phase 0 discovery, capture both sides of the mapping explicitly:
```
For each token found in the codebase:
CSS variable: --sds-color-background-brand-default
Figma name: color/bg/brand/default (slash hierarchy, no vendor prefix)
WEB syntax: var(--sds-color-background-brand-default) (exact CSS name)
ANDROID syntax: sdsColorBackgroundBrandDefault (camelCase)
iOS syntax: Color.backgroundBrandDefault (dot-notation)
```
Store this mapping in the state ledger. Use it when calling `setVariableCodeSyntax` in Phase 1. Never derive the code syntax from the Figma name if you have the original CSS variable name — always use the original.
@@ -0,0 +1,888 @@
> Part of the [figma-generate-library skill](../SKILL.md).
# Token Creation Reference
This document covers Phase 1: creating variable collections, modes, primitives, semantic aliases, scopes, code syntax, styles, and validation. All code is copy-paste ready for `use_figma`.
---
## 1. Collection Architecture
Choose the pattern that matches your token count and complexity:
### Simple Pattern (< 50 tokens)
One collection, 2 modes. Appropriate for small projects or brand kits.
```
Collection: "Tokens" modes: ["Light", "Dark"]
color/bg/primary → Light: #FFFFFF, Dark: #1A1A1A
spacing/sm = 8
```
### Standard Pattern (50–200 tokens) — Recommended Starting Point
Separate primitives from semantics. The real-world reference is Figma's Simple Design System (SDS): 7 collections, 368 variables, light/dark modes on semantic colors, single-mode primitives.
```
Collection: "Primitives" modes: ["Value"] ← raw hex values, no modes
blue/500 = #3B82F6
gray/900 = #111827
white/1000 = #FFFFFF
Collection: "Color" modes: ["Light", "Dark"] ← aliases to Primitives
color/bg/primary → Light: alias Primitives/white/1000, Dark: alias Primitives/gray/900
color/text/primary → Light: alias Primitives/gray/900, Dark: alias Primitives/white/1000
Collection: "Spacing" modes: ["Value"]
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, spacing/lg = 24, spacing/xl = 32
Collection: "Typography Primitives" modes: ["Value"]
family/sans = "Inter", scale/01 = 12, scale/02 = 14, scale/03 = 16, weight/regular = 400
Collection: "Typography" modes: ["Value"] ← aliases to Typography Primitives
body/font-family → alias family/sans
body/size-md → alias scale/03
```
### Advanced Pattern (200+ tokens) — M3 Model
Multiple semantic collections, 4–8 modes. Use when you need light/dark × contrast × brand or responsive breakpoints.
```
Collection: "M3" modes: ["Light", "Dark", "Light High Contrast", "Dark High Contrast", ...]
Collection: "Typeface" modes: ["Baseline", "Wireframe"]
Collection: "Typescale" modes: ["Value"] ← aliases into Typeface
Collection: "Shape" modes: ["Value"]
```
Key insight from M3: ALL 196 semantic color variables live in a SINGLE collection with 8 modes. Switching a frame's mode once updates every color simultaneously.
---
## 2. Creating Collections + Modes
### Creating a Primitives Collection
```javascript
const RUN_ID = "ds-build-2024-001"; // use the same RUN_ID throughout the build
// Create the collection
const primColl = figma.variables.createVariableCollection("Primitives");
// Rename the default "Mode 1" to "Value"
primColl.renameMode(primColl.modes[0].modeId, "Value");
const valueMode = primColl.modes[0].modeId;
// Tag for idempotency
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
primColl.setSharedPluginData('dsb', 'key', 'collection/primitives');
return {
collectionId: primColl.id,
modeId: valueMode,
name: primColl.name
};
```
### Creating a Semantic Color Collection with Light/Dark Modes
```javascript
const RUN_ID = "ds-build-2024-001";
const colorColl = figma.variables.createVariableCollection("Color");
// Rename default "Mode 1" to "Light"
colorColl.renameMode(colorColl.modes[0].modeId, "Light");
const lightModeId = colorColl.modes[0].modeId;
// Add "Dark" mode — requires Professional plan or higher
// Throws "in addMode: Limited to N modes only" on Starter plan
const darkModeId = colorColl.addMode("Dark");
colorColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
colorColl.setSharedPluginData('dsb', 'key', 'collection/color');
return {
collectionId: colorColl.id,
lightModeId,
darkModeId
};
```
**Mode plan limits:** Starter = 1 mode, Professional = 4 modes, Organization/Enterprise = 40 modes. If `addMode` throws, the file is on a Starter plan — tell the user and ask how to proceed.
### Creating a Spacing Collection (single mode)
```javascript
const RUN_ID = "ds-build-2024-001";
const spacingColl = figma.variables.createVariableCollection("Spacing");
spacingColl.renameMode(spacingColl.modes[0].modeId, "Value");
const valueMode = spacingColl.modes[0].modeId;
spacingColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
spacingColl.setSharedPluginData('dsb', 'key', 'collection/spacing');
return {
collectionId: spacingColl.id,
modeId: valueMode
};
```
---
## 3. Creating All Variable Types
### hex → {r, g, b} Conversion Helper
Colors in the Figma Plugin API are 0–1 range, not 0–255. Embed this helper in any script that creates color variables:
```javascript
function hexToRgb(hex) {
const clean = hex.replace('#', '');
return {
r: parseInt(clean.substring(0, 2), 16) / 255,
g: parseInt(clean.substring(2, 4), 16) / 255,
b: parseInt(clean.substring(4, 6), 16) / 255
};
}
// With alpha channel (for semi-transparent primitives like Black/200 at 10%):
function hexToRgba(hex) {
const clean = hex.replace('#', '');
const hasAlpha = clean.length === 8;
return {
r: parseInt(clean.substring(0, 2), 16) / 255,
g: parseInt(clean.substring(2, 4), 16) / 255,
b: parseInt(clean.substring(4, 6), 16) / 255,
a: hasAlpha ? parseInt(clean.substring(6, 8), 16) / 255 : 1
};
}
// Usage:
// hexToRgb('#3B82F6') → {r: 0.231, g: 0.510, b: 0.965}
// hexToRgb('#14AE5C') → {r: 0.078, g: 0.682, b: 0.361}
// hexToRgba('#0c0c0d1a') → {r: 0.047, g: 0.047, b: 0.051, a: 0.102}
```
### Creating Primitive Color Variables (Real SDS Data)
This creates a subset of the Simple Design System's `Color Primitives` collection (Blue family, from the Standard pattern used by real design systems):
```javascript
function hexToRgb(hex) {
const c = hex.replace('#', '');
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
}
const RUN_ID = "ds-build-2024-001";
// Get the Primitives collection created in the previous step
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
if (!primColl) throw new Error("Primitives collection not found — run collection creation first");
const valueMode = primColl.modes[0].modeId;
// Define primitives — use real values from your codebase
const primitiveColors = [
// Blue scale
{ name: 'blue/100', hex: '#EFF6FF' },
{ name: 'blue/200', hex: '#DBEAFE' },
{ name: 'blue/300', hex: '#93C5FD' },
{ name: 'blue/400', hex: '#60A5FA' },
{ name: 'blue/500', hex: '#3B82F6' },
{ name: 'blue/600', hex: '#2563EB' },
{ name: 'blue/700', hex: '#1D4ED8' },
{ name: 'blue/800', hex: '#1E40AF' },
{ name: 'blue/900', hex: '#1E3A8A' },
// Gray scale
{ name: 'gray/100', hex: '#F9FAFB' },
{ name: 'gray/200', hex: '#F3F4F6' },
{ name: 'gray/300', hex: '#D1D5DB' },
{ name: 'gray/400', hex: '#9CA3AF' },
{ name: 'gray/500', hex: '#6B7280' },
{ name: 'gray/600', hex: '#4B5563' },
{ name: 'gray/700', hex: '#374151' },
{ name: 'gray/800', hex: '#1F2937' },
{ name: 'gray/900', hex: '#111827' },
// White / Black
{ name: 'white/1000', hex: '#FFFFFF' },
{ name: 'black/1000', hex: '#000000' },
];
const created = [];
for (const { name, hex } of primitiveColors) {
const v = figma.variables.createVariable(name, primColl, 'COLOR');
v.setValueForMode(valueMode, hexToRgb(hex));
// Primitives: EMPTY scopes (hidden from all pickers — designers use semantics)
v.scopes = [];
// Code syntax from the actual CSS variable name
v.setVariableCodeSyntax('WEB', `var(--color-${name.replace('/', '-')})`);
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
v.setSharedPluginData('dsb', 'key', `primitive/${name}`);
created.push({ name, id: v.id });
}
return { created, count: created.length };
```
**Critical scope rule for primitives:** Set `v.scopes = []`. This hides primitives from every picker. Designers should only see semantic tokens. The exception is semi-transparent overlay primitives (Black/White with alpha) — those get `["EFFECT_COLOR"]` so they appear in shadow pickers.
### Creating FLOAT Variables (Spacing, Radius, Font Size)
```javascript
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const spacingColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/spacing');
if (!spacingColl) throw new Error("Spacing collection not found");
const valueMode = spacingColl.modes[0].modeId;
const spacingTokens = [
{ name: 'spacing/xs', value: 4, scope: 'GAP', cssVar: '--spacing-xs' },
{ name: 'spacing/sm', value: 8, scope: 'GAP', cssVar: '--spacing-sm' },
{ name: 'spacing/md', value: 16, scope: 'GAP', cssVar: '--spacing-md' },
{ name: 'spacing/lg', value: 24, scope: 'GAP', cssVar: '--spacing-lg' },
{ name: 'spacing/xl', value: 32, scope: 'GAP', cssVar: '--spacing-xl' },
{ name: 'spacing/2xl', value: 48, scope: 'GAP', cssVar: '--spacing-2xl' },
];
const radiusTokens = [
{ name: 'radius/none', value: 0, scope: 'CORNER_RADIUS', cssVar: '--radius-none' },
{ name: 'radius/sm', value: 4, scope: 'CORNER_RADIUS', cssVar: '--radius-sm' },
{ name: 'radius/md', value: 8, scope: 'CORNER_RADIUS', cssVar: '--radius-md' },
{ name: 'radius/lg', value: 16, scope: 'CORNER_RADIUS', cssVar: '--radius-lg' },
{ name: 'radius/full', value: 9999, scope: 'CORNER_RADIUS', cssVar: '--radius-full' },
];
const created = [];
for (const { name, value, scope, cssVar } of [...spacingTokens, ...radiusTokens]) {
const v = figma.variables.createVariable(name, spacingColl, 'FLOAT');
v.setValueForMode(valueMode, value);
v.scopes = [scope];
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
v.setSharedPluginData('dsb', 'key', name);
created.push({ name, value, id: v.id });
}
return { created, count: created.length };
```
### Creating STRING Variables (Font Family, Font Style)
```javascript
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const typoPrimColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/typography-primitives');
if (!typoPrimColl) throw new Error("Typography Primitives collection not found");
const valueMode = typoPrimColl.modes[0].modeId;
const fontTokens = [
{ name: 'family/sans', value: 'Inter', scope: 'FONT_FAMILY', cssVar: '--font-family-sans' },
{ name: 'family/mono', value: 'Roboto Mono', scope: 'FONT_FAMILY', cssVar: '--font-family-mono' },
// Font style strings — these are the Figma fontName.style values:
{ name: 'weight/regular', value: 'Regular', scope: 'FONT_STYLE', cssVar: '--font-weight-regular' },
{ name: 'weight/medium', value: 'Medium', scope: 'FONT_STYLE', cssVar: '--font-weight-medium' },
{ name: 'weight/semibold', value: 'Semi Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-semibold' },
{ name: 'weight/bold', value: 'Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-bold' },
];
const created = [];
for (const { name, value, scope, cssVar } of fontTokens) {
const v = figma.variables.createVariable(name, typoPrimColl, 'STRING');
v.setValueForMode(valueMode, value);
v.scopes = [scope];
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
v.setSharedPluginData('dsb', 'key', `typo-prim/${name}`);
created.push({ name, value, id: v.id });
}
return { created, count: created.length };
```
### Creating BOOLEAN Variables
BOOLEAN variables have no scopes (scopes are not supported for BOOLEAN type).
```javascript
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const coll = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/tokens');
if (!coll) throw new Error("Collection not found");
const valueMode = coll.modes[0].modeId;
const v = figma.variables.createVariable('feature-flags/show-beta-badge', coll, 'BOOLEAN');
v.setValueForMode(valueMode, false);
// No scopes — BOOLEAN does not support scopes
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
v.setSharedPluginData('dsb', 'key', 'feature-flags/show-beta-badge');
return { id: v.id, name: v.name };
```
---
## 4. Variable Aliasing (VARIABLE_ALIAS) — Primitive → Semantic Chain
Semantic tokens reference primitives via `VARIABLE_ALIAS`. This is the core pattern that makes light/dark theming work.
**Architecture:**
```
Color Primitives collection (1 mode: Value)
blue/500 = #3B82F6 ← raw value
Color collection (2 modes: Light, Dark)
color/bg/accent/default:
Light → VARIABLE_ALIAS → Primitives/blue/500
Dark → VARIABLE_ALIAS → Primitives/blue/300
```
### Complete Semantic Alias Creation Script (SDS-style)
```javascript
function hexToRgb(hex) {
const c = hex.replace('#', '');
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
}
const RUN_ID = "ds-build-2024-001";
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
const colorColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/color');
if (!primColl || !colorColl) throw new Error("Collections not found — run primitive/color collection creation first");
const primValueMode = primColl.modes[0].modeId;
const lightModeId = colorColl.modes.find(m => m.name === 'Light').modeId;
const darkModeId = colorColl.modes.find(m => m.name === 'Dark').modeId;
// Load all primitive variables for lookup
const allVars = await figma.variables.getLocalVariablesAsync();
const primsByKey = {};
for (const v of allVars) {
if (v.variableCollectionId === primColl.id) {
primsByKey[v.getSharedPluginData('dsb', 'key')] = v;
}
}
function getPrim(name) {
const v = primsByKey[`primitive/${name}`];
if (!v) throw new Error(`Primitive not found: primitive/${name}`);
return v;
}
// Define semantic → [lightPrimitiveName, darkPrimitiveName]
// Following the SDS pattern: Background/{Intent}/{Emphasis}
const semanticColors = [
// Background
{ name: 'color/bg/default/default', lightPrim: 'white/1000', darkPrim: 'gray/900',
cssVar: '--color-bg-default-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
{ name: 'color/bg/default/secondary', lightPrim: 'gray/100', darkPrim: 'gray/800',
cssVar: '--color-bg-default-secondary', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
{ name: 'color/bg/brand/default', lightPrim: 'blue/600', darkPrim: 'blue/300',
cssVar: '--color-bg-brand-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
// Text
{ name: 'color/text/default/default', lightPrim: 'gray/900', darkPrim: 'white/1000',
cssVar: '--color-text-default-default', scopes: ['TEXT_FILL'] },
{ name: 'color/text/default/secondary', lightPrim: 'gray/500', darkPrim: 'gray/400',
cssVar: '--color-text-default-secondary', scopes: ['TEXT_FILL'] },
{ name: 'color/text/brand/default', lightPrim: 'blue/700', darkPrim: 'blue/200',
cssVar: '--color-text-brand-default', scopes: ['TEXT_FILL'] },
// Border
{ name: 'color/border/default/default', lightPrim: 'gray/300', darkPrim: 'gray/600',
cssVar: '--color-border-default-default', scopes: ['STROKE_COLOR'] },
{ name: 'color/border/brand/default', lightPrim: 'blue/500', darkPrim: 'blue/400',
cssVar: '--color-border-brand-default', scopes: ['STROKE_COLOR'] },
];
const created = [];
for (const { name, lightPrim, darkPrim, cssVar, scopes } of semanticColors) {
const v = figma.variables.createVariable(name, colorColl, 'COLOR');
// Alias to primitive in Light mode
v.setValueForMode(lightModeId, figma.variables.createVariableAlias(getPrim(lightPrim)));
// Alias to primitive in Dark mode
v.setValueForMode(darkModeId, figma.variables.createVariableAlias(getPrim(darkPrim)));
// Set scopes (semantic layer — these ARE shown in pickers)
v.scopes = scopes;
// Code syntax
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
v.setSharedPluginData('dsb', 'key', name);
created.push({ name, id: v.id });
}
return { created, count: created.length };
```
**Key API points:**
- `figma.variables.createVariableAlias(variable)` — takes a Variable object, returns `{type:'VARIABLE_ALIAS', id: variable.id}`
- The aliased variable MUST have the same `resolvedType` as the semantic variable
- Never duplicate raw values in the semantic layer — always alias
---
## 5. Variable Scopes — Complete Reference Table
| Semantic Role | Recommended Scopes | Variable Type |
|---|---|---|
| Primitive colors (raw) | `[]` — empty, hidden from all pickers | COLOR |
| Semi-transparent overlay primitives | `["EFFECT_COLOR"]` | COLOR |
| Background fills (frame, shape) | `["FRAME_FILL", "SHAPE_FILL"]` | COLOR |
| Text color | `["TEXT_FILL"]` | COLOR |
| Icon / shape fill | `["SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
| Border / stroke color | `["STROKE_COLOR"]` | COLOR |
| Background + border combined | `["FRAME_FILL", "SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
| Shadow color | `["EFFECT_COLOR"]` | COLOR |
| Spacing / gap between items | `["GAP"]` | FLOAT |
| Padding (if separate from gap) | `["GAP"]` | FLOAT |
| Corner radius | `["CORNER_RADIUS"]` | FLOAT |
| Width / height dimensions | `["WIDTH_HEIGHT"]` | FLOAT |
| Font size | `["FONT_SIZE"]` | FLOAT |
| Line height | `["LINE_HEIGHT"]` | FLOAT |
| Letter spacing | `["LETTER_SPACING"]` | FLOAT |
| Font weight (numeric) | `["FONT_WEIGHT"]` | FLOAT |
| Stroke width | `["STROKE_FLOAT"]` | FLOAT |
| Effect blur radius | `["EFFECT_FLOAT"]` | FLOAT |
| Opacity | `["OPACITY"]` | FLOAT |
| Font family | `["FONT_FAMILY"]` | STRING |
| Font style (e.g. "Semi Bold") | `["FONT_STYLE"]` | STRING |
| Boolean flags | *(scopes not supported)* | BOOLEAN |
**Never use `ALL_SCOPES`** on any variable. It pollutes every picker with irrelevant tokens. The Simple Design System (SDS), the gold standard, uses targeted scopes on every variable.
**`ALL_FILLS` note:** `ALL_FILLS` is exclusive among fill scopes — it covers `FRAME_FILL`, `SHAPE_FILL`, and `TEXT_FILL` together. If set, you cannot also add individual fill scopes. Prefer specifying individual scopes for precision.
### Batch Scope-Setting (After Variables are Created)
If you created variables without scopes and need to set them in batch:
```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
// Scope mapping: partial name match → scopes
const scopeRules = [
{ match: 'color/bg/', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
{ match: 'color/text/', scopes: ['TEXT_FILL'] },
{ match: 'color/icon/', scopes: ['SHAPE_FILL', 'STROKE_COLOR'] },
{ match: 'color/border/', scopes: ['STROKE_COLOR'] },
{ match: 'spacing/', scopes: ['GAP'] },
{ match: 'radius/', scopes: ['CORNER_RADIUS'] },
{ match: 'blue/', scopes: [] }, // primitives — hide
{ match: 'gray/', scopes: [] },
{ match: 'white/', scopes: [] },
{ match: 'black/', scopes: [] },
];
const updated = [];
for (const v of allVars) {
if (v.remote) continue; // skip library variables
for (const rule of scopeRules) {
if (v.name.startsWith(rule.match)) {
v.scopes = rule.scopes;
updated.push({ name: v.name, scopes: rule.scopes });
break;
}
}
}
return { updated, count: updated.length };
```
---
## 6. Code Syntax — WEB/ANDROID/iOS
Every variable must have code syntax set. This is what powers the developer handoff experience:
**What code syntax does:** When a developer inspects any element in Figma Dev Mode that has a variable-bound property (fill, padding, radius, etc.), the code snippet shown uses the variable's code syntax name — not the Figma variable name. For example, a button's background fill bound to `color/bg/primary` will show `background: var(--color-bg-primary)` in the CSS snippet, not `color/bg/primary`. Without code syntax set, Dev Mode shows raw hex values or nothing useful.
You can set up to **3 syntaxes per variable** — one per platform (Web, iOS, Android). Set all three if the codebase targets multiple platforms; set only WEB if it's a web-only project.
```javascript
// WEB: MUST include the var() wrapper — this is the full CSS function syntax
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
// ^^^^ ^
// var() wrapper is REQUIRED
// ANDROID: Kotlin property name — camelCase, no wrapper
variable.setVariableCodeSyntax('ANDROID', 'colorBgPrimary');
// iOS: Swift property — dot-notation, no wrapper
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
```
> **CRITICAL — WEB code syntax MUST use the `var()` wrapper.** Setting just `--color-bg-primary` (without `var()`) will cause Dev Mode to show raw hex values instead of the CSS variable reference. Always use the full `var(--name)` form. ANDROID and iOS do NOT use a wrapper.
**Platform derivation rules from the CSS variable name:**
| Platform | Pattern | Example |
|---|---|---|
| WEB | **`var(--{css-var-name})`** — `var()` wrapper required | `var(--sds-color-bg-primary)` |
| ANDROID | camelCase, no wrapper, strip `--` prefix | `sdsColorBgPrimary` |
| iOS | PascalCase after `.`, no wrapper, strip `--` prefix | `Color.SdsColorBgPrimary` or `Color.bgPrimary` |
**Always use the actual CSS variable name from the codebase** — do not derive it from the Figma variable name. If the code uses `--sds-color-background-brand-default`, that exact string is the WEB code syntax (minus the `var()` wrapper that you add).
### Batch Code Syntax Setting
```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
const updated = [];
for (const v of allVars) {
if (v.remote) continue;
// If code syntax already set, skip
if (v.codeSyntax['WEB']) continue;
// FALLBACK: derive from Figma name: color/bg/primary → var(--color-bg-primary)
// PREFERRED: pass in a cssVarMap built from actual codebase CSS variable names
// e.g. cssVarMap = { 'color/bg/primary': '--color-bg-primary', ... }
const cssName = cssVarMap?.[v.name]
?? v.name.replace(/\//g, '-').replace(/\s/g, '-').toLowerCase();
v.setVariableCodeSyntax('WEB', `var(--${cssName})`);
updated.push({ name: v.name, web: `var(--${cssName})` });
}
return { updated, count: updated.length };
```
Note: derived names are a fallback only. Always prefer overriding with actual CSS variable names from the codebase when they are known.
---
## 7. Effect Styles (Shadows) and Text Styles
Shadows and composite typography cannot be variables — they are Styles.
### Creating Effect Styles (Shadows)
Reference from SDS (15 effect styles) and the SDS shadow pattern `Shadow/{Level}`:
```javascript
const RUN_ID = "ds-build-2024-001";
// Shadow definitions — CSS equivalent in comments
// CSS: 0 1px 2px rgba(0,0,0,0.05)
const shadows = [
{
name: 'Shadow/Subtle',
effects: [{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.05 },
offset: { x: 0, y: 1 },
radius: 2,
spread: 0,
visible: true,
blendMode: 'NORMAL'
}]
},
{
// CSS: 0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -1px rgba(0,0,0,0.06)
name: 'Shadow/Medium',
effects: [
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.10 },
offset: { x: 0, y: 4 },
radius: 6,
spread: -1,
visible: true,
blendMode: 'NORMAL'
},
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.06 },
offset: { x: 0, y: 2 },
radius: 4,
spread: -1,
visible: true,
blendMode: 'NORMAL'
}
]
},
{
// CSS: 0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -2px rgba(0,0,0,0.05)
name: 'Shadow/Strong',
effects: [
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.10 },
offset: { x: 0, y: 10 },
radius: 15,
spread: -3,
visible: true,
blendMode: 'NORMAL'
},
{
type: 'DROP_SHADOW',
color: { r: 0, g: 0, b: 0, a: 0.05 },
offset: { x: 0, y: 4 },
radius: 6,
spread: -2,
visible: true,
blendMode: 'NORMAL'
}
]
}
];
// M3-style dual shadow (umbra + penumbra pattern):
const m3Shadows = [
{
name: 'Elevation/1',
effects: [
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:1}, radius:3, spread:1, visible:true, blendMode:'NORMAL' }
]
},
{
name: 'Elevation/2',
effects: [
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:2}, radius:6, spread:2, visible:true, blendMode:'NORMAL' }
]
},
{
name: 'Elevation/3',
effects: [
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:3, spread:0, visible:true, blendMode:'NORMAL' },
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:4}, radius:8, spread:3, visible:true, blendMode:'NORMAL' }
]
}
];
const created = [];
for (const { name, effects } of shadows) {
const style = figma.createEffectStyle();
style.name = name;
style.effects = effects;
style.setSharedPluginData('dsb', 'run_id', RUN_ID);
style.setSharedPluginData('dsb', 'key', `effect-style/${name}`);
created.push({ name, id: style.id });
}
return { created, count: created.length };
```
### Creating Text Styles
Fonts must be loaded before creating text styles.
```javascript
const RUN_ID = "ds-build-2024-001";
// Define text styles — based on SDS typography hierarchy
const textStyles = [
// Display / Hero
{ name: 'Display/Hero', family: 'Inter', style: 'Bold', size: 72, lineHeight: 80, letterSpacing: -1.5 },
// Headings
{ name: 'Heading/H1', family: 'Inter', style: 'Bold', size: 48, lineHeight: 56, letterSpacing: -1.0 },
{ name: 'Heading/H2', family: 'Inter', style: 'Bold', size: 40, lineHeight: 48, letterSpacing: -0.5 },
{ name: 'Heading/H3', family: 'Inter', style: 'Semi Bold', size: 32, lineHeight: 40, letterSpacing: 0 },
{ name: 'Heading/H4', family: 'Inter', style: 'Semi Bold', size: 24, lineHeight: 32, letterSpacing: 0 },
// Body
{ name: 'Body/Large', family: 'Inter', style: 'Regular', size: 18, lineHeight: 28, letterSpacing: 0 },
{ name: 'Body/Medium', family: 'Inter', style: 'Regular', size: 16, lineHeight: 24, letterSpacing: 0 },
{ name: 'Body/Small', family: 'Inter', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
// Label
{ name: 'Label/Large', family: 'Inter', style: 'Medium', size: 14, lineHeight: 20, letterSpacing: 0.1 },
{ name: 'Label/Medium', family: 'Inter', style: 'Medium', size: 12, lineHeight: 16, letterSpacing: 0.5 },
{ name: 'Label/Small', family: 'Inter', style: 'Medium', size: 11, lineHeight: 16, letterSpacing: 0.5 },
// Code
{ name: 'Code/Base', family: 'Roboto Mono', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
];
// Load all required fonts first
const fontSet = new Set(textStyles.map(s => JSON.stringify({ family: s.family, style: s.style })));
await Promise.all([...fontSet].map(f => figma.loadFontAsync(JSON.parse(f))));
const created = [];
for (const { name, family, style, size, lineHeight, letterSpacing } of textStyles) {
const ts = figma.createTextStyle();
ts.name = name;
ts.fontName = { family, style };
ts.fontSize = size;
ts.lineHeight = { value: lineHeight, unit: 'PIXELS' };
ts.letterSpacing = { value: letterSpacing, unit: 'PIXELS' };
ts.setSharedPluginData('dsb', 'run_id', RUN_ID);
ts.setSharedPluginData('dsb', 'key', `text-style/${name}`);
created.push({ name, id: ts.id });
}
return { created, count: created.length };
```
---
## 8. Idempotency — Check-Before-Create Pattern
Every creation script should check whether the entity already exists before creating it. This prevents duplicates when a script is re-run after partial failure.
### Check-Before-Create for Collections
```javascript
const DSB_KEY = 'collection/primitives';
const RUN_ID = "ds-build-2024-001";
// Check if already exists
const existing = await figma.variables.getLocalVariableCollectionsAsync();
let primColl = existing.find(c => c.getSharedPluginData('dsb', 'key') === DSB_KEY);
if (primColl) {
return { status: 'already_exists', collectionId: primColl.id, name: primColl.name };
}
// Create only if not found
primColl = figma.variables.createVariableCollection("Primitives");
primColl.renameMode(primColl.modes[0].modeId, "Value");
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
primColl.setSharedPluginData('dsb', 'key', DSB_KEY);
return { status: 'created', collectionId: primColl.id };
```
### Check-Before-Create for Variables
```javascript
const VARIABLE_KEY = 'primitive/blue/500';
const RUN_ID = "ds-build-2024-001";
// Check if already exists by sharedPluginData key
const allVars = await figma.variables.getLocalVariablesAsync();
const existing = allVars.find(v => v.getSharedPluginData('dsb', 'key') === VARIABLE_KEY);
if (existing) {
return { status: 'already_exists', id: existing.id, name: existing.name };
}
// ... create the variable ...
return { status: 'created' };
```
### sharedPluginData Tagging Strategy
Tag every created node immediately after creation. The `key` is the stable logical identifier used for idempotency checks. The `run_id` identifies which build run created it (useful for cleanup).
```javascript
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // build run ID
node.setSharedPluginData('dsb', 'phase', 'phase1'); // which phase
node.setSharedPluginData('dsb', 'key', 'color/bg/primary'); // stable logical key
```
**Cleanup by run ID (safe — targets only tagged nodes, never user-owned nodes):**
```javascript
const TARGET_RUN_ID = "ds-build-2024-001"; // run to remove
const allVars = await figma.variables.getLocalVariablesAsync();
const removed = [];
for (const v of allVars) {
if (v.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
removed.push(v.name);
v.remove();
}
}
return { removed, count: removed.length };
```
**Never clean up by name prefix** (e.g., deleting everything starting with `color/`). This will destroy user-created variables that happen to share the prefix.
---
## 9. Validation — Verify Counts, Aliases, and Scopes
Run these scripts after Phase 1 to verify everything was created correctly before proceeding to Phase 2.
### Verify Collection and Variable Counts
```javascript
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const allVars = await figma.variables.getLocalVariablesAsync();
const summary = collections.map(c => {
const vars = allVars.filter(v => v.variableCollectionId === c.id);
return {
name: c.name,
id: c.id,
modes: c.modes.map(m => m.name),
variableCount: vars.length,
missingScopes: vars.filter(v => v.scopes.length === 0 && v.resolvedType !== 'BOOLEAN').length,
missingCodeSyntax: vars.filter(v => !v.codeSyntax['WEB'] && !v.remote).length,
sampleVariables: vars.slice(0, 3).map(v => v.name)
};
});
return {
collectionCount: collections.length,
totalVariables: allVars.length,
collections: summary
};
```
Interpret: `missingScopes > 0` (for non-primitives and non-BOOLEANs) → scope-setting failed, re-run scope script. `missingCodeSyntax > 0` → code syntax not set, run batch code syntax script.
Note: primitives correctly have `scopes = []` (empty, hidden). `missingScopes` above counts non-BOOLEAN variables with empty scopes — review the list to confirm they are all primitives.
### Verify Aliases Resolve
```javascript
const allVars = await figma.variables.getLocalVariablesAsync();
const collections = await figma.variables.getLocalVariableCollectionsAsync();
const brokenAliases = [];
const aliasedVars = [];
for (const v of allVars) {
if (v.remote) continue;
const coll = collections.find(c => c.id === v.variableCollectionId);
if (!coll) continue;
for (const [modeId, val] of Object.entries(v.valuesByMode)) {
if (val && typeof val === 'object' && val.type === 'VARIABLE_ALIAS') {
aliasedVars.push({ name: v.name, aliasTargetId: val.id });
// Verify the target exists
const target = allVars.find(t => t.id === val.id);
if (!target) {
brokenAliases.push({ variable: v.name, modeId, missingTargetId: val.id });
}
}
}
}
return {
totalAliased: aliasedVars.length,
brokenAliases,
brokenCount: brokenAliases.length,
status: brokenAliases.length === 0 ? 'all_aliases_resolve' : 'BROKEN_ALIASES_FOUND'
};
```
Interpret: `brokenCount > 0` means a semantic variable references a primitive that was deleted or not yet created. Create the missing primitives, then re-run alias creation for the affected semantic variables.
### Verify Style Counts
```javascript
const [textStyles, effectStyles] = await Promise.all([
figma.getLocalTextStylesAsync(),
figma.getLocalEffectStylesAsync()
]);
return {
textStyles: textStyles.map(s => ({ name: s.name, fontSize: s.fontSize, fontFamily: s.fontName.family })),
effectStyles: effectStyles.map(s => ({ name: s.name, effectCount: s.effects.length })),
counts: { text: textStyles.length, effect: effectStyles.length }
};
```
### Phase 1 Exit Criteria Checklist
Before proceeding to Phase 2, verify all of the following:
- Every planned collection exists with the correct number of modes
- Primitive variables: `scopes = []`, code syntax set
- Semantic variables: targeted scopes set, code syntax set, aliases pointing to primitives (not raw values)
- All broken alias count = 0
- All planned text styles exist with correct font family/size/weight
- All planned effect styles exist with correct shadow values
- No variable has `ALL_SCOPES` unless explicitly approved by the user