> 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
(async () => {
  try {
    // 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 pluginData for idempotency
    docFrame.setPluginData('dsb_run_id', RUN_ID);
    docFrame.setPluginData('dsb_key', 'doc/button');

    figma.closePlugin(JSON.stringify({ docFrameId: docFrame.id, pageId: page.id }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

---

## 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
(async () => {
  try {
    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.setPluginData('dsb_run_id', RUN_ID);
    comp.setPluginData('dsb_phase', 'phase3');
    comp.setPluginData('dsb_key', 'component/button/base');

    figma.closePlugin(JSON.stringify({ baseCompId: comp.id }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

**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
(async () => {
  try {
    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.setPluginData('dsb_run_id', RUN_ID);
          clone.setPluginData('dsb_key', `component/button/variant/${size}/${style}/${state}`);

          components.push(clone);
        }
      }
    }

    figma.closePlugin(JSON.stringify({ variantIds: components.map(c => c.id) }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

---

## 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
(async () => {
  try {
    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.setPluginData('dsb_run_id', 'ds-build-2024-001');
    cs.setPluginData('dsb_key', 'componentset/button');

    figma.closePlugin(JSON.stringify({ componentSetId: cs.id }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

**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
(async () => {
  try {
    // 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.setPluginData('dsb_run_id', RUN_ID);
    iconComp.setPluginData('dsb_key', 'icon/chevron-right');

    figma.closePlugin(JSON.stringify({ iconCompId: iconComp.id }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

**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. `pluginData` Tagging for Idempotency

Tag EVERY created node immediately after creation. This enables safe cleanup, resumability, and idempotency checks.

```javascript
// After creating any node:
node.setPluginData('dsb_run_id', RUN_ID);   // identifies the build run
node.setPluginData('dsb_phase', 'phase3');  // which phase created it
node.setPluginData('dsb_key', KEY);         // unique logical key for this entity

// Reading back:
const runId = node.getPluginData('dsb_run_id'); // '' if not set
const key   = node.getPluginData('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 `dsb_key`:

```javascript
const existing = figma.currentPage.findAll(n =>
  n.getPluginData('dsb_key') === 'componentset/button'
);
if (existing.length > 0) {
  // Skip creation — already done. Return existing node's ID.
  figma.closePlugin(JSON.stringify({ componentSetId: existing[0].id }));
  return;
}
```

---

## 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
(async () => {
  try {
    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
      ),
    }));
    figma.closePlugin(JSON.stringify({ sampleVariants: sample, totalChildren: cs.children.length }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```
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
(async () => {
  try {
    let page = figma.root.children.find(p => p.name === 'Button');
    if (!page) { page = figma.createPage(); page.name = 'Button'; }
    page.setPluginData('dsb_run_id', 'ds-build-2024-001');
    page.setPluginData('dsb_key', 'page/button');
    figma.closePlugin(JSON.stringify({ pageId: page.id }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

### Call 2: Create documentation frame

**Goal:** Add title + description frame.
**State input:** `{ pageId }`
**State output:** `{ docFrameId }`

```javascript
(async () => {
  try {
    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.getPluginData('dsb_key') === 'doc/button');
    if (existing.length > 0) {
      figma.closePlugin(JSON.stringify({ docFrameId: existing[0].id }));
      return;
    }

    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.setPluginData('dsb_run_id', 'ds-build-2024-001');
    docFrame.setPluginData('dsb_key', 'doc/button');

    figma.closePlugin(JSON.stringify({ docFrameId: docFrame.id }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

### 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
(async () => {
  try {
    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.setPluginData('dsb_run_id', RUN_ID);
          clone.setPluginData('dsb_key', `component/button/variant/${size}/${style}/${state}`);
          components.push(clone);
        }
      }
    }

    figma.closePlugin(JSON.stringify({ variantIds: components.map(c => c.id) }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

### 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
(async () => {
  try {
    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 } : {}),
        };
      }
    }

    figma.closePlugin(JSON.stringify({
      componentSetId: cs.id,
      properties: { labelKey, showIconKey, iconKey },
    }));
  } catch (e) { figma.closePluginWithFailure(e.toString()); }
})();
```

### 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.
