diff --git a/.agents/skills/figma-use/SKILL.md b/.agents/skills/figma-use/SKILL.md new file mode 100644 index 00000000000..5213e08f8db --- /dev/null +++ b/.agents/skills/figma-use/SKILL.md @@ -0,0 +1,405 @@ +--- +name: figma-use +description: "**MANDATORY prerequisite** — you MUST invoke this skill BEFORE every `use_figma` tool call. NEVER call `use_figma` directly without loading this skill first. Skipping it causes common, hard-to-debug failures. Trigger whenever the user wants to perform a write action or a unique read action that requires JavaScript execution in the Figma file context — e.g. create/edit/delete nodes, set up variables or tokens, build components and variants, modify auto-layout or fills, bind variables to properties, or inspect file structure programmatically." +disable-model-invocation: false +--- + +# use_figma — Figma Plugin API Skill + +Use the `use_figma` tool to execute JavaScript in Figma files via the Plugin API. All detailed reference docs live in `references/`. + +**Always pass `skillNames: "figma-use"` when calling `use_figma`.** This is a logging parameter used to track skill usage — it does not affect execution. + +**If Figma MCP tools appear as deferred tools, batch-load all their schemas in a single `ToolSearch` call** using the `select:` syntax — e.g. `ToolSearch query="select:use_figma,get_screenshot,get_metadata,create_new_file"`. One round trip beats six. + +**If the task involves building or updating a full page, screen, or multi-section layout in Figma from code**, also load [figma-generate-design](../figma-generate-design/SKILL.md). It provides the workflow for discovering design system components via `search_design_system`, importing them, and assembling screens incrementally. Both skills work together: this one for the API rules, that one for the screen-building workflow. + +Before anything, load [plugin-api-standalone.index.md](references/plugin-api-standalone.index.md) to understand what is possible. When you are asked to write plugin API code, use this context to grep [plugin-api-standalone.d.ts](references/plugin-api-standalone.d.ts) for relevant types, methods, and properties. This is the definitive source of truth for the API surface. It is a large typings file, so do not load it all at once, grep for relevant sections as needed. + +IMPORTANT: Whenever you work with design systems, start with [working-with-design-systems/wwds.md](references/working-with-design-systems/wwds.md) to understand the key concepts, processes, and guidelines for working with design systems in Figma. Then load the more specific references for components, variables, text styles, and effect styles as needed. + +## 1. Critical Rules + +1. **Use `return` to send data back.** The return value is JSON-serialized automatically (objects, arrays, strings, numbers). Do NOT call `figma.closePlugin()` or wrap code in an async IIFE — this is handled for you. +2. **Write plain JavaScript with top-level `await` and `return`.** Code is automatically wrapped in an async context. Do NOT wrap in `(async () => { ... })()`. +3. `figma.notify()` **throws "not implemented"** — never use it +3a. `getPluginData()` / `setPluginData()` are **not supported** in `use_figma` — do not use them. Use `getSharedPluginData()` / `setSharedPluginData()` instead (these ARE supported), or track node IDs by returning them and passing them to subsequent calls. +4. `console.log()` is NOT returned — use `return` for output +5. **Work incrementally in small steps.** Break large operations into multiple `use_figma` calls. Validate after each step. This is the single most important practice for avoiding bugs. +6. Colors are **0–1 range** (not 0–255): `{r: 1, g: 0, b: 0}` = red +7. Fills/strokes are **read-only arrays** — clone, modify, reassign +8. **Font loading is required before ANY operation on nodes that contain unloaded fonts** — not just text-setting operations. This includes `appendChild`, `insertChild`, `setBoundVariable`, `setExplicitVariableModeForCollection`, `setValueForMode`, and even `findAll` callbacks. If the document has existing text nodes, preload all their fonts at the start of the script. Use `await figma.listAvailableFontsAsync()` to discover available fonts and styles, then `await figma.loadFontAsync({family, style})` to load each one. See [Gotchas](references/gotchas.md) for the full preload pattern. +9. **Pages load incrementally** — use `await figma.setCurrentPageAsync(page)` to switch pages and load their content. The sync setter `figma.currentPage = page` does **NOT** work and will throw (see Page Rules below) +10. `setBoundVariableForPaint` returns a **NEW** paint — must capture and reassign +11. `createVariable` accepts collection **object or ID string** (object preferred) +12. **`layoutSizingHorizontal/Vertical = 'FILL'` MUST be set AFTER `parent.appendChild(child)`** — setting before append throws. Same applies to `'HUG'` on non-auto-layout nodes. +13. **Position new top-level nodes away from (0,0).** Nodes appended directly to the page default to (0,0). Scan `figma.currentPage.children` to find a clear position (e.g., to the right of the rightmost node). This only applies to page-level nodes — nodes nested inside other frames or auto-layout containers are positioned by their parent. See [Gotchas](references/gotchas.md). +14. **On `use_figma` error, STOP. Do NOT immediately retry.** Failed scripts are **atomic** — if a script errors, it is not executed at all and no changes are made to the file. Read the error message carefully, fix the script, then retry. See [Error Recovery](#6-error-recovery--self-correction). +15. **MUST `return` ALL created/mutated node IDs.** Whenever a script creates new nodes or mutates existing ones on the canvas, collect every affected node ID and return them in a structured object (e.g. `return { createdNodeIds: [...], mutatedNodeIds: [...] }`). This is essential for subsequent calls to reference, validate, or clean up those nodes. +16. **Always set `variable.scopes` explicitly when creating variables.** The default `ALL_SCOPES` pollutes every property picker — almost never what you want. Use specific scopes like `["FRAME_FILL", "SHAPE_FILL"]` for backgrounds, `["TEXT_FILL"]` for text colors, `["GAP"]` for spacing, etc. See [variable-patterns.md](references/variable-patterns.md) for the full list. +17. **`await` every Promise.** Never leave a Promise unawaited — unawaited async calls (e.g. `figma.loadFontAsync(...)` without `await`, or `figma.setCurrentPageAsync(page)` without `await`) will fire-and-forget, causing silent failures or race conditions. The script may return before the async operation completes, leading to missing data or half-applied changes. + +> For detailed WRONG/CORRECT examples of each rule, see [Gotchas & Common Mistakes](references/gotchas.md). + +## 2. Page Rules (Critical) + +**Page context resets between `use_figma` calls** — `figma.currentPage` starts on the first page each time. + +### Switching pages + +Use `await figma.setCurrentPageAsync(page)` to switch pages and load their content. The sync setter `figma.currentPage = page` does **NOT work** — it throws `"Setting figma.currentPage is not supported"` in `use_figma`. Always use the async method. + +```js +// Switch to a specific page (loads its content) +const targetPage = figma.root.children.find((p) => p.name === "My Page"); +await figma.setCurrentPageAsync(targetPage); +// targetPage.children is now populated + +// Iterate over all pages +for (const page of figma.root.children) { + await figma.setCurrentPageAsync(page); + // page.children is now loaded — read or modify them here +} +``` + +### Across script runs + +`figma.currentPage` resets to the **first page** at the start of each `use_figma` call. If your workflow spans multiple calls and targets a non-default page, call `await figma.setCurrentPageAsync(page)` at the start of each invocation. + +You can call `use_figma` multiple times to incrementally build on the file state, or to retrieve information before writing another script. For example, write a script to get metadata about existing nodes, `return` that data, then use it in a subsequent script to modify those nodes. + +## 3. `return` Is Your Output Channel + +The agent sees **ONLY** the value you `return`. Everything else is invisible. + +- **Returning IDs (CRITICAL)**: Every script that creates or mutates canvas nodes **MUST** return all affected node IDs — e.g. `return { createdNodeIds: [...], mutatedNodeIds: [...] }`. This is a hard requirement, not optional. +- **Progress reporting**: `return { createdNodeIds: [...], count: 5, errors: [] }` +- **Error info**: Thrown errors are automatically captured and returned — just let them propagate or `throw` explicitly. +- `console.log()` output is **never** returned to the agent +- Always return actionable data (IDs, counts, status) so subsequent calls can reference created objects + +## 4. Editor Mode + +`use_figma` works in **design mode** (editorType `"figma"`, the default). FigJam (`"figjam"`) and Slides (`"slides"`) have different sets of available node types — most design nodes are blocked in FigJam, and FigJam-only nodes are blocked in Slides. + +Available in design mode: Rectangle, Frame, Component, Text, Ellipse, Star, Line, Vector, Polygon, BooleanOperation, Slice, Page, Section, TextPath. + +**Blocked** in design mode: Sticky, Connector, ShapeWithText, CodeBlock, Slide, SlideRow, SlideGrid, InteractiveSlideElement, Webpage. + +Available in Slides mode: Rectangle, Frame, Component, Text, Ellipse, Star, Line, Vector, Polygon, BooleanOperation, Slice, Section, TextPath, Slide, SlideRow, SlideGrid, InteractiveSlideElement. + +**Blocked** in Slides mode: Sticky, Connector, ShapeWithText, CodeBlock, Webpage, Page. + +> **Slides note:** There is no dedicated read tool for Slides files yet. Use `use_figma` with read-only scripts for inspection (see Section 6 "Inspect first" pattern), and `get_screenshot` / `await node.screenshot()` for visual context. For Slides-specific API guidance, load the [figma-use-slides](../figma-use-slides/SKILL.md) skill. + +## 5. Efficient APIs — Prefer These Over Verbose Alternatives + +These APIs reduce boilerplate, eliminate ordering errors, and compress token output. **Always prefer them over the verbose alternatives.** + +### `node.query(selector)` — CSS-like node search + +Find nodes within a subtree using CSS-like selectors. Replaces verbose `findAll` + filter loops. + +```js +// BEFORE — verbose traversal +const texts = frame.findAll(n => n.type === 'TEXT' && n.name === 'Title') + +// AFTER — one-liner with query +const texts = frame.query('TEXT[name=Title]') +``` + +**Selector syntax:** +- Type: `FRAME`, `TEXT`, `RECTANGLE`, `ELLIPSE`, `COMPONENT`, `INSTANCE`, `SECTION` (case-insensitive) +- Attribute exact: `[name=Card]`, `[visible=true]`, `[opacity=0.5]` +- Attribute substring: `[name*=art]` (contains), `[name^=Header]` (starts-with), `[name$=Nav]` (ends-with) +- Dot-path traversal: `[fills.0.type=SOLID]`, `[fills.*.type=SOLID]` (wildcard index) +- Instance matching: `[mainComponent=nodeId]`, `[mainComponent.name=Button]` +- Combinators: `FRAME > TEXT` (direct child), `FRAME TEXT` (any descendant), `A + B` (adjacent sibling), `A ~ B` (general sibling) +- Pseudo-classes: `:first-child`, `:last-child`, `:nth-child(2)`, `:not(TYPE)`, `:is(FRAME, RECTANGLE)`, `:where(TEXT, ELLIPSE)` +- Node ID: `#nodeId` or bare GUID +- Comma: `TEXT, RECTANGLE` (union) +- Wildcard: `*` (any type) + +**QueryResult methods:** +| Method | Description | +|---|---| +| `.length` | Number of matched nodes | +| `.first()` | First matched node (or `null`) | +| `.last()` | Last matched node (or `null`) | +| `.toArray()` | Convert to regular array | +| `.each(fn)` | Iterate with callback, returns `this` for chaining | +| `.map(fn)` | Map to new array | +| `.filter(fn)` | Filter to new QueryResult | +| `.values(keys)` | Extract property values: `.values(['name', 'x', 'y'])` → `[{name, x, y}, ...]` | +| `.set(props)` | Set properties on all matched nodes (see `node.set()` below) | +| `.query(selector)` | Sub-query within matched nodes | +| `for...of` | Iterable — works in `for` loops | + +**Scope:** `node.query()` searches within that node's subtree. To search the whole page: `figma.currentPage.query('...')`. There is no global `figma.query()`. + +**Examples:** +```js +// Recolor all text inside cards +figma.currentPage.query('FRAME[name^=Card] TEXT').set({ + fills: [{type: 'SOLID', color: {r: 0.2, g: 0.2, b: 0.8}}] +}) + +// Get names and positions of all frames +return figma.currentPage.query('FRAME').values(['name', 'x', 'y']) + +// Find the first component named "Button" +const btn = figma.currentPage.query('COMPONENT[name=Button]').first() + +// Find all instances of a specific component +figma.currentPage.query(`INSTANCE[mainComponent=${compId}]`) + +// Find nodes with solid fills using dot-path traversal +figma.currentPage.query('[fills.0.type=SOLID]') +``` + +### `node.set(props)` — batch property updates + +Set multiple properties in one call. Returns `this` for chaining. + +```js +// BEFORE — one line per property +frame.opacity = 0.5 +frame.cornerRadius = 8 +frame.name = "Card" + +// AFTER — single call +frame.set({ opacity: 0.5, cornerRadius: 8, name: "Card" }) +``` + +**Priority key ordering:** `layoutMode` is always applied before other properties (like `width`/`height`) regardless of object key order. This prevents the common bug where `resize()` behaves differently depending on whether `layoutMode` is set. + +**Width/height handling:** `width` and `height` are routed through `node.resize()` automatically — setting `{ width: 200 }` calls `resize(200, currentHeight)`. + +**Chaining with query:** +```js +// Find all rectangles named "Divider" and update them +figma.currentPage.query('RECTANGLE[name=Divider]').set({ + fills: [{type: 'SOLID', color: {r: 0.9, g: 0.9, b: 0.9}}], + cornerRadius: 2 +}) +``` + +### `figma.createAutoLayout(direction?, props?)` — auto-layout frames + +Creates a frame with auto-layout already enabled and both axes hugging content. **Prefer this over `figma.createFrame()` for any container that needs auto-layout.** + +```js +// BEFORE — manual setup, easy to get ordering wrong +const frame = figma.createFrame() +frame.layoutMode = 'VERTICAL' +frame.primaryAxisSizingMode = 'AUTO' +frame.counterAxisSizingMode = 'AUTO' +frame.layoutSizingHorizontal = 'HUG' +frame.layoutSizingVertical = 'HUG' + +// AFTER — one call, layout ready +const frame = figma.createAutoLayout('VERTICAL') +``` + +Children can immediately use `layoutSizingHorizontal/Vertical = 'FILL'` after being appended — no need to set sizing modes manually. + +Accepts an optional props object as the first or second argument: +```js +figma.createAutoLayout({ name: 'Card', itemSpacing: 12 }) // HORIZONTAL + props +figma.createAutoLayout('VERTICAL', { name: 'Column', itemSpacing: 8 }) // VERTICAL + props +``` + +### `node.placeholder` — shimmer overlay for AI-in-progress feedback + +Sets a visual shimmer overlay on a node indicating work is in progress. **Always remove the shimmer when done** — leftover shimmers confuse users and indicate incomplete work. + +```js +// Mark as in-progress +frame.placeholder = true + +// ... build out the content ... + +// MUST remove when done — never leave shimmers on finished nodes +frame.placeholder = false +``` + +When building complex layouts, set `placeholder = true` on sections before populating them, then set `placeholder = false` on each section as it's completed. + +### `await node.screenshot(opts?)` — inline screenshots + +Capture a node as a PNG and return it inline in the response. Eliminates the need for a separate `get_screenshot` call. + +```js +// Take a screenshot of a frame (returned inline in the tool response) +await frame.screenshot() + +// Custom scale (default auto-scales: 0.5x or capped so max dimension ≤ 1024px) +await frame.screenshot({ scale: 2 }) + +// Include overlapping content from sibling nodes +await frame.screenshot({ contentsOnly: false }) +``` + +**When to use:** After creating or modifying nodes, call `screenshot()` to visually verify the result within the same script. No need for a separate `get_screenshot` call. + +**Auto-naming:** The image caption includes node metadata — `"Card (300x150 at 0,60).png"` — giving spatial context without parsing the image. + +**Default scaling:** Uses 0.5x scale, but automatically caps so the largest output dimension never exceeds 1024px. Explicit `{ scale: N }` bypasses the cap. + +## 6. Incremental Workflow (How to Avoid Bugs) + +The most common cause of bugs is trying to do too much in a single `use_figma` call. **Work in small steps and validate after each one.** + +### Key rules + +- **At most 10 logical operations per `use_figma` call.** A "logical operation" is creating a node, setting its properties, and parenting it. If you need to create 20 nodes, split across 2-3 calls. +- **Build top-down, starting with placeholders.** Create the outer structure first with `placeholder = true` on each section, then incrementally replace placeholders with real content in subsequent calls. + +### The pattern + +1. **Inspect first.** Before creating anything, run a read-only `use_figma` to discover what already exists in the file — pages, components, variables, naming conventions. Match what's there. +2. **Build the skeleton.** Create the top-level structure with placeholder sections. Set `placeholder = true` on each section so the user sees progress. +3. **Fill in sections incrementally.** In each subsequent call, populate one section and set its `placeholder = false` when done. Take a `screenshot()` to verify. +4. **Return IDs from every call.** Always `return` created node IDs, variable IDs, collection IDs as objects (e.g. `return { createdNodeIds: [...] }`). You'll need these as inputs to subsequent calls. +5. **Validate after each step.** Use `get_metadata` to verify structure (counts, names, hierarchy, positions). Use `await node.screenshot()` inline or `get_screenshot` after major milestones to catch visual issues. +6. **Fix before moving on.** If validation reveals a problem, fix it before proceeding to the next step. Don't build on a broken foundation. + +### Suggested step order for complex tasks + +``` +Step 1: Inspect file — discover existing pages, components, variables, conventions +Step 2: Create tokens/variables (if needed) + → validate with get_metadata +Step 3: Create individual components + → validate with get_metadata + get_screenshot +Step 4: Compose layouts from component instances + → validate with get_screenshot +Step 5: Final verification +``` + +### What to validate at each step + +| After... | Check with `get_metadata` | Check with `get_screenshot` | +|---|---|---| +| Creating variables | Collection count, variable count, mode names | — | +| Creating components | Child count, variant names, property definitions | Variants visible, not collapsed, grid readable | +| Binding variables | Node properties reflect bindings | Colors/tokens resolved correctly | +| Composing layouts | Instance nodes have mainComponent, hierarchy correct | No cropped/clipped text, no overlapping elements, correct spacing | + +## 7. Error Recovery & Self-Correction + +**`use_figma` is atomic — failed scripts do not execute.** If a script errors, no changes are made to the file. The file remains in the same state as before the call. This means there are no partial nodes, no orphaned elements from the failed script, and retrying after a fix is safe. + +### When `use_figma` returns an error + +1. **STOP.** Do not immediately fix the code and retry. +2. **Read the error message carefully.** Understand exactly what went wrong — wrong API usage, missing font, invalid property value, etc. +3. **If the error is unclear**, call `get_metadata` or `get_screenshot` to understand the current file state. +4. **Fix the script** based on the error message. +5. **Retry** the corrected script. + +### Common self-correction patterns + +| Error message | Likely cause | How to fix | +|---|---|---| +| `"not implemented"` | Used `figma.notify()` | Remove it — use `return` for output | +| `"node must be an auto-layout frame..."` | Set `FILL`/`HUG` before appending to auto-layout parent | Move `appendChild` before `layoutSizingX = 'FILL'` | +| `"Setting figma.currentPage is not supported"` | Used sync page setter (`figma.currentPage = page`) which does NOT work | Use `await figma.setCurrentPageAsync(page)` — the only way to switch pages | +| Property value out of range | Color channel > 1 (used 0–255 instead of 0–1) | Divide by 255 | +| `"Cannot read properties of null"` | Node doesn't exist (wrong ID, wrong page) | Check page context, verify ID | +| Script hangs / no response | Infinite loop or unresolved promise | Check for `while(true)` or missing `await`; ensure code terminates | +| `"The node with id X does not exist"` | Parent instance was implicitly detached by a child `detachInstance()`, changing IDs | Re-discover nodes by traversal from a stable (non-instance) parent frame | + +### When the script succeeds but the result looks wrong + +1. Call `get_metadata` to check structural correctness (hierarchy, counts, positions). +2. Call `get_screenshot` to check visual correctness. Look closely for cropped/clipped text (line heights cutting off content) and overlapping elements — these are common and easy to miss. +3. Identify the discrepancy — is it structural (wrong hierarchy, missing nodes) or visual (wrong colors, broken layout, clipped content)? +4. Write a targeted fix script that modifies only the broken parts — don't recreate everything. + +> For the full validation workflow, see [Validation & Error Recovery](references/validation-and-recovery.md). + +## 8. Pre-Flight Checklist + +Before submitting ANY `use_figma` call, verify: + +- [ ] Code uses `return` to send data back (NOT `figma.closePlugin()`) +- [ ] Code is NOT wrapped in an async IIFE (auto-wrapped for you) +- [ ] `return` value includes structured data with actionable info (IDs, counts) +- [ ] NO usage of `figma.notify()` anywhere +- [ ] NO usage of `console.log()` as output (use `return` instead) +- [ ] All colors use 0–1 range (not 0–255) +- [ ] Paint `color` objects use `{r, g, b}` only — no `a` field (opacity goes at the paint level: `{ type: 'SOLID', color: {...}, opacity: 0.5 }`) +- [ ] Fills/strokes are reassigned as new arrays (not mutated in place) +- [ ] Page switches use `await figma.setCurrentPageAsync(page)` (sync setter `figma.currentPage = page` does NOT work) +- [ ] `layoutSizingVertical/Horizontal = 'FILL'` is set AFTER `parent.appendChild(child)` +- [ ] `loadFontAsync()` called before any text property changes (use `listAvailableFontsAsync()` to verify font availability if unsure) +- [ ] Style names have already been verified via `listAvailableFontsAsync()` — NOT guessed from memory (`"SemiBold"` vs `"Semi Bold"` is a common footgun) +- [ ] For `FONT_FAMILY`-scoped variables: every value across every relevant mode is loaded before `setBoundVariable("fontFamily", …)`, `setValueForMode`, or `setExplicitVariableModeForCollection` +- [ ] `lineHeight`/`letterSpacing` use `{unit, value}` format (not bare numbers) +- [ ] `resize()` is called BEFORE setting sizing modes (resize resets them to FIXED) +- [ ] For multi-step workflows: IDs from previous calls are passed as string literals (not variables) +- [ ] New top-level nodes are positioned away from (0,0) to avoid overlapping existing content +- [ ] ALL created/mutated node IDs are collected and included in the `return` value +- [ ] Every async call (`loadFontAsync`, `setCurrentPageAsync`, `importComponentByKeyAsync`, etc.) is `await`ed — no fire-and-forget Promises + +## 9. Discover Conventions Before Creating + +**Always inspect the Figma file before creating anything.** Different files use different naming conventions, variable structures, and component patterns. Your code should match what's already there, not impose new conventions. + +When in doubt about any convention (naming, scoping, structure), check the Figma file first, then the user's codebase. Only fall back to common patterns when neither exists. + +### Quick inspection scripts + +**List all pages and top-level nodes:** +```js +const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`); +return pages.join('\n'); +``` + +**List existing components across all pages:** +```js +const results = []; +for (const page of figma.root.children) { + await figma.setCurrentPageAsync(page); + page.findAll(n => { + if (n.type === 'COMPONENT' || n.type === 'COMPONENT_SET') + results.push(`[${page.name}] ${n.name} (${n.type}) id=${n.id}`); + return false; + }); +} +return results.join('\n'); +``` + +**List existing variable collections and their conventions:** +```js +const collections = await figma.variables.getLocalVariableCollectionsAsync(); +const results = collections.map(c => ({ + name: c.name, id: c.id, + varCount: c.variableIds.length, + modes: c.modes.map(m => m.name) +})); +return results; +``` + +## 10. Reference Docs + +Load these as needed based on what your task involves: + +| Doc | When to load | What it covers | +|-----|-------------|----------------| +| [gotchas.md](references/gotchas.md) | Before any `use_figma` | Every known pitfall with WRONG/CORRECT code examples | +| [common-patterns.md](references/common-patterns.md) | Need working code examples | Script scaffolds: shapes, text, auto-layout, variables, components, multi-step workflows | +| [plugin-api-patterns.md](references/plugin-api-patterns.md) | Creating/editing nodes | Fills, strokes, Auto Layout, effects, grouping, cloning, styles | +| [api-reference.md](references/api-reference.md) | Need exact API surface | Node creation, variables API, core properties, what works and what doesn't | +| [validation-and-recovery.md](references/validation-and-recovery.md) | Multi-step writes or error recovery | `get_metadata` vs `get_screenshot` workflow, mandatory error recovery steps | +| [component-patterns.md](references/component-patterns.md) | Creating components/variants | combineAsVariants, component properties, INSTANCE_SWAP, variant layout, discovering existing components, metadata traversal | +| [variable-patterns.md](references/variable-patterns.md) | Creating/binding variables | Collections, modes, scopes, aliasing, binding patterns, discovering existing variables | +| [text-style-patterns.md](references/text-style-patterns.md) | Creating/applying text styles | Type ramps, font discovery via `listAvailableFontsAsync`, listing styles, applying styles to nodes | +| [effect-style-patterns.md](references/effect-style-patterns.md) | Creating/applying effect styles | Drop shadows, listing styles, applying styles to nodes | +| [plugin-api-standalone.index.md](references/plugin-api-standalone.index.md) | Need to understand the full API surface | Index of all types, methods, and properties in the Plugin API | +| [plugin-api-standalone.d.ts](references/plugin-api-standalone.d.ts) | Need exact type signatures | Full typings file — grep for specific symbols, don't load all at once | + +## 11. Snippet examples + +You will see snippets throughout documentation here. These snippets contain useful plugin API code that can be repurposed. Use them as is, or as starter code as you go. If there are key concepts that are best documented as generic snippets, call them out and write to disk so you can reuse in the future. diff --git a/.agents/skills/figma-use/references/api-reference.md b/.agents/skills/figma-use/references/api-reference.md new file mode 100644 index 00000000000..16c1258f4db --- /dev/null +++ b/.agents/skills/figma-use/references/api-reference.md @@ -0,0 +1,337 @@ +# Figma Plugin API Reference + +> Part of the [use_figma skill](../SKILL.md). What works and what doesn't in the `use_figma` environment. + +## Contents + +- Node Creation +- Grouping and Boolean Operations +- Library Imports +- Variables API +- Core Properties +- Node Manipulation +- Descriptions and Documentation Links +- SVG and Images +- Utilities and Plugin Lifecycle +- Node Traversal +- Unsupported APIs + + +## Node Creation (Design Mode) + +```js +figma.createRectangle() +figma.createFrame() +figma.createAutoLayout() // Frame with auto layout enabled, both axes hug — prefer over createFrame() for layout containers +figma.createAutoLayout("VERTICAL") // Same but vertical direction +figma.createComponent() // Creates a ComponentNode +figma.createText() +figma.createEllipse() +figma.createStar() +figma.createLine() +figma.createVector() +figma.createPolygon() +figma.createBooleanOperation() +figma.createSlice() +figma.createPage() // Page node can be created, but child persistence is limited in use_figma +figma.createSection() +figma.createTextPath() +``` + +## Grouping & Boolean Operations + +```js +figma.group(nodes, parent, index?) // Group nodes +figma.flatten(nodes, parent?, index?) // Flatten to vector +figma.union(nodes, parent?, index?) // Boolean union +figma.subtract(nodes, parent?, index?) // Boolean subtract +figma.intersect(nodes, parent?, index?) // Boolean intersect +figma.exclude(nodes, parent?, index?) // Boolean exclude +figma.combineAsVariants(components, parent?) // Combine ComponentNodes into ComponentSet (Design/Sites only) +``` + +## Library Component Import + +These methods import components from **team libraries** (not the same file you're working in). For components in the current file, use `use_figma` with `figma.getNodeByIdAsync()` or `findOne()`/`findAll()` to locate them directly. + +```js +// Import a published component from a team library by key +const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY") +const instance = comp.createInstance() + +// Import a published component set from a team library by key +const compSet = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY") +const variant = + compSet.children.find((c) => c.type === "COMPONENT" && c.name.includes("size=md")) || + compSet.defaultVariant +const variantInstance = variant.createInstance() +``` + +## Library Style Import (Team Libraries) + +These methods import styles from **team libraries** (not the same file). For styles in the current file, use `figma.getLocalPaintStyles()`, `figma.getLocalTextStyles()`, etc. + +```js +// Import a published style from a team library by key +const style = await figma.importStyleByKeyAsync("STYLE_KEY") + +// Apply the imported style to a node +await node.setFillStyleIdAsync(style.id) // for PaintStyle as fill +await node.setStrokeStyleIdAsync(style.id) // for PaintStyle as stroke +await node.setTextStyleIdAsync(style.id) // for TextStyle +await node.setEffectStyleIdAsync(style.id) // for EffectStyle +await node.setGridStyleIdAsync(style.id) // for GridStyle +``` + +## Library Variable Import (Team Libraries) + +This imports variables from **team libraries** (not the same file). For variables in the current file, use `figma.variables.getLocalVariablesAsync()` or `figma.variables.getVariableByIdAsync()`. + +```js +// Import a published variable from a team library by key +const variable = await figma.variables.importVariableByKeyAsync("VARIABLE_KEY") + +// Bind the imported variable to node properties +node.setBoundVariable("width", variable) // FLOAT variable + +// Bind to fills/strokes (COLOR variable) — returns a NEW paint, must capture it +const newPaint = figma.variables.setBoundVariableForPaint(paintCopy, "color", variable) +node.fills = [newPaint] +``` + +## Variables API + +```js +// Collections +const collection = figma.variables.createVariableCollection("Name") +collection.name // Get/set name +collection.modes // Array of {modeId, name} — starts with 1 mode +collection.addMode("Dark") // Returns new modeId string +collection.renameMode(modeId, "Light") + +// Variables +const variable = figma.variables.createVariable("name", collection, "COLOR") +// ^ must be a collection object (passing an ID string is deprecated) +// resolvedType: "COLOR" | "FLOAT" | "STRING" | "BOOLEAN" +variable.setValueForMode(modeId, value) + +// Scopes — controls where variable appears in property pickers +variable.scopes = ["FRAME_FILL", "SHAPE_FILL"] // only fill pickers +variable.scopes = ["TEXT_FILL"] // only text color picker +variable.scopes = ["STROKE_COLOR"] // only stroke picker +variable.scopes = [] // hidden from all pickers (use for primitives) +// All valid scope values: +// ALL_SCOPES, TEXT_CONTENT, CORNER_RADIUS, WIDTH_HEIGHT, GAP, +// ALL_FILLS, FRAME_FILL, SHAPE_FILL, TEXT_FILL, +// STROKE_COLOR, STROKE_FLOAT, EFFECT_FLOAT, EFFECT_COLOR, +// OPACITY, FONT_FAMILY, FONT_STYLE, FONT_WEIGHT, FONT_SIZE, +// LINE_HEIGHT, LETTER_SPACING, PARAGRAPH_SPACING, PARAGRAPH_INDENT + +// Querying (always use the Async variants — sync versions are deprecated) +await figma.variables.getVariableByIdAsync(id) +await figma.variables.getLocalVariablesAsync(resolvedType?) +await figma.variables.getVariableCollectionByIdAsync(id) +await figma.variables.getLocalVariableCollectionsAsync() + +// Binding variables to paints (COLOR variables) +const newPaint = figma.variables.setBoundVariableForPaint(paintCopy, "color", variable) +// ⚠️ Returns a NEW paint — must capture return value! +node.fills = [newPaint] + +// Binding variables to effects (COLOR/FLOAT variables) +const newEffect = figma.variables.setBoundVariableForEffect(effectCopy, field, variable) +// field for shadows: "color" (COLOR), "radius" | "spread" | "offsetX" | "offsetY" (FLOAT) +// field for blurs: "radius" (FLOAT) +// ⚠️ Returns a NEW effect — must capture return value! +node.effects = [newEffect] + +// Binding variables to layout grids (FLOAT variables) +const newGrid = figma.variables.setBoundVariableForLayoutGrid(gridCopy, field, variable) +// field: "sectionSize" | "offset" | "count" | "gutterSize" +// ⚠️ Returns a NEW layout grid — must capture return value! +node.layoutGrids = [newGrid] + +// Binding variables to node properties (FLOAT/STRING/BOOLEAN) +// Layout & sizing (FLOAT): +node.setBoundVariable("width", variable) +node.setBoundVariable("height", variable) +node.setBoundVariable("minWidth", variable) +node.setBoundVariable("maxWidth", variable) +node.setBoundVariable("minHeight", variable) +node.setBoundVariable("maxHeight", variable) +node.setBoundVariable("paddingLeft", variable) +node.setBoundVariable("paddingRight", variable) +node.setBoundVariable("paddingTop", variable) +node.setBoundVariable("paddingBottom", variable) +node.setBoundVariable("itemSpacing", variable) +node.setBoundVariable("counterAxisSpacing", variable) +// Corner radii (FLOAT) — use individual corners, NOT cornerRadius: +node.setBoundVariable("topLeftRadius", variable) +node.setBoundVariable("topRightRadius", variable) +node.setBoundVariable("bottomLeftRadius", variable) +node.setBoundVariable("bottomRightRadius", variable) +// Other (FLOAT): +node.setBoundVariable("opacity", variable) +node.setBoundVariable("strokeWeight", variable) +// ⚠️ fontSize, fontWeight, lineHeight are NOT bindable via setBoundVariable +// — set these directly as values on text nodes + +// Aliases +figma.variables.createVariableAlias(variable) + +// Explicit modes — CRITICAL for variant components +node.setExplicitVariableModeForCollection(collection, modeId) // pass collection object, NOT an ID string +// Without this, all nodes use the default (first) mode of the collection +``` + +## Core Properties + +```js +figma.root // DocumentNode +figma.currentPage // Current page — READ ONLY; the sync setter (figma.currentPage = page) does NOT work and throws +figma.setCurrentPageAsync(page) // Switch page and load its content (MUST await) — this is the ONLY way to change pages +figma.fileKey // File key string +figma.mixed // Mixed sentinel value +``` + +## Node Manipulation + +```js +// Fills & Strokes (read-only arrays — must clone) +node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }] +node.strokes = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }] +node.strokeWeight = 1 +node.strokeAlign = 'INSIDE' // 'INSIDE' | 'CENTER' | 'OUTSIDE' + +// Effects +node.effects = [{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.25}, offset:{x:0,y:4}, radius:4, visible:true }] + +// Layout +node.layoutMode = 'HORIZONTAL' // 'NONE' | 'HORIZONTAL' | 'VERTICAL' +node.primaryAxisAlignItems = 'CENTER' // 'MIN' | 'CENTER' | 'MAX' | 'SPACE_BETWEEN' +node.counterAxisAlignItems = 'CENTER' // 'MIN' | 'CENTER' | 'MAX' | 'BASELINE' +node.paddingLeft = 8 +node.paddingRight = 8 +node.paddingTop = 4 +node.paddingBottom = 4 +node.itemSpacing = 4 +node.layoutSizingHorizontal = 'HUG' // 'FIXED' | 'HUG' | 'FILL' +node.layoutSizingVertical = 'HUG' // 'FIXED' | 'HUG' | 'FILL' + +// Sizing +node.resize(width, height) // ⚠️ Resets sizing modes to FIXED +node.resizeWithoutConstraints(width, height) // Doesn't affect constraints + +// Corner radius +node.cornerRadius = 8 + +// Visibility & Opacity +node.visible = true +node.opacity = 0.5 + +// Naming & Hierarchy +node.name = "My Node" +parent.appendChild(child) +parent.insertChild(index, child) +node.remove() +``` + +## Descriptions & Documentation Links + +```js +// Description — plain text, shown in Figma's component panel +node.description = "A short summary of this component's purpose and usage." + +// Documentation links — array of {uri, label} shown as clickable links +componentSet.documentationLinks = [ + { uri: "https://example.com/docs", label: "Component Docs" } +] +// ⚠️ uri MUST be a valid URL (https://...) — relative paths will throw +``` + +## SVG Import + +```js +const svgNode = figma.createNodeFromSvg('...') +``` + +## Images + +**`upload_assets` is the ONLY supported way to upload images into a Figma file** — Design, FigJam, and Slides all share this path. **Do NOT use `figma.createImage()` or `figma.createImageAsync()` from inside `use_figma`.** Both are unsupported as image-upload entry points and will be removed from agent flows; `use_figma` has no network access (so `createImageAsync(src)` cannot fetch URLs) and bytes inside the script are not durable assets in the file. + +The `upload_assets` tool is the ONLY supported way. It returns single-use upload URLs that you POST raw bytes to, and the response contains an `imageHash` plus placement details. Server-side commit and canvas placement happen automatically. Pass `nodeId` (with `count: 1`) to set the upload as a fill on an existing node directly, or omit `nodeId` to place the image on the canvas as a new layer. + +```text +upload_assets({ fileKey, count: 1, nodeId, scaleMode: 'FILL' }) + → { uploads: [{ submitUrl }], instructions: "..." } +// Then POST the image bytes to submitUrl (multipart/form-data 'file' field +// preferred — the filename becomes the layer name). +``` + +### Re-using an existing imageHash (not an upload) + +Once an image is in the file via `upload_assets`, you can reference its `imageHash` from another node without re-uploading. This is the only legitimate use of an `imageHash` inside `use_figma`: + +```js +// Re-using an imageHash that already exists on another node in the file +node.fills = [{ type: 'IMAGE', scaleMode: 'FILL', imageHash: 'hash_from_existing_node' }] +``` + +For anything originating outside the file (URLs, local files, generated bytes, screenshots) — always call `upload_assets` first. + +## Fonts + +```js +// Discover all available fonts and their exact style strings +const allFonts = await figma.listAvailableFontsAsync() // Font[] — each has { fontName: { family, style } } +const interStyles = allFonts.filter(f => f.fontName.family === "Inter") + +// MUST load a font before any text property edit +await figma.loadFontAsync({ family: "Inter", style: "Regular" }) + +// Check if the file has missing fonts +figma.hasMissingFont // boolean +``` + +## Utilities + +```js +figma.base64Encode(uint8Array) // Uint8Array → base64 string +figma.base64Decode(base64String) // base64 string → Uint8Array +figma.createComponentFromNode(node) // Convert existing node to component (Design/Sites only) +``` + +## Plugin Lifecycle + +Scripts are automatically wrapped in an async IIFE with error handling. Use `return` to send data back: + +```js +return { nodeId: frame.id } // Return object — auto-serialized to JSON +return "success message" // Return string +// Errors are auto-captured — no try/catch or closePlugin needed +``` + +## Node Traversal + +```js +node.findAll(pred?) // Find all descendants matching predicate +node.findOne(pred?) // Find first descendant matching predicate +node.findChildren(pred?) // Find direct children matching predicate +node.findChild(pred?) // Find first direct child matching predicate +node.children // Direct children array +node.parent // Parent node +``` + +--- + +## What Does NOT Work + +| API | Status | +|-----|--------| +| `figma.notify()` | **Throws "not implemented"** — most common mistake | +| `figma.showUI()` | No-op (silently ignored) | +| `figma.openExternal()` | No-op (silently ignored) | +| `figma.loadAllPagesAsync()` | Not implemented | +| `figma.variables.extendLibraryCollectionByKeyAsync()` | Not implemented | +| `figma.teamLibrary.*` | Not implemented (requires the team-library backend) | +| `figma.getLocalComponents*()` | **Does not exist** — unlike styles, there is no `getLocalComponents()` or `getLocalComponentSetsAsync()` (or any `getLocalComponent*` variant). Use `findAll(n => n.type === 'COMPONENT')` / `findAll(n => n.type === 'COMPONENT_SET')` to locate components in the current file. | diff --git a/.agents/skills/figma-use/references/common-patterns.md b/.agents/skills/figma-use/references/common-patterns.md new file mode 100644 index 00000000000..4cdcdc27859 --- /dev/null +++ b/.agents/skills/figma-use/references/common-patterns.md @@ -0,0 +1,438 @@ +# Common Patterns + +> Part of the [use_figma skill](../SKILL.md). Working code examples for frequently used operations. + +## Contents + +- Basic Script Structure +- Create a Styled Shape +- Create a Text Node +- Create Frame with Auto-Layout +- Create Variable Collections and Bindings +- Create Components and Import by Key +- Component Sets with Variable Modes +- Multi-Step Large ComponentSet Pattern +- Read Existing Nodes and Return Data + + +## Basic Script Structure + +```js +const createdNodeIds = [] +const mutatedNodeIds = [] + +// Your code here — track every node you create or mutate +// createdNodeIds.push(newNode.id) +// mutatedNodeIds.push(existingNode.id) + +return { + success: true, + createdNodeIds, + mutatedNodeIds, + // Plus any other useful data for subsequent calls + count: createdNodeIds.length +} +``` + +## Create a Styled Shape + +```js +// Find clear space to the right of existing content +const page = figma.currentPage +let maxX = 0 +for (const child of page.children) { + maxX = Math.max(maxX, child.x + child.width) +} + +const rect = figma.createRectangle() +rect.name = "Blue Box" +rect.resize(200, 100) +rect.fills = [{ type: 'SOLID', color: { r: 0.047, g: 0.549, b: 0.914 } }] +rect.cornerRadius = 8 +rect.x = maxX + 100 // offset from existing content +rect.y = 0 +figma.currentPage.appendChild(rect) +return { nodeId: rect.id } +``` + +## Create a Text Node + +```js +// Find clear space to the right of existing content +const page = figma.currentPage +let maxX = 0 +for (const child of page.children) { + maxX = Math.max(maxX, child.x + child.width) +} + +await figma.loadFontAsync({ family: "Inter", style: "Regular" }) +const text = figma.createText() +text.characters = "Hello World" +text.fontSize = 16 +text.fills = [{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }] +text.textAutoResize = 'WIDTH_AND_HEIGHT' +text.x = maxX + 100 +text.y = 0 +figma.currentPage.appendChild(text) +return { nodeId: text.id } +``` + +## Create Frame with Auto-Layout + +```js +// Find clear space to the right of existing content +const page = figma.currentPage +let maxX = 0 +for (const child of page.children) { + maxX = Math.max(maxX, child.x + child.width) +} + +const frame = figma.createAutoLayout('VERTICAL') +frame.name = "Card" +frame.primaryAxisAlignItems = 'MIN' +frame.counterAxisAlignItems = 'MIN' +frame.paddingLeft = 16 +frame.paddingRight = 16 +frame.paddingTop = 12 +frame.paddingBottom = 12 +frame.itemSpacing = 8 +frame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }] +frame.cornerRadius = 8 +frame.x = maxX + 100 +frame.y = 0 +figma.currentPage.appendChild(frame) +return { nodeId: frame.id } +``` + +## Create Variable Collection with Multiple Modes + +```js +const collection = figma.variables.createVariableCollection("Theme/Colors") +// Rename the default mode +collection.renameMode(collection.modes[0].modeId, "Light") +const darkModeId = collection.addMode("Dark") +const lightModeId = collection.modes[0].modeId + +const bgVar = figma.variables.createVariable("bg", collection, "COLOR") +bgVar.setValueForMode(lightModeId, { r: 1, g: 1, b: 1, a: 1 }) +bgVar.setValueForMode(darkModeId, { r: 0.1, g: 0.1, b: 0.1, a: 1 }) + +const textVar = figma.variables.createVariable("text", collection, "COLOR") +textVar.setValueForMode(lightModeId, { r: 0, g: 0, b: 0, a: 1 }) +textVar.setValueForMode(darkModeId, { r: 1, g: 1, b: 1, a: 1 }) + +return { + collectionId: collection.id, + lightModeId, + darkModeId, + bgVarId: bgVar.id, + textVarId: textVar.id +} +``` + +## Bind Color Variable to a Fill + +```js +const variable = await figma.variables.getVariableByIdAsync("VariableID:1:2") +const rect = figma.createRectangle() +const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } } + +// setBoundVariableForPaint returns a NEW paint — capture it! +const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", variable) +rect.fills = [boundPaint] + +return { nodeId: rect.id } +``` + +## Create Component Variants with Component Properties + +Component properties (TEXT, BOOLEAN, INSTANCE_SWAP) MUST be added inside the per-variant loop, BEFORE `combineAsVariants`. The component set inherits them from its children. + +```js +await figma.loadFontAsync({ family: "Inter", style: "Regular" }) + +// Assume defaultIconComp is an existing icon component (discovered earlier) +const defaultIconComp = figma.getNodeById('ICON_COMPONENT_ID') + +const components = [] +const variants = ["primary", "secondary"] + +for (const variant of variants) { + const comp = figma.createComponent() + comp.name = `variant=${variant}` + comp.layoutMode = 'HORIZONTAL' + comp.primaryAxisAlignItems = 'CENTER' + comp.counterAxisAlignItems = 'CENTER' + comp.paddingLeft = 12 + comp.paddingRight = 12 + comp.paddingTop = 8 + comp.paddingBottom = 8 + comp.layoutSizingHorizontal = 'HUG' + comp.layoutSizingVertical = 'HUG' + comp.cornerRadius = 6 + comp.itemSpacing = 8 + + // TEXT property — label + const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button') + const label = figma.createText() + label.characters = "Button" + label.fontSize = 14 + comp.appendChild(label) + label.componentPropertyReferences = { characters: labelKey } + + // BOOLEAN + INSTANCE_SWAP — icon slot + const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', false) + const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', defaultIconComp.id) + const iconInstance = defaultIconComp.createInstance() + comp.insertChild(0, iconInstance) // icon before label + iconInstance.componentPropertyReferences = { + visible: showIconKey, + mainComponent: iconSlotKey + } + + components.push(comp) +} + +const componentSet = figma.combineAsVariants(components, figma.currentPage) +componentSet.name = "Button" + +// Layout variants in a row after combining (they stack at 0,0 by default) +const colW = 140 +componentSet.children.forEach((child, i) => { + child.x = i * colW + child.y = 0 +}) +// Resize from actual child bounds — formula-based sizing is error-prone +let maxX = 0, maxY = 0 +for (const c of componentSet.children) { + maxX = Math.max(maxX, c.x + c.width) + maxY = Math.max(maxY, c.y + c.height) +} +componentSet.resizeWithoutConstraints(maxX + 40, maxY + 40) + +return { + componentSetId: componentSet.id, + componentIds: components.map(c => c.id) +} +``` + +## Import a Component by Key (Team Libraries) + +`importComponentByKeyAsync` and `importComponentSetByKeyAsync` import components from **team libraries** (not the same file you're working in). For components in the current file, use `figma.getNodeByIdAsync()` or `findOne()`/`findAll()` to locate them directly. + +```js +// Import a single published component by key +const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY") +const instance = comp.createInstance() +instance.x = 40 +instance.y = 40 +figma.currentPage.appendChild(instance) + +// Import a published component set by key and select a variant +const compSet = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY") +const variant = + compSet.children.find((c) => + c.type === "COMPONENT" && c.name.includes("size=md") + ) || compSet.defaultVariant + +const variantInstance = variant.createInstance() +variantInstance.x = 240 +variantInstance.y = 40 +figma.currentPage.appendChild(variantInstance) + +return { + componentId: comp.id, + componentSetId: compSet.id, + placedInstanceIds: [instance.id, variantInstance.id] +} +``` + +## Component Set with Variable Modes (Full Pattern) + +```js +await figma.loadFontAsync({ family: "Inter", style: "Medium" }) + +// 1. Create color collection with modes per variant +const colors = figma.variables.createVariableCollection("Component/Colors") +colors.renameMode(colors.modes[0].modeId, "primary") +const primaryMode = colors.modes[0].modeId +const secondaryMode = colors.addMode("secondary") + +const bgVar = figma.variables.createVariable("bg", colors, "COLOR") +bgVar.setValueForMode(primaryMode, { r: 0, g: 0.4, b: 0.9, a: 1 }) +bgVar.setValueForMode(secondaryMode, { r: 0, g: 0, b: 0, a: 0 }) + +const textVar = figma.variables.createVariable("text-color", colors, "COLOR") +textVar.setValueForMode(primaryMode, { r: 1, g: 1, b: 1, a: 1 }) +textVar.setValueForMode(secondaryMode, { r: 0.1, g: 0.1, b: 0.1, a: 1 }) + +// 2. Create components with variable bindings +const modeMap = { primary: primaryMode, secondary: secondaryMode } +const components = [] + +for (const [variantName, modeId] of Object.entries(modeMap)) { + const comp = figma.createComponent() + comp.name = "variant=" + variantName + comp.layoutMode = "HORIZONTAL" + comp.primaryAxisAlignItems = "CENTER" + comp.counterAxisAlignItems = "CENTER" + comp.paddingLeft = 12; comp.paddingRight = 12 + comp.layoutSizingHorizontal = "HUG" + comp.layoutSizingVertical = "HUG" + comp.cornerRadius = 6 + + // Bind background fill to variable + const bgPaint = figma.variables.setBoundVariableForPaint( + { type: "SOLID", color: { r: 0, g: 0, b: 0 } }, "color", bgVar + ) + comp.fills = [bgPaint] + + // Add text with bound color + const label = figma.createText() + label.fontName = { family: "Inter", style: "Medium" } + label.characters = "Button" + label.fontSize = 14 + const textPaint = figma.variables.setBoundVariableForPaint( + { type: "SOLID", color: { r: 0, g: 0, b: 0 } }, "color", textVar + ) + label.fills = [textPaint] + comp.appendChild(label) + + // 3. CRITICAL: Set explicit mode so this variant renders correctly + comp.setExplicitVariableModeForCollection(colors, modeId) + + components.push(comp) +} + +// 4. Combine into component set +const componentSet = figma.combineAsVariants(components, figma.currentPage) +componentSet.name = "Button" + +return { + componentSetId: componentSet.id, + colorCollectionId: colors.id +} +``` + +## Large ComponentSet with Variable Modes (Multi-Step Pattern) + +For component sets with many variants (50+), split into multiple `use_figma` calls: + +**Call 1: Create variable collections and return IDs** + +```js +// Hex-to-0-1 helper +const hex = (h) => { + if (!h) return { r: 0, g: 0, b: 0, a: 0 }; // transparent + return { + r: parseInt(h.slice(1,3), 16) / 255, + g: parseInt(h.slice(3,5), 16) / 255, + b: parseInt(h.slice(5,7), 16) / 255, + a: 1 + }; +}; + +const coll = figma.variables.createVariableCollection("MyComponent/Colors"); +coll.renameMode(coll.modes[0].modeId, "mode1"); +const mode2Id = coll.addMode("mode2"); + +// Create variables from data map +const colorData = { "bg/default": ["#0B6BCB", "#636B74"], /* ... */ }; +const modeOrder = ["mode1", "mode2"]; +const modeIds = { mode1: coll.modes[0].modeId, mode2: mode2Id }; +const varIds = {}; + +for (const [name, values] of Object.entries(colorData)) { + const v = figma.variables.createVariable(name, coll, "COLOR"); + values.forEach((hex_val, i) => { + v.setValueForMode(modeIds[modeOrder[i]], hex_val ? hex(hex_val) : { r:0, g:0, b:0, a:0 }); + }); + varIds[name] = v.id; +} + +// Return ALL IDs — needed by subsequent calls +return { collId: coll.id, modeIds, varIds }; +``` + +**Call 2: Create components using stored IDs, combine and layout** + +```js +await figma.loadFontAsync({ family: "Inter", style: "Semi Bold" }); + +// Paste IDs from Call 1 as literals +const collId = "VariableCollectionId:X:Y"; +const modeIds = { mode1: "X:0", mode2: "X:1" }; +const varIds = { /* ... from Call 1 ... */ }; + +const getVar = async (id) => await figma.variables.getVariableByIdAsync(id); +const bindColor = async (varId) => figma.variables.setBoundVariableForPaint( + { type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', await getVar(varId) +); +const collection = await figma.variables.getVariableCollectionByIdAsync(collId); + +const components = []; +for (const mode of ["mode1", "mode2"]) { + for (const state of ["default", "hover"]) { + const comp = figma.createComponent(); + comp.name = `mode=${mode}, state=${state}`; + comp.layoutMode = 'HORIZONTAL'; + comp.primaryAxisAlignItems = 'CENTER'; + comp.counterAxisAlignItems = 'CENTER'; + comp.layoutSizingHorizontal = 'HUG'; + comp.layoutSizingVertical = 'HUG'; + comp.fills = [await bindColor(varIds[`bg/${state}`])]; + comp.setExplicitVariableModeForCollection(collection, modeIds[mode]); + // ... add text children ... + components.push(comp); + } +} + +// Combine — all children stack at (0,0)! +const cs = figma.combineAsVariants(components, figma.currentPage); +cs.name = "MyComponent"; + +// CRITICAL: layout variants in a structured grid mapped to variant axes. +const stateOrder = ["default", "hover"]; +const modeOrder2 = ["mode1", "mode2"]; +const colW = 140, rowH = 56; + +for (const child of cs.children) { + const props = Object.fromEntries( + child.name.split(', ').map(p => p.split('=')) + ); + const col = stateOrder.indexOf(props.state); + const row = modeOrder2.indexOf(props.mode); + child.x = col * colW; + child.y = row * rowH; +} +// Resize from actual child bounds +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 + 40, maxY + 40); + +// Wrap in section +const section = figma.createSection(); +section.name = "MyComponent Section"; +section.appendChild(cs); +section.resize(cs.width + 200, cs.height + 200); + +return { csId: cs.id, count: components.length }; +``` + +## Read Existing Nodes and Return Data + +```js +const page = figma.currentPage +const nodes = page.findAll(n => n.type === 'FRAME') +const data = nodes.map(n => ({ + id: n.id, + name: n.name, + width: n.width, + height: n.height, + childCount: n.children?.length || 0 +})) +return { frames: data } +``` diff --git a/.agents/skills/figma-use/references/component-patterns.md b/.agents/skills/figma-use/references/component-patterns.md new file mode 100644 index 00000000000..75a9be53b0f --- /dev/null +++ b/.agents/skills/figma-use/references/component-patterns.md @@ -0,0 +1,554 @@ +# Component & Variant API Patterns + +> Part of the [use_figma skill](../SKILL.md). How to correctly use the Plugin API for components, variants, and component properties. +> +> For design system context (when to use variants vs properties, code-to-Figma translation, property model), see [wwds-components](working-with-design-systems/wwds-components.md). + +## Contents + +- Creating a Component +- Combining Components into a Component Set (Variants) +- Laying Out Variants After combineAsVariants (Required) +- Component Properties: addComponentProperty API +- Linking Properties to Child Nodes (Required) +- INSTANCE_SWAP: Avoiding Variant Explosion +- Slots: createSlot and SLOT Properties +- Discovering Existing Conventions in the File +- Importing Components by Key +- Working with Instances (finding variants, setProperties, text overrides, detachInstance) + + +## Creating a Component + +`figma.createComponent()` returns a `ComponentNode`, which behaves like a `FrameNode` but can be published, instanced, and combined into variant sets. + +```javascript +const comp = figma.createComponent(); +comp.name = "MyComponent"; +comp.layoutMode = "HORIZONTAL"; +comp.primaryAxisAlignItems = "CENTER"; +comp.counterAxisAlignItems = "CENTER"; +comp.paddingLeft = 12; +comp.paddingRight = 12; +comp.layoutSizingHorizontal = "HUG"; +comp.layoutSizingVertical = "HUG"; +comp.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 } }]; +``` + +## Combining Components into a Component Set (Variants) + +`figma.combineAsVariants(components, parent)` takes an array of `ComponentNode`s (not frames — frames will throw) and groups them into a `ComponentSetNode`. + +Variant names use a `Property=Value` format. Every unique combination must exist as a child component — missing ones show as blank gaps in the variant picker. + +```javascript +// Each component's name encodes its variant properties +const comp1 = figma.createComponent(); +comp1.name = "size=md, style=primary"; +const comp2 = figma.createComponent(); +comp2.name = "size=md, style=secondary"; + +const componentSet = figma.combineAsVariants([comp1, comp2], figma.currentPage); +componentSet.name = "Button"; +``` + +**Before creating variants, inspect the file** for existing naming patterns. Different files use different conventions (`State=Default` vs `state=default` vs `State/Default`). Always match what's already there. + +## Laying Out Variants After combineAsVariants (Required) + +After `combineAsVariants`, all children stack at `(0, 0)`. You **must** position them or the component set will appear as a single collapsed element with all variants overlapping. + +```javascript +const cs = figma.combineAsVariants(components, figma.currentPage); + +// Simple row layout +cs.children.forEach((child, i) => { + child.x = i * 150; + child.y = 0; +}); + +// CRITICAL: resize the component set from actual child bounds +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 + 40, maxY + 40); +``` + +For multi-axis variants (e.g., size × style × state), parse the child's name to determine grid position: + +```javascript +for (const child of cs.children) { + const props = Object.fromEntries( + child.name.split(', ').map(p => p.split('=')) + ); + const col = stateValues.indexOf(props.state); + const row = styleValues.indexOf(props.style); + child.x = col * colWidth; + child.y = row * rowHeight; +} +``` + +## Component Properties: addComponentProperty API + +`addComponentProperty` adds a TEXT, BOOLEAN, or INSTANCE_SWAP property to a component. It returns a **string key** (e.g., `"label#4:0"`) — never hardcode or guess this key. + +```javascript +// Returns the key as a string — capture it! +const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Default text'); +const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true); +const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComponentId); +``` + +**Timing**: Add component properties to each variant component **before** calling `combineAsVariants`. After combining, the component set inherits all properties from its children. Do not add properties to the `ComponentSetNode` directly. + +## Linking Properties to Child Nodes (Required) + +A property that is added but not linked to a child node does **nothing**. You must set `componentPropertyReferences` on the child: + +```javascript +// TEXT property → link to a text node's characters +const labelKey = comp.addComponentProperty('Label', 'TEXT', 'Button'); +const textNode = figma.createText(); +textNode.characters = "Button"; +comp.appendChild(textNode); +textNode.componentPropertyReferences = { characters: labelKey }; + +// BOOLEAN + INSTANCE_SWAP → link to an instance node +const showIconKey = comp.addComponentProperty('Show Icon', 'BOOLEAN', true); +const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id); +const iconInstance = iconComp.createInstance(); +comp.appendChild(iconInstance); +iconInstance.componentPropertyReferences = { + visible: showIconKey, // BOOLEAN controls show/hide + mainComponent: iconSlotKey // INSTANCE_SWAP controls which component +}; +``` + +**Valid `componentPropertyReferences` keys:** +- `characters` — TEXT property on a TextNode +- `visible` — BOOLEAN property (any node) +- `mainComponent` — INSTANCE_SWAP property on an InstanceNode + +## Slots: createSlot and SLOT Properties + +Slots are designated drop zones inside a component where designers can place arbitrary content in instances — more flexible than INSTANCE_SWAP (which only swaps component instances). They appear as `SlotNode` (type `'SLOT'`) in the Plugin API and as a `SLOT`-typed component property. + +### Option 1 — `component.createSlot()` (preferred) + +Creates a `SlotNode` as a direct child of the component and automatically creates a linked `SLOT` component property. No manual wiring needed. + +```javascript +const card = figma.createComponent(); +card.name = "Card"; +card.layoutMode = "VERTICAL"; +card.primaryAxisSizingMode = "AUTO"; +card.counterAxisSizingMode = "FIXED"; +card.resize(320, 100); + +// Creates a SlotNode and auto-wires a SLOT component property +const contentSlot = card.createSlot(); +contentSlot.name = "Content"; +contentSlot.layoutMode = "VERTICAL"; // GRID is NOT allowed on slots +contentSlot.resize(320, 200); + +// The auto-created property key is accessible via componentPropertyReferences +const slotPropKey = contentSlot.componentPropertyReferences["slotContentId"]; +// e.g. "Content#7:1" +``` + +Multiple slots are supported — each call to `createSlot()` produces a separate slot and property: + +```javascript +const contentSlot = card.createSlot(); +contentSlot.name = "Content"; + +const footerSlot = card.createSlot(); +footerSlot.name = "Footer"; + +// Component now has two SLOT properties automatically +return Object.keys(card.componentPropertyDefinitions); +// → ["Content#7:1", "Footer#7:2"] +``` + +### Option 2 — Manual binding via addComponentProperty + +Link a regular frame to a `SLOT` property with `componentPropertyReferences`: + +```javascript +const slotPropKey = component.addComponentProperty("Content", "SLOT", ""); +const slotFrame = figma.createFrame(); +component.appendChild(slotFrame); +// slotFrame must not have GRID layoutMode, and must be a direct child (not nested inside another slot) +slotFrame.componentPropertyReferences = { slotContentId: slotPropKey }; +``` + +### Populating slots in instances + +In a component instance, slot nodes are accessible by `findOne()`. Build content and append it to the slot like any other node. In narrow cases the original node handle can be invalidated by the append, so if a post-append edit throws `"Internal Figma Error: Parent not found"`, re-find the sublayer through the slot's `children` and edit through the fresh handle. + +```javascript +const instance = card.createInstance(); +figma.currentPage.appendChild(instance); + +const btn = figma.createFrame(); +btn.layoutMode = "HORIZONTAL"; +btn.cornerRadius = 8; + +const contentSlot = instance.findOne(n => n.type === "SLOT" && n.name === "Content"); +contentSlot.appendChild(btn); + +// If a post-append edit throws "Parent not found", re-find via the slot: +// const appended = contentSlot.children[contentSlot.children.length - 1]; +// appended.someProperty = ...; +``` + +### Slot restrictions + +- `GRID` layoutMode is not allowed on slot nodes +- Widgets, Stickies, and ComponentNodes cannot be appended directly to a slot +- Frames nested inside another slot cannot themselves be bound to a slot property +- `instance.setProperties({ [slotPropKey]: ... })` throws — slot content is set by appending children, not via `setProperties` +- `slotNode.resetSlot()` (in an instance) reverts the slot to its default empty state + +## INSTANCE_SWAP: Avoiding Variant Explosion + +When a component has many possible sub-elements (e.g., 30 different icons), **never** create a variant per sub-element. Use a single INSTANCE_SWAP property instead — the user picks from any compatible component at design time. + +```javascript +// Create icon as its own ComponentNode +const iconComp = figma.createComponent(); +iconComp.name = "Icon/Search"; +iconComp.resize(24, 24); +const svgNode = figma.createNodeFromSvg('...'); +iconComp.appendChild(svgNode); + +// Use it as the default for INSTANCE_SWAP +const iconSlotKey = comp.addComponentProperty('Icon', 'INSTANCE_SWAP', iconComp.id); +const instance = iconComp.createInstance(); +comp.appendChild(instance); +instance.componentPropertyReferences = { mainComponent: iconSlotKey }; +``` + +This works for icons, avatars, badges, or any swappable nested element. + +## Discovering Existing Conventions in the File + +**Always inspect the file before creating components.** Different files have different naming styles, structures, and conventions. Your code should match what's already there. + +### List all existing components across all pages + +```javascript +const results = []; +for (const page of figma.root.children) { + await figma.setCurrentPageAsync(page); + page.findAll(n => { + if (n.type === 'COMPONENT') results.push(`[${page.name}] ${n.name} (COMPONENT) id=${n.id}`); + if (n.type === 'COMPONENT_SET') results.push(`[${page.name}] ${n.name} (COMPONENT_SET) id=${n.id}`); + return false; + }); +} +return results.join('\n'); +``` + +### Inspect an existing component set's variant naming pattern + +```javascript +const cs = await figma.getNodeByIdAsync('COMPONENT_SET_ID'); +const variantNames = cs.children.map(c => c.name); +const propDefs = cs.componentPropertyDefinitions; +return { variantNames, propDefs }; +``` + +### Find existing components in the file + +```javascript +const components = []; +for (const page of figma.root.children) { + await figma.setCurrentPageAsync(page); + page.findAll(n => { + if (n.type === 'COMPONENT') { + components.push({ name: n.name, id: n.id, page: page.name, w: n.width, h: n.height }); + } + return false; + }); +} +return components; +``` + +## Importing Components by Key (Team Libraries) + +`importComponentByKeyAsync` and `importComponentSetByKeyAsync` import components from **team libraries** (not the same file you're working in). For components in the current file, use `figma.getNodeByIdAsync()` or `findOne()`/`findAll()` to locate them directly. + +```javascript +// Import a component from a team library +const comp = await figma.importComponentByKeyAsync("COMPONENT_KEY"); +const instance = comp.createInstance(); + +// Import a component set from a team library and pick a variant +const set = await figma.importComponentSetByKeyAsync("COMPONENT_SET_KEY"); +const variant = set.children.find(c => + c.type === "COMPONENT" && c.name.includes("size=md") +) || set.defaultVariant; +const variantInstance = variant.createInstance(); +``` + +## Working with Instances + +### Finding the right variant in a component set + +Parse variant names to match on multiple properties simultaneously: + +```javascript +const compSet = await figma.importComponentSetByKeyAsync("KEY"); + +const variant = compSet.children.find(c => { + const props = Object.fromEntries( + c.name.split(', ').map(p => p.split('=')) + ); + return props.variant === "primary" && props.size === "md"; +}) || compSet.defaultVariant; + +const instance = variant.createInstance(); +``` + +### Setting variant properties on an instance + +After creating an instance from a component set, you can set variant properties via `setProperties`: + +```javascript +const instance = defaultVariant.createInstance(); +instance.setProperties({ + "variant": "primary", + "size": "medium" +}); +``` + +### Overriding text in a component instance + +**Always discover component properties BEFORE writing text overrides.** Components expose text as `TEXT`-type component properties, and `setProperties()` is the correct way to override them. Direct `node.characters` changes on property-managed text may be overridden by the component property system on render. + +**Step 1: Inspect componentProperties on a sample instance:** + +```javascript +const instance = comp.createInstance(); +const propDefs = instance.componentProperties; +// Returns e.g.: { "Label#2:0": { type: "TEXT", value: "Button" }, "Has Icon#4:64": { type: "BOOLEAN", value: true } } +return propDefs; +``` + +Also check nested instances — a parent component may not expose text properties directly, but its nested child instances might: + +```javascript +const nestedInstances = instance.findAll(n => n.type === "INSTANCE"); +const nestedProps = nestedInstances.map(ni => ({ + name: ni.name, + id: ni.id, + properties: ni.componentProperties +})); +``` + +**Step 2: Use setProperties() for TEXT-type properties:** + +```javascript +const instance = comp.createInstance(); +const propDefs = instance.componentProperties; +for (const [key, def] of Object.entries(propDefs)) { + if (def.type === "TEXT") { + instance.setProperties({ [key]: "New text value" }); + } +} +``` + +For nested instances that expose their own TEXT properties, call `setProperties()` on the nested instance: + +```javascript +const nestedHeading = instance.findOne(n => n.type === "INSTANCE" && n.name === "Text Heading"); +if (nestedHeading) { + nestedHeading.setProperties({ "Text#2104:5": "Actual heading text" }); +} +``` + +**Step 3: Only fall back to direct node.characters for unmanaged text.** If text is NOT controlled by any component property, find text nodes directly. **Always load the node's actual font first** — instance text nodes inherit fonts from the source component, so don't assume Inter Regular: + +```javascript +const textNodes = instance.findAll(n => n.type === "TEXT"); +for (const t of textNodes) { + await figma.loadFontAsync(t.fontName); + t.characters = "Updated text"; +} +``` + +### detachInstance() invalidates ancestor node IDs + +**Warning:** When `detachInstance()` is called on a nested instance inside a library component instance, the parent instance may also get implicitly detached (converted from INSTANCE to FRAME with a **new ID**). Subsequent `getNodeByIdAsync(oldParentId)` returns null. + +```javascript +// WRONG — cached parent ID becomes invalid after child detach +const parentId = parentInstance.id; +nestedChild.detachInstance(); +const parent = await figma.getNodeByIdAsync(parentId); // null! + +// CORRECT — re-discover nodes by traversal from a stable (non-instance) parent +const stableFrame = await figma.getNodeByIdAsync(manualFrameId); // a frame YOU created +nestedChild.detachInstance(); +// Re-find the parent by traversing from the stable frame +const parent = stableFrame.findOne(n => n.name === "ParentName"); +``` + +If you must detach multiple nested instances across sibling components, do it in a **single** `use_figma` call — discover all targets by traversal at the start before any detachment mutates the tree. + +## Inspecting Component Metadata (Deep Traversal) + +These helpers extract the full property schema and descendant structure of a component. Useful for understanding complex components before creating instances or setting properties. + +```javascript +/** + * Imports a component or component set from a library by its published key. + * Tries COMPONENT first, then falls back to COMPONENT_SET. + * + * @param {string} componentKey - The published key of the component or component set. + * @returns {Promise} + */ +async function importComponentByKey(componentKey) { + try { + return await figma.importComponentByKeyAsync(componentKey); + } catch { + try { + return await figma.importComponentSetByKeyAsync(componentKey); + } catch { + throw new Error(`No Component or Component Set available with key '${componentKey}'`); + } + } +} + +/** + * Given a main component node, returns the component set parent if one exists, + * otherwise returns the component itself. Used to get the top-level node that + * holds `componentPropertyDefinitions`. + * + * @param {ComponentNode} mainComponent + * @returns {ComponentNode|ComponentSetNode} + */ +function getRelevantComponentNode(mainComponent) { + return mainComponent.parent.type === "COMPONENT_SET" + ? mainComponent.parent + : mainComponent; +} + +/** + * Extracts `componentPropertyDefinitions` from a component or component set node + * into a flat map keyed by property key. + * + * @param {ComponentNode|ComponentSetNode} node + * @returns {Record} + */ +function getComponentProps(node) { + const result = {}; + for (let key in node.componentPropertyDefinitions) { + const prop = { + name: key.replace(/#[^#]+$/, ""), + type: node.componentPropertyDefinitions[key].type, + key: key + }; + if (prop.type === "VARIANT") { + prop.variantOptions = node.componentPropertyDefinitions[key].variantOptions; + } + result[key] = prop; + } + return result; +} + +/** + * Recursively walks a component tree and collects all INSTANCE and TEXT nodes + * into `result`, keyed by `TYPE[name]`. Handles variant namespacing and + * deduplicates nodes with identical names but differing property references. + * + * @param {SceneNode} node - The node to traverse. + * @param {string[]} namespace - Accumulated variant names for the current path. + * @param {Record} result - Accumulator object populated in place. + */ +function collectDescendants(node, namespace, result) { + if (node.type === "INSTANCE" || node.type === "TEXT") { + const references = node.componentPropertyReferences || {}; + if (!node.visible && !references.visible) return; + + const object = { type: node.type, name: node.name, references }; + let key = `${node.type}[${node.name}]`; + + if (result[key] && JSON.stringify(references) !== JSON.stringify(result[key].references)) { + key += btoa(btoa(unescape(encodeURIComponent(JSON.stringify(references))))); + } + + if (node.type === "INSTANCE") { + const mainComponent = getRelevantComponentNode(node.mainComponent); + object.properties = getComponentProps(mainComponent); + object.descendants = {}; + object.mainComponentName = mainComponent.name; + collectDescendants(mainComponent, [], object.descendants); + } + + const start = namespace.length ? { variants: [] } : {}; + result[key] = Object.assign(object, result[key] || start); + if (namespace.length) result[key].variants.push(namespace[namespace.length - 1]); + } else if ("children" in node && node.visible) { + if (node.type === "COMPONENT" && node.parent.type === "COMPONENT_SET") namespace.push(node.name); + node.children.forEach(child => collectDescendants(child, namespace, result)); + } +} + +/** + * Returns structured metadata for a component or component set defined in the current file. + * + * @param {string} componentId - The node ID of a COMPONENT or COMPONENT_SET node. + * @returns {Promise<{name: string, nodeId: string, properties: object, descendants: object}|undefined>} + */ +async function getLocalComponentMetadata(componentId) { + const node = await figma.getNodeByIdAsync(componentId); + if (node.type === "COMPONENT_SET" || node.type === "COMPONENT") { + const result = { + name: node.name, + nodeId: node.id, + properties: {}, + descendants: {} + }; + result.properties = getComponentProps(node); + collectDescendants(node, [], result.descendants); + return result; + } else { + throw new Error("Node is not a Component or Component Set"); + } +} + +/** + * Returns structured metadata for a published component or component set loaded by its key. + * + * @param {string} componentKey - The published key of the component or component set. + * @returns {Promise<{name: string, nodeId: string, properties: object, descendants: object}>} + */ +async function getPublishedComponentMetadata(componentKey) { + const node = await importComponentByKey(componentKey); + const result = { + name: node.name, + nodeId: node.id, + properties: {}, + descendants: {} + }; + result.properties = getComponentProps(node); + collectDescendants(node, [], result.descendants); + return result; +} +``` + +### Full metadata extraction script + +```javascript +// For local components, use getLocalComponentMetadata: +const result = await getLocalComponentMetadata('COMPONENT_OR_SET_ID'); +return result; + +// For published components, use getPublishedComponentMetadata: +// const result = await getPublishedComponentMetadata('COMPONENT_KEY'); +// return result; +``` diff --git a/.agents/skills/figma-use/references/effect-style-patterns.md b/.agents/skills/figma-use/references/effect-style-patterns.md new file mode 100644 index 00000000000..40ebad2b4d2 --- /dev/null +++ b/.agents/skills/figma-use/references/effect-style-patterns.md @@ -0,0 +1,125 @@ +# Effect Style API Patterns + +> Part of the [use_figma skill](../SKILL.md). How to create, apply, and inspect effect styles using the Plugin API. +> +> For design system context (effect types, variable bindings on effects, gotchas), see [wwds-effect-styles](working-with-design-systems/wwds-effect-styles.md). + +## Contents + +- Listing Effect Styles +- Creating a Drop Shadow Style +- Importing Library Effect Styles +- Applying Effect Styles to Nodes + +## Listing Effect Styles + +```javascript +/** + * Lists all local effect styles. + * + * @returns {Promise>} + */ +async function listEffectStyles() { + const styles = await figma.getLocalEffectStylesAsync(); + return styles.map(s => ({ + id: s.id, + name: s.name, + key: s.key, + effectCount: s.effects.length + })); +} +``` + +Full runnable script: + +```javascript +const results = await listEffectStyles(); +return results; +``` + +## Creating a Drop Shadow Style + +Colors are **RGBA 0–1 range**. `effects` is a read-only array — always reassign, never mutate in place. + +```javascript +/** + * Creates a drop shadow effect style. + * + * @param {string} name - e.g. "Elevation/200" + * @param {{ r: number, g: number, b: number, a: number }} color - RGBA, 0-1 range + * @param {{ x: number, y: number }} offset + * @param {number} radius - blur radius + * @param {number} [spread=0] + * @returns {EffectStyle} + */ +function createDropShadowStyle(name, color, offset, radius, spread) { + const style = figma.createEffectStyle(); + style.name = name; + style.effects = [{ + type: "DROP_SHADOW", + color, + offset, + radius, + spread: spread || 0, + visible: true, + blendMode: "NORMAL" + }]; + return style; +} +``` + +Full runnable script: + +```javascript +const style = createDropShadowStyle( + "Elevation/200", + { r: 0, g: 0, b: 0, a: 0.15 }, + { x: 0, y: 4 }, + 12, + 0 +); +return { id: style.id, name: style.name }; +``` + +## Importing Library Effect Styles + +For effect styles from **team libraries**, use `importStyleByKeyAsync`: + +```javascript +// Import a library effect style by key +const shadowStyle = await figma.importStyleByKeyAsync("EFFECT_STYLE_KEY"); +// Apply to a node +node.effectStyleId = shadowStyle.id; +``` + +`search_design_system` with `includeStyles: true` returns style keys you can import this way. Prefer importing library styles over creating new ones. + +## Applying Effect Styles to Nodes + +```javascript +/** + * Applies an effect style to all nodes on the current page that match a given name pattern. + * + * @param {string} styleId - The ID of an EffectStyle. + * @param {string} nodeNamePattern - Substring match against node names. + * @returns {number} - Number of nodes the style was applied to. + */ +function applyEffectStyleToMatchingNodes(styleId, nodeNamePattern) { + const nodes = figma.currentPage.findAll(n => n.name.includes(nodeNamePattern)); + let applied = 0; + for (const node of nodes) { + if ('effectStyleId' in node) { + node.effectStyleId = styleId; + applied++; + } + } + return applied; +} +``` + +Full runnable script: + +```javascript +const applied = applyEffectStyleToMatchingNodes('STYLE_ID', 'Card'); +return { applied }; +``` diff --git a/.agents/skills/figma-use/references/gotchas.md b/.agents/skills/figma-use/references/gotchas.md new file mode 100644 index 00000000000..a415315d757 --- /dev/null +++ b/.agents/skills/figma-use/references/gotchas.md @@ -0,0 +1,677 @@ +# Gotchas & Common Mistakes + +> Part of the [use_figma skill](../SKILL.md). Every known pitfall with WRONG/CORRECT code examples. + +## Contents + +- Component properties and variant creation pitfalls +- Paint, color, and variable binding pitfalls +- Page context and plugin lifecycle pitfalls +- Auto Layout and sizing order pitfalls (including HUG/FILL interactions) +- Variant layout and geometry pitfalls +- Font loading and text/typography pitfalls +- Variable scopes and mode pitfalls +- Node cleanup and empty-fill pitfalls +- Type-specific method calls without node type guards +- Non-existent property writes and "object is not extensible" +- width/height are read-only — use resize() +- detachInstance() and node ID invalidation + + +## New nodes default to (0,0) and overlap existing content + +Every `figma.create*()` call places the node at position (0,0). If you append multiple nodes directly to the page, they all stack on top of each other and on top of any existing content. + +**This only matters for nodes appended directly to the page** (i.e., top-level nodes). Nodes appended as children of other frames, components, or auto-layout containers are positioned by their parent — don't scan for overlaps when nesting nodes. + +```js +// WRONG — top-level node lands at (0,0), overlapping existing page content +const frame = figma.createFrame() +frame.name = "My New Frame" +frame.resize(400, 300) +figma.currentPage.appendChild(frame) + +// CORRECT — find existing content bounds and place the new top-level node to the right +const page = figma.currentPage +let maxX = 0 +for (const child of page.children) { + const right = child.x + child.width + if (right > maxX) maxX = right +} +const frame = figma.createFrame() +frame.name = "My New Frame" +frame.resize(400, 300) +figma.currentPage.appendChild(frame) +frame.x = maxX + 100 // 100px gap from rightmost existing content +frame.y = 0 + +// NOT NEEDED — child nodes inside a parent don't need overlap scanning +const card = figma.createAutoLayout('VERTICAL') +const label = figma.createText() +card.appendChild(label) // positioned by auto-layout, no x/y needed +``` + +## `addComponentProperty` returns a string key, not an object — never hardcode or guess it + +Figma generates the property key dynamically (e.g. `"label#4:0"`). The suffix is unpredictable. Always capture and use the return value directly. + +```js +// WRONG — guessing / hardcoding the key +comp.addComponentProperty('label', 'TEXT', 'Button') +labelNode.componentPropertyReferences = { characters: 'label#0:1' } // Error: key not found + +// WRONG — treating the return value as an object +const result = comp.addComponentProperty('Label', 'TEXT', 'Button') +const propKey = Object.keys(result)[0] // BUG: returns '0' (first char index of string!) +labelNode.componentPropertyReferences = { characters: propKey } // Error: property '0' not found + +// CORRECT — the return value IS the key string, use it directly +const propKey = comp.addComponentProperty('Label', 'TEXT', 'Button') +// propKey === "label#4:0" (exact value varies; never assume it) +labelNode.componentPropertyReferences = { characters: propKey } +``` + +The same applies to `COMPONENT_SET` nodes — `addComponentProperty` always returns the property key as a string. + +## MUST return ALL created/mutated node IDs + +Every script that creates or mutates nodes on the canvas must track and return all affected node IDs in the return value. Without these IDs, subsequent calls cannot reference, validate, or clean up those nodes. + +```js +// WRONG — only returns the parent frame ID, loses track of children +const frame = figma.createFrame() +const rect = figma.createRectangle() +const text = figma.createText() +frame.appendChild(rect) +frame.appendChild(text) +return { nodeId: frame.id } + +// CORRECT — returns all created node IDs in a structured response +const frame = figma.createFrame() +const rect = figma.createRectangle() +const text = figma.createText() +frame.appendChild(rect) +frame.appendChild(text) +return { + createdNodeIds: [frame.id, rect.id, text.id], + rootNodeId: frame.id +} + +// CORRECT — when mutating existing nodes, return those IDs too +const nodes = figma.currentPage.findAll(n => n.name === 'Card') +for (const n of nodes) { + n.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }] +} +return { + mutatedNodeIds: nodes.map(n => n.id), + count: nodes.length +} +``` + +## Colors are 0–1 range + +```js +// WRONG — will throw validation error (ZeroToOne enforced) +node.fills = [{ type: 'SOLID', color: { r: 255, g: 0, b: 0 } }] + +// CORRECT +node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }] +``` + +## Fills/strokes are immutable arrays + +```js +// WRONG — modifying in place does nothing +node.fills[0].color = { r: 1, g: 0, b: 0 } + +// CORRECT — clone, modify, reassign +const fills = JSON.parse(JSON.stringify(node.fills)) +fills[0].color = { r: 1, g: 0, b: 0 } +node.fills = fills +``` + +## setBoundVariableForPaint returns a NEW paint + +```js +// WRONG — ignoring return value +figma.variables.setBoundVariableForPaint(paint, "color", colorVar) +node.fills = [paint] // paint is unchanged! + +// CORRECT — capture the returned new paint +const boundPaint = figma.variables.setBoundVariableForPaint(paint, "color", colorVar) +node.fills = [boundPaint] +``` + +## Variable collection starts with 1 mode + +```js +// A new collection already has one mode — rename it, don't try to add first +const collection = figma.variables.createVariableCollection("Colors") +// collection.modes = [{ modeId: "...", name: "Mode 1" }] +collection.renameMode(collection.modes[0].modeId, "Light") +const darkModeId = collection.addMode("Dark") +``` + +## combineAsVariants requires ComponentNodes + +```js +// WRONG — passing frames +const f1 = figma.createFrame() +figma.combineAsVariants([f1], figma.currentPage) // Error! + +// CORRECT — passing components +const c1 = figma.createComponent() +c1.name = "variant=primary, size=md" +const c2 = figma.createComponent() +c2.name = "variant=secondary, size=md" +figma.combineAsVariants([c1, c2], figma.currentPage) +``` + +## Page switching: sync setter does NOT work + +The sync setter `figma.currentPage = page` does **NOT work** in `use_figma` — it throws `"Setting figma.currentPage is not supported"`. You **must** use `await figma.setCurrentPageAsync(page)` instead, which switches the page and loads its content. + +Note: **reading** `figma.currentPage` is fine — it's only the **assignment** (`figma.currentPage = ...`) that throws. + +```js +// WRONG — throws "Setting figma.currentPage is not supported" +figma.currentPage = targetPage + +// CORRECT — async method switches and loads content +await figma.setCurrentPageAsync(targetPage) + +// ALSO CORRECT — reading currentPage is fine +const page = figma.currentPage // works +``` + +## `get_metadata` operates on one subtree — discover pages explicitly + +A Figma file can have multiple pages (canvas nodes). `get_metadata` only returns the subtree of whichever node you pass it. To get a usable index of every page: + +- Call `get_metadata` with **no nodeId** — it returns the document's top-level pages as `{guid, name}` entries (no XML dump). This is the cheapest way to discover pages. +- For more detail per page (e.g. child counts, top-level node types), fall back to `use_figma`: + +```js +const pages = figma.root.children.map(p => `${p.name} id=${p.id} children=${p.children.length}`); +return pages.join('\n'); +``` + +Icons, variables, and components may live on pages other than the first. Always enumerate all pages before concluding that the file has no existing assets. + +## Never use figma.notify() + +```js +// WRONG — throws "not implemented" error +figma.notify("Done!") + +// CORRECT — return a value to send data back to the agent +return "Done!" +``` + +## `getPluginData()` / `setPluginData()` are not supported + +These APIs are not available in `use_figma`. Use `getSharedPluginData()` / `setSharedPluginData()` instead (these ARE supported), or track nodes by returning IDs. + +```js +// WRONG — not supported in use_figma +node.setPluginData('my_key', 'my_value') +const val = node.getPluginData('my_key') + +// CORRECT — use shared plugin data (requires a namespace) +node.setSharedPluginData('my_namespace', 'my_key', 'my_value') +const val = node.getSharedPluginData('my_namespace', 'my_key') + +// ALSO CORRECT — return node IDs and track them across calls +const rect = figma.createRectangle() +return { nodeId: rect.id } +// Then pass nodeId as a string literal in the next use_figma call +``` + +## Script must always return a value + +```js +// WRONG — no return, caller gets no useful response +figma.createRectangle() + +// CORRECT — return a result (objects are auto-serialized, errors are auto-captured) +const rect = figma.createRectangle() +return { nodeId: rect.id } +``` + +## setBoundVariable for paint fields only works on SOLID paints + +```js +// Only SOLID paint type supports color variable binding +// Gradient paints, image paints, etc. will throw +const solidPaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } } +const bound = figma.variables.setBoundVariableForPaint(solidPaint, "color", colorVar) +``` + +## Explicit variable modes must be set per component + +```js +// WRONG — all variants render with the default (first) mode +const colorCollection = figma.variables.createVariableCollection("Colors") +// ... create variables and modes ... +// Components all show the first mode's values by default! + +// CORRECT — set explicit mode on each component to get variant-specific values +component.setExplicitVariableModeForCollection(colorCollection, targetModeId) +``` + +## `lineHeight` and `letterSpacing` must be objects, not bare numbers + +```js +// WRONG — throws or silently does nothing +style.lineHeight = 1.5 +style.lineHeight = 24 +style.letterSpacing = 0 + +// CORRECT +style.lineHeight = { unit: "AUTO" } // auto/intrinsic +style.lineHeight = { value: 24, unit: "PIXELS" } // fixed pixel height +style.lineHeight = { value: 150, unit: "PERCENT" } // percentage of font size + +style.letterSpacing = { value: 0, unit: "PIXELS" } // no tracking +style.letterSpacing = { value: -0.5, unit: "PIXELS" } // tight +style.letterSpacing = { value: 5, unit: "PERCENT" } // percent-based +``` + +This applies to both `TextStyle` and `TextNode` properties. The same rule applies inside `use_figma`, interactive plugins, and any other plugin API context. + +## Font style names are file-dependent — use `listAvailableFontsAsync` to discover them + +Font style names vary per provider and per Figma file. Always call `figma.listAvailableFontsAsync()` to discover exact style strings before loading — never guess or probe with try/catch. See [text-style-patterns.md](text-style-patterns.md#discovering-available-font-styles) for the discovery + load pattern. + +## combineAsVariants does NOT auto-layout in `use_figma` + +```js +// WRONG — all variants stack at position (0, 0), resulting in a tiny ComponentSet +const components = [comp1, comp2, comp3] +const cs = figma.combineAsVariants(components, figma.currentPage) +// cs.width/height will be the size of a SINGLE variant! + +// CORRECT — manually layout children in a grid after combining +const cs = figma.combineAsVariants(components, figma.currentPage) +const colWidth = 120 +const rowHeight = 56 +cs.children.forEach((child, i) => { + const col = i % numCols + const row = Math.floor(i / numCols) + child.x = col * colWidth + child.y = row * rowHeight +}) +// CRITICAL: resize from actual child bounds, not formula — formula errors leave variants outside the boundary +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 + 40, maxY + 40) +``` + +## Paint `color` must not include `a` — use `opacity` at the paint level instead + +Paint `color` only accepts `{r, g, b}`. Adding `a` to it throws `"Unrecognized key(s) in object: 'a' at [0].color"`. This is a common mistake coming from CSS `rgba()` muscle memory. + +Alpha/opacity belongs at the **paint level** as `opacity`, not inside `color`. + +```js +// WRONG — 'a' is not valid inside color; throws validation error +node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.1 } }] + +// CORRECT — opacity goes at the paint level +node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 0.1 }] + +// CORRECT — fully opaque (no opacity needed) +node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }] +``` + +**COLOR variable values are the exception** — they do use `{r, g, b, a}`: + +```js +// Variable values use {r, g, b, a} — this is correct for variables only +const colorVar = figma.variables.createVariable("bg", collection, "COLOR") +colorVar.setValueForMode(modeId, { r: 1, g: 0, b: 0, a: 1 }) // opaque red +colorVar.setValueForMode(modeId, { r: 0, g: 0, b: 0, a: 0 }) // fully transparent +``` + +## `layoutSizingVertical`/`layoutSizingHorizontal` = `'FILL'` requires auto-layout parent FIRST + +```js +// WRONG — setting FILL before the node is a child of an auto-layout frame +const child = figma.createFrame() +child.layoutSizingVertical = 'FILL' // ERROR: "FILL can only be set on children of auto-layout frames" +parent.appendChild(child) + +// CORRECT — append to auto-layout parent FIRST, then set FILL +const child = figma.createFrame() +parent.appendChild(child) // parent must have layoutMode set +child.layoutSizingVertical = 'FILL' // Works! +``` + +**Tip:** use `figma.createAutoLayout()` (or `figma.createAutoLayout('VERTICAL')`) instead of `figma.createFrame()` when you want a parent that supports `FILL` children. It returns a frame with `layoutMode` already set and both axes hugging content, so you don't have to remember the property dance. + +```js +const parent = figma.createAutoLayout() // layoutMode = 'HORIZONTAL', sizing = AUTO +const child = figma.createFrame() +parent.appendChild(child) +child.layoutSizingHorizontal = 'FILL' // Works immediately +``` + +## HUG parents collapse FILL children + +A `HUG` parent cannot give `FILL` children meaningful size. If children have `layoutSizingHorizontal = "FILL"` but the parent is `"HUG"`, the children collapse to minimum size. The parent must be `"FILL"` or `"FIXED"` for FILL children to expand. This is a common cause of truncated text in select fields, inputs, and action rows. + +```js +// WRONG — parent hugs, so FILL children get zero extra space +const parent = figma.createAutoLayout() +parent.layoutSizingHorizontal = 'HUG' +const child = figma.createFrame() +parent.appendChild(child) +child.layoutSizingHorizontal = 'FILL' // collapses to min size! + +// CORRECT — parent must be FIXED or FILL for FILL children to expand +const parent = figma.createAutoLayout() +parent.resize(400, 50) +parent.layoutSizingHorizontal = 'FIXED' // or 'FILL' if inside another auto-layout +const child = figma.createFrame() +parent.appendChild(child) +child.layoutSizingHorizontal = 'FILL' // expands to fill remaining 400px +``` + +## `layoutGrow` with a hugging parent causes content compression + +```js +// WRONG — layoutGrow on a child when parent has primaryAxisSizingMode='AUTO' (hug) +// causes the child to SHRINK below its natural size instead of expanding +const parent = figma.createComponent() +parent.layoutMode = 'VERTICAL' +parent.primaryAxisSizingMode = 'AUTO' // hug contents +const content = figma.createAutoLayout('VERTICAL') +parent.appendChild(content) +content.layoutGrow = 1 // BUG: content compresses, children hidden! + +// CORRECT — only use layoutGrow when parent has FIXED sizing with extra space +content.layoutGrow = 0 // let content take its natural size +// OR: set parent to FIXED sizing first +parent.primaryAxisSizingMode = 'FIXED' +parent.resizeWithoutConstraints(300, 500) +content.layoutGrow = 1 // NOW it correctly fills remaining space +``` + +## `width` and `height` are read-only — use `resize()` + +`node.width` and `node.height` are read-only. Assigning to them throws `"TypeError: no setter for property"`. Use `resize()` or `resizeWithoutConstraints()` instead. + +Note: `x` and `y` are **not** read-only and can be set directly. + +```js +// WRONG — throws "no setter for property" +node.width = 300 +node.height = 64 + +// CORRECT — use resize() to change dimensions +node.resize(300, 64) // change both +node.resize(300, node.height) // change width only +node.resize(node.width, 64) // change height only + +// CORRECT — x and y are writable directly +node.x = 100 +node.y = 200 +``` + +For sections and component sets, use `resizeWithoutConstraints()` instead of `resize()` (see the sections gotcha above). + +## `resize()` resets `primaryAxisSizingMode` and `counterAxisSizingMode` to FIXED + +`resize(w, h)` silently resets **both** sizing modes to `FIXED`. If you call it after setting `HUG`, the frame locks to the exact pixel value you passed — even a throwaway like `1`. + +```js +// WRONG — resize() after setting sizing mode overwrites it back to FIXED +const frame = figma.createComponent() +frame.layoutMode = 'VERTICAL' +frame.primaryAxisSizingMode = 'AUTO' // hug height +frame.counterAxisSizingMode = 'FIXED' +frame.resize(300, 10) // BUG: resets BOTH axes to 'FIXED'! Height stays at 10px forever. + +// ESPECIALLY DANGEROUS — throwaway values when you only care about one axis +const comp = figma.createComponent() +comp.layoutMode = 'VERTICAL' +comp.layoutSizingHorizontal = 'FIXED' +comp.layoutSizingVertical = 'HUG' +comp.resize(280, 1) // BUG: "I only want width=280" but this locks height to 1px! +// HUG was reset to FIXED by resize(), frame is now permanently 280×1 + +// CORRECT — call resize() FIRST, then set sizing modes +const frame = figma.createComponent() +frame.layoutMode = 'VERTICAL' +frame.resize(300, 40) // use a reasonable default, never 0 or 1 +frame.counterAxisSizingMode = 'FIXED' // keep width fixed at 300 +frame.primaryAxisSizingMode = 'AUTO' // NOW set height to hug — this sticks! +// Or use the modern shorthand (equivalent): +// frame.layoutSizingHorizontal = 'FIXED' +// frame.layoutSizingVertical = 'HUG' +``` + +**Rule of thumb**: Never pass a throwaway/garbage value (like `1` or `0`) to `resize()` for an axis you intend to be `HUG`. Either call `resize()` before setting sizing modes, or use a reasonable default that won't cause visual bugs if the mode reset goes unnoticed. + +## Node positions don't auto-reset after reparenting + +```js +// WRONG — assuming positions reset when moving a node into a new parent +const node = figma.createRectangle() +node.x = 500; node.y = 500; +figma.currentPage.appendChild(node) +section.appendChild(node) // node still at (500, 500) relative to section! + +// CORRECT — explicitly set x/y after ANY reparenting operation +section.appendChild(node) +node.x = 80; node.y = 80; // reset to desired position within section +``` + +## Grid layout with mixed-width rows causes overlaps + +```js +// WRONG — using a single column offset for rows with different-width items +// e.g. vertical cards (320px) and horizontal cards (500px) in a 2-row grid +for (let i = 0; i < allCards.length; i++) { + allCards[i].x = (i % 4) * 370 // 370 works for 320px cards but NOT 500px cards! +} + +// CORRECT — compute each row's spacing independently based on actual child widths +const gap = 50 +let x = 0 +for (const card of horizontalCards) { + card.x = x + x += card.width + gap // use actual width, not a fixed column size +} +``` + +## Sections don't auto-resize to fit content + +```js +// WRONG — section stays at default size, content overflows +const section = figma.createSection() +section.name = "My Section" +section.appendChild(someNode) // node may be outside section bounds + +// CORRECT — explicitly resize after adding content +const section = figma.createSection() +section.name = "My Section" +section.appendChild(someNode) +section.resize( + Math.max(someNode.width + 100, 800), + Math.max(someNode.height + 100, 600) +) +``` + +## `counterAxisAlignItems` does NOT support `'STRETCH'` + +```js +// WRONG — 'STRETCH' is not a valid enum value +comp.counterAxisAlignItems = 'STRETCH' +// Error: Invalid enum value. Expected 'MIN' | 'MAX' | 'CENTER' | 'BASELINE', received 'STRETCH' + +// CORRECT — use 'MIN' on the parent, then set children to FILL on the cross axis +comp.counterAxisAlignItems = 'MIN' +comp.appendChild(child) +// For vertical layout, stretch width: +child.layoutSizingHorizontal = 'FILL' +// For horizontal layout, stretch height: +child.layoutSizingVertical = 'FILL' +``` + +## Variable collection mode limits are plan-dependent + +```js +// Figma limits modes per collection based on the team/org plan: +// Free: 1 mode only (no addMode) +// Professional: up to 4 modes +// Organization/Enterprise: up to 40+ modes +// +// WRONG — creating 20 modes on a Professional plan will fail silently or throw +const coll = figma.variables.createVariableCollection("Variants") +for (let i = 0; i < 20; i++) coll.addMode("mode" + i) // May fail! + +// CORRECT — if you need many modes, split across multiple collections +// E.g., instead of 1 collection with 20 modes (variant×color): +// Collection A: 4 modes (variant: plain/outlined/soft/solid) +// Collection B: 5 modes (color: neutral/primary/danger/success/warning) +// Then use setExplicitVariableModeForCollection for BOTH on each component +``` + +## Variables default to `ALL_SCOPES` — always set scopes explicitly + +```js +// WRONG — variable appears in every property picker (fills, text, strokes, spacing, etc.) +const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR") +// bgColor.scopes defaults to ["ALL_SCOPES"] — pollutes all dropdowns + +// CORRECT — restrict to relevant property pickers +const bgColor = figma.variables.createVariable("Background/Default", coll, "COLOR") +bgColor.scopes = ["FRAME_FILL", "SHAPE_FILL"] // fill pickers only + +const textColor = figma.variables.createVariable("Text/Default", coll, "COLOR") +textColor.scopes = ["TEXT_FILL"] // text color picker only + +const borderColor = figma.variables.createVariable("Border/Default", coll, "COLOR") +borderColor.scopes = ["STROKE_COLOR"] // stroke picker only + +const spacing = figma.variables.createVariable("Space/400", coll, "FLOAT") +spacing.scopes = ["GAP"] // gap/spacing pickers only + +// Hide primitives that are only referenced via aliases +const primitive = figma.variables.createVariable("Brand/500", coll, "COLOR") +primitive.scopes = [] // hidden from all pickers +``` + +## Binding fills on nodes with empty fills + +```js +// WRONG — binding to a node with no fills does nothing +const comp = figma.createComponent() +comp.fills = [] // transparent +// Can't bind a color variable to fills that don't exist + +// CORRECT — add a placeholder SOLID fill, then bind the variable +const comp = figma.createComponent() +const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } } +const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar) +comp.fills = [boundPaint] +// The variable's resolved value (which may be transparent) will control the actual color +``` + +## Mode names must be descriptive — never leave 'Mode 1' + +Every new `VariableCollection` starts with one mode named `'Mode 1'`. Always rename it immediately. For single-mode collections use `'Default'`; for multi-mode collections use names from the source (e.g. `'Light'`/`'Dark'`, `'Desktop'`/`'Tablet'`/`'Mobile'`). + + // WRONG — generic names give no semantic meaning + const coll = figma.variables.createVariableCollection('Colors') + // coll.modes[0].name === 'Mode 1' — left as-is + const darkId = coll.addMode('Mode 2') + + // CORRECT — rename immediately to match the source + const coll = figma.variables.createVariableCollection('Colors') + coll.renameMode(coll.modes[0].modeId, 'Light') // was 'Mode 1' + const darkId = coll.addMode('Dark') + + // For single-mode collections (primitives, spacing, etc.) + const spacing = figma.variables.createVariableCollection('Spacing') + spacing.renameMode(spacing.modes[0].modeId, 'Default') // was 'Mode 1' + +## CSS variable names must not contain spaces + +When constructing a `var(--name)` string from a Figma variable name, replace BOTH slashes AND spaces with hyphens and convert to lowercase. + + // WRONG — only replacing slashes leaves spaces like 'var(--color-bg-brand secondary hover)' + v.setVariableCodeSyntax('WEB', `var(--${figmaName.replace(/\//g, '-').toLowerCase()})`) + + // CORRECT — replace all whitespace and slashes in one pass + v.setVariableCodeSyntax('WEB', `var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()})`) + +**Best practice**: Preserve the original CSS variable name from the source token file rather than deriving it from the Figma name. + + // Preferred — use the source CSS name directly + v.setVariableCodeSyntax('WEB', `var(${token.cssVar})`) // e.g. '--color-bg-brand-secondary-hover' + +## Calling type-specific methods without checking node type + +Some methods only exist on specific node types. Calling them on the wrong type throws "TypeError: not a function". Always guard with a type check before calling type-specific methods. + +```js +// WRONG — node might not be a TextNode +const node = await figma.getNodeByIdAsync('952:1253'); +const segments = node.getStyledTextSegments(['hyperlink']); // TypeError if node isn't TEXT + +// CORRECT — check type first +const node = await figma.getNodeByIdAsync('952:1253'); +if (!node || node.type !== 'TEXT') return { error: `Expected TextNode, got ${node?.type ?? 'null'}` }; +const segments = node.getStyledTextSegments(['hyperlink']); +``` + +Common type-specific methods and the types that have them: + +| Method | Node type required | +|--------|-------------------| +| `getStyledTextSegments()` | `TEXT` | +| `setRangeFontName()`, `setRangeFontSize()` | `TEXT` | +| `createInstance()` | `COMPONENT` | +| `addComponentProperty()` | `COMPONENT`, `COMPONENT_SET` | +| `createVariant()` | `COMPONENT_SET` | + +## Setting a non-existent property throws "object is not extensible" + +Figma plugin API node objects are non-extensible — you cannot add new properties to them. Setting a property name that doesn't exist on a node type throws `"Cannot add property X, object is not extensible"` (surfaced as `"object is not extensible"`). This only fires on **write**, and only for properties not defined on that node type. + +```js +// WRONG — 'strokeDashes' does not exist on VectorNode; throws "object is not extensible" +const v = figma.createVector() +v.strokeDashes = [4, 8] // Error! + +// CORRECT — the actual property is dashPattern +v.dashPattern = [4, 8] + +// WRONG — any invented property name throws the same error +node.customColor = '#ff0000' // Error — not a real API property +``` + +**How to avoid this**: Before setting any property, verify it exists on the node type by grepping [plugin-api-standalone.d.ts](plugin-api-standalone.d.ts). Property names that sound plausible but aren't in the typings will always throw. + +## `detachInstance()` invalidates ancestor node IDs + +When `detachInstance()` is called on a nested instance inside a library component instance, the parent instance may also get implicitly detached (converted from INSTANCE to FRAME with a **new ID**). Any previously cached ID for the parent becomes invalid. + +```js +// WRONG — using cached parent ID after child detach +const parentId = parentInstance.id; +nestedChild.detachInstance(); +const parent = await figma.getNodeByIdAsync(parentId); // null! ID changed. + +// CORRECT — re-discover by traversal from a stable (non-instance) frame +const stableFrame = await figma.getNodeByIdAsync(manualFrameId); +nestedChild.detachInstance(); +const parent = stableFrame.findOne(n => n.name === "ParentName"); +``` + +If detaching multiple nested instances across siblings, do it in a **single** `use_figma` call — discover all targets by traversal before any detachment mutates the tree. diff --git a/.agents/skills/figma-use/references/plugin-api-patterns.md b/.agents/skills/figma-use/references/plugin-api-patterns.md new file mode 100644 index 00000000000..36b1e165be9 --- /dev/null +++ b/.agents/skills/figma-use/references/plugin-api-patterns.md @@ -0,0 +1,529 @@ +# Plugin API Patterns + +> Part of the [use_figma skill](../SKILL.md). Quick reference for common Figma Plugin API operations. + +## Contents + +- Execution Basics +- Creating Nodes +- Fills and Strokes +- Auto Layout +- Effects +- Opacity and Blend Modes +- Corner Radius and Clipping +- Grouping and Organization +- Components and Variants +- Styles +- Cloning, Finding Nodes, and Grids +- Constraints and Viewport + + +## Execution Basics + +### Page Context + +Page context resets between `use_figma` calls — `figma.currentPage` always starts on the first page. Use `await figma.setCurrentPageAsync(page)` at the start of each invocation to switch to the correct page. The sync setter `figma.currentPage = page` does **NOT work** and will throw — always use the async method. + +```javascript +const targetPage = figma.root.children.find(p => p.name === "My Page"); +await figma.setCurrentPageAsync(targetPage); +// targetPage.children is now populated +``` + +### Returning Results + +Scripts are automatically wrapped in an async IIFE with error handling. Just write plain JS and use `return` to send data back to the agent: + +```javascript +// Return an object — auto-serialized to JSON +return { nodeId: frame.id, count: 5 } + +// Return a string +return "Created 3 components" +``` + +Errors are automatically captured — no try/catch needed. `figma.notify()` does **not** exist. Return all information via the `return` value. + +### Working Incrementally + +Don't build an entire screen in one call. Break work into small steps: +1. Create tokens/variables +2. Create text styles +3. Build individual components +4. Compose sections +5. Assemble screens + +Verify structure with `get_metadata` between steps. Use `get_screenshot` after each major creation milestone to catch visual problems early. + +## Creating Nodes + +### Frames + +```javascript +const frame = figma.createFrame(); +frame.name = "Container"; +frame.resize(1440, 900); +frame.x = 0; +frame.y = 0; +frame.fills = [{ type: "SOLID", color: { r: 0.98, g: 0.98, b: 0.99 } }]; +``` + +### Text + +```javascript +// MUST load font before any text operations +await figma.loadFontAsync({ family: "Inter", style: "Regular" }); + +const text = figma.createText(); +text.fontName = { family: "Inter", style: "Regular" }; +text.fontSize = 16; +text.lineHeight = { value: 24, unit: "PIXELS" }; +text.letterSpacing = { value: 0, unit: "PERCENT" }; +text.characters = "Hello World"; +text.fills = [{ type: "SOLID", color: { r: 0.1, g: 0.1, b: 0.12 } }]; +``` + +### Rectangles + +```javascript +const rect = figma.createRectangle(); +rect.name = "Background"; +rect.resize(400, 300); +rect.cornerRadius = 12; +rect.fills = [{ type: "SOLID", color: { r: 0.95, g: 0.95, b: 0.96 } }]; +``` + +### Ellipses + +```javascript +const circle = figma.createEllipse(); +circle.name = "Avatar Circle"; +circle.resize(48, 48); +circle.fills = [{ type: "SOLID", color: { r: 0.85, g: 0.87, b: 0.90 } }]; +``` + +### Lines + +```javascript +const line = figma.createLine(); +line.name = "Divider"; +line.resize(400, 0); +line.strokes = [{ type: "SOLID", color: { r: 0, g: 0, b: 0 }, opacity: 0.08 }]; +line.strokeWeight = 1; +``` + +### SVG Import + +```javascript +const svgString = ` + +`; + +const node = figma.createNodeFromSvg(svgString); +node.name = "Icon/Arrow Right"; +node.resize(24, 24); +``` + +## Fills & Strokes + +### Solid Fill + +```javascript +node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 } }]; +``` + +### Fill with Opacity + +```javascript +node.fills = [{ type: "SOLID", color: { r: 0.2, g: 0.2, b: 0.25 }, opacity: 0.5 }]; +``` + +### No Fill (Transparent) + +```javascript +node.fills = []; +``` + +### Linear Gradient + +```javascript +node.fills = [{ + type: "GRADIENT_LINEAR", + gradientStops: [ + { color: { r: 0.2, g: 0.36, b: 0.96, a: 1 }, position: 0 }, + { color: { r: 0.56, g: 0.24, b: 0.88, a: 1 }, position: 1 } + ], + gradientTransform: [[1, 0, 0], [0, 1, 0]] +}]; +``` + +### Strokes + +```javascript +node.strokes = [{ type: "SOLID", color: { r: 0.85, g: 0.85, b: 0.87 } }]; +node.strokeWeight = 1; +node.strokeAlign = "INSIDE"; // "CENTER", "OUTSIDE" +``` + +### Multiple Fills (Layered) + +```javascript +node.fills = [ + { type: "SOLID", color: { r: 0.95, g: 0.95, b: 0.96 } }, + { type: "SOLID", color: { r: 0.2, g: 0.36, b: 0.96 }, opacity: 0.05 } +]; +``` + +## Auto Layout + +### Setting Up Auto Layout + +**Prefer `figma.createAutoLayout()`** — it returns a frame with `layoutMode` already set and both axes hugging content, so children can immediately use `layoutSizingHorizontal/Vertical = "FILL"`. + +```javascript +const frame = figma.createAutoLayout(); // HORIZONTAL by default +const column = figma.createAutoLayout("VERTICAL"); + +// Customize from there as usual: +frame.itemSpacing = 16; +frame.paddingTop = 24; +frame.paddingBottom = 24; +frame.paddingLeft = 24; +frame.paddingRight = 24; +``` + +If you need a non-auto-layout frame, use `figma.createFrame()` and set the properties manually: + +```javascript +const frame = figma.createFrame(); +frame.layoutMode = "VERTICAL"; // or "HORIZONTAL" +frame.resize(360, 1); // Width fixed, height auto +frame.primaryAxisSizingMode = "AUTO"; // Hug main axis +frame.counterAxisSizingMode = "FIXED"; // Fixed cross axis +``` + +**CRITICAL ORDERING:** Always call `resize()` BEFORE setting sizing modes. The `resize()` method silently resets both sizing modes to FIXED, so calling it after setting `primaryAxisSizingMode = "AUTO"` will override your HUG settings and lock the frame to the exact pixel dimensions you passed (even throwaway values like `1`). This causes the common "1px dimension" bug. + +### Alignment + +```javascript +// Main axis (direction of layout) +frame.primaryAxisAlignItems = "MIN"; // Start +frame.primaryAxisAlignItems = "CENTER"; // Center +frame.primaryAxisAlignItems = "MAX"; // End +frame.primaryAxisAlignItems = "SPACE_BETWEEN"; // Distribute + +// Cross axis +frame.counterAxisAlignItems = "MIN"; // Start +frame.counterAxisAlignItems = "CENTER"; // Center +frame.counterAxisAlignItems = "MAX"; // End +// NOTE: 'STRETCH' is NOT valid — use 'MIN' + child.layoutSizingX = 'FILL' +``` + +### Child Sizing + +```javascript +// IMPORTANT: FILL can only be set AFTER the child is appended to an auto-layout parent +parent.appendChild(child) +child.layoutSizingHorizontal = "FILL"; // Stretch to parent +child.layoutSizingHorizontal = "HUG"; // Shrink to content +child.layoutSizingHorizontal = "FIXED"; // Manual width + +child.layoutSizingVertical = "FILL"; +child.layoutSizingVertical = "HUG"; +child.layoutSizingVertical = "FIXED"; +``` + +### Wrapping (Grid-like Layout) + +```javascript +frame.layoutMode = "HORIZONTAL"; +frame.layoutWrap = "WRAP"; +frame.itemSpacing = 24; // Horizontal gap +frame.counterAxisSpacing = 24; // Vertical gap (between rows) +``` + +### Absolute Positioning Within Auto Layout + +```javascript +child.layoutPositioning = "ABSOLUTE"; +child.constraints = { horizontal: "MAX", vertical: "MIN" }; // Top-right +child.x = parentWidth - childWidth - 8; +child.y = 8; +``` + +## Effects + +### Drop Shadow + +```javascript +node.effects = [{ + type: "DROP_SHADOW", + color: { r: 0, g: 0, b: 0, a: 0.08 }, + offset: { x: 0, y: 4 }, + radius: 16, + spread: -2, + visible: true, + blendMode: "NORMAL" +}]; +``` + +### Inner Shadow + +```javascript +node.effects = [{ + type: "INNER_SHADOW", + color: { r: 0, g: 0, b: 0, a: 0.05 }, + offset: { x: 0, y: 1 }, + radius: 2, + spread: 0, + visible: true, + blendMode: "NORMAL" +}]; +``` + +### Background Blur + +```javascript +node.effects = [{ + type: "BACKGROUND_BLUR", + radius: 16, + visible: true +}]; +``` + +### Layer Blur + +```javascript +node.effects = [{ + type: "LAYER_BLUR", + radius: 8, + visible: true +}]; +``` + +### Multiple Effects + +```javascript +node.effects = [ + { type: "DROP_SHADOW", color: { r: 0, g: 0, b: 0, a: 0.04 }, 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.06 }, offset: { x: 0, y: 8 }, radius: 24, spread: -4, visible: true, blendMode: "NORMAL" } +]; +``` + +## Opacity & Blend Modes + +```javascript +node.opacity = 0.5; +node.blendMode = "NORMAL"; // "MULTIPLY", "SCREEN", "OVERLAY", "DARKEN", "LIGHTEN", etc. +``` + +## Corner Radius + +```javascript +// Uniform +node.cornerRadius = 12; + +// Per-corner +node.topLeftRadius = 12; +node.topRightRadius = 12; +node.bottomLeftRadius = 0; +node.bottomRightRadius = 0; +``` + +## Clipping + +```javascript +frame.clipsContent = true; // Children clipped to frame bounds +``` + +## Grouping & Organization + +### Groups + +```javascript +const group = figma.group([node1, node2, node3], figma.currentPage); +group.name = "Grouped Elements"; +``` + +### Sections + +```javascript +const section = figma.createSection(); +section.name = "My Section"; +section.resize(800, 600); // `resize` and `resizeWithoutConstraints` are equivalent on sections +section.x = 0; +section.y = 0; +// IMPORTANT: Sections don't auto-resize — always resize after adding content +``` + +### Appending Children + +```javascript +parentFrame.appendChild(childNode); + +// Insert at a specific index +parentFrame.insertChild(0, childNode); // Insert at beginning +``` + +## Components & Variants + +### Create Component + +```javascript +const component = figma.createComponent(); +component.name = "Button/Primary"; +component.description = "Primary action button."; +``` + +### Create Instance + +```javascript +const instance = component.createInstance(); +instance.x = 200; +instance.y = 100; +``` + +### Import Components by Key (Team Libraries) + +These methods import components from **team libraries** (not the same file). For components in the current file, use `figma.getNodeByIdAsync()` or `findOne()`/`findAll()`. + +```javascript +// Import a published component from a team library by its key +const comp = await figma.importComponentByKeyAsync(componentKey) +const instance = comp.createInstance() + +// Import a published component set from a team library by its key +const set = await figma.importComponentSetByKeyAsync(componentSetKey) +const variant = set.defaultVariant +const variantInstance = variant.createInstance() +``` + +### Combine as Variants + +```javascript +// IMPORTANT: Pass ComponentNodes (not frames) +const componentSet = figma.combineAsVariants( + [variantA, variantB, variantC], + figma.currentPage +); +componentSet.name = "Button"; +componentSet.description = "Button component with multiple variants."; + +// CRITICAL: Layout variants in a grid after combining (they stack at 0,0) +let maxX = 0, maxY = 0; +componentSet.children.forEach((child, i) => { + child.x = (i % numCols) * colWidth; + child.y = Math.floor(i / numCols) * rowHeight; +}); +for (const child of componentSet.children) { + maxX = Math.max(maxX, child.x + child.width); + maxY = Math.max(maxY, child.y + child.height); +} +componentSet.resizeWithoutConstraints(maxX + 40, maxY + 40); +``` + +### Component Properties + +```javascript +// addComponentProperty returns a STRING key — capture it! +const labelKey = component.addComponentProperty("label", "TEXT", "Button"); +const showIconKey = component.addComponentProperty("showIcon", "BOOLEAN", true); +const iconSlotKey = component.addComponentProperty("iconSlot", "INSTANCE_SWAP", defaultIconId); + +// MUST link properties to child nodes via componentPropertyReferences +labelNode.componentPropertyReferences = { characters: labelKey }; +iconInstance.componentPropertyReferences = { + visible: showIconKey, + mainComponent: iconSlotKey +}; +``` + +## Styles + +### Text Style + +```javascript +await figma.loadFontAsync({ family: "Inter", style: "Regular" }); + +const style = figma.createTextStyle(); +style.name = "Body/Default"; +style.fontName = { family: "Inter", style: "Regular" }; +style.fontSize = 16; +style.lineHeight = { value: 24, unit: "PIXELS" }; +style.letterSpacing = { value: 0, unit: "PERCENT" }; + +// Apply to a text node +textNode.textStyleId = style.id; +``` + +### Effect Style + +```javascript +const shadowStyle = figma.createEffectStyle(); +shadowStyle.name = "Shadow/Subtle"; +shadowStyle.effects = [{ + type: "DROP_SHADOW", + color: { r: 0, g: 0, b: 0, a: 0.06 }, + offset: { x: 0, y: 2 }, + radius: 8, + spread: 0, + visible: true, + blendMode: "NORMAL" +}]; + +// Apply to a node +frame.effectStyleId = shadowStyle.id; +``` + +## Cloning & Duplication + +```javascript +const clone = originalNode.clone(); +clone.x = originalNode.x + originalNode.width + 40; +clone.name = "Copy of " + originalNode.name; +``` + +## Finding Nodes + +```javascript +// Find by name on current page +const node = figma.currentPage.findOne(n => n.name === "My Frame"); + +// Find all by type +const allTexts = figma.currentPage.findAll(n => n.type === "TEXT"); + +// Find all by name pattern +const allButtons = figma.currentPage.findAll(n => n.name.startsWith("Button/")); +``` + +## Layout Grids + +```javascript +frame.layoutGrids = [ + { + pattern: "COLUMNS", + alignment: "STRETCH", + count: 12, + gutterSize: 24, + offset: 80, + visible: true + } +]; +``` + +## Constraints (Non-Auto-Layout Frames) + +```javascript +child.constraints = { + horizontal: "LEFT_RIGHT", // LEFT, RIGHT, CENTER, LEFT_RIGHT, SCALE + vertical: "TOP" // TOP, BOTTOM, CENTER, TOP_BOTTOM, SCALE +}; +``` + +## Viewport & Zoom + +```javascript +// Zoom to fit specific nodes +figma.viewport.scrollAndZoomIntoView([frame1, frame2]); +``` diff --git a/.agents/skills/figma-use/references/plugin-api-standalone.d.ts b/.agents/skills/figma-use/references/plugin-api-standalone.d.ts new file mode 100644 index 00000000000..fc8aea7cc82 --- /dev/null +++ b/.agents/skills/figma-use/references/plugin-api-standalone.d.ts @@ -0,0 +1,11428 @@ +// https://raw.githubusercontent.com/figma/plugin-typings/refs/heads/master/plugin-api-standalone.d.ts + +/* plugin-typings are auto-generated. Do not update them directly. See developer-docs/ for instructions. */ +/** + * NOTE: This file is useful if you want to import specific types eg. + * import type { SceneNode } from "@figma/plugin-typings/plugin-api-standalone" + */ +/** + * @see https://developers.figma.com/docs/plugins/api/properties/figma-on + */ +declare type ArgFreeEventType = + | 'selectionchange' + | 'currentpagechange' + | 'close' + | 'timerstart' + | 'timerstop' + | 'timerpause' + | 'timerresume' + | 'timeradjust' + | 'timerdone' +/** + * @see https://developers.figma.com/docs/plugins/api/figma + */ +interface PluginAPI { + /** + * The version of the Figma API this plugin is running on, as defined in your `manifest.json` in the `"api"` field. + */ + readonly apiVersion: '1.0.0' + /** + * The currently executing command from the `manifest.json` file. It is the command string in the `ManifestMenuItem` (more details in the [manifest guide](https://developers.figma.com/docs/plugins/manifest)). If the plugin does not have any menu item, this property is undefined. + */ + readonly command: string + /** + * The current editor type this plugin is running in. See also [Setting editor type](https://developers.figma.com/docs/plugins/setting-editor-type). + */ + readonly editorType: 'figma' | 'figjam' | 'dev' | 'slides' | 'buzz' + /** + * Return the context the plugin is current running in. + * + * - `default` - The plugin is running as a normal plugin. + * - `textreview` - The plugin is running to provide text review functionality. + * - `inspect` - The plugin is running in the Inspect panel in Dev Mode. + * - `codegen` - The plugin is running in the Code section of the Inspect panel in Dev Mode. + * - `linkpreview` - The plugin is generating a link preview for a [Dev resource](https://help.figma.com/hc/en-us/articles/15023124644247#Add_external_links_and_resources_for_developers) in Dev Mode. + * - `auth` - The plugin is running to authenticate a user in Dev Mode. + * + * Caution: The `linkpreview` and `auth` modes are only available to partner and Figma-owned plugins. + * + * @remarks + * Here’s a simplified example where you can create an if statement in a plugin that has one set of functionality when it is run in `Dev Mode`, and another set of functionality when run in Figma design: + * ```ts title="Code sample to determine editorType and mode" + * if (figma.editorType === "dev") { + * // Read the document and listen to API events + * if (figma.mode === "inspect") { + * // Running in inspect panel mode + * } else if (figma.mode === "codegen") { + * // Running in codegen mode + * } + * } else if (figma.editorType === "figma") { + * // If the plugin is run in Figma design, edit the document + * if (figma.mode === 'textreview') { + * // Running in text review mode + * } + * } else if (figma.editorType === "figjam") { + * // Do FigJam only operations + * if (figma.mode === 'textreview') { + * // Running in text review mode + * } + * } + * ``` + */ + readonly mode: 'default' | 'textreview' | 'inspect' | 'codegen' | 'linkpreview' | 'auth' + /** + * The value specified in the `manifest.json` "id" field. This only exists for Plugins. + */ + readonly pluginId?: string + /** + * Similar to `figma.pluginId` but for widgets. The value specified in the `manifest.json` "id" field. This only exists for Widgets. + */ + readonly widgetId?: string + /** + * The file key of the current file this plugin is running on. + * **Only [private plugins](https://help.figma.com/hc/en-us/articles/4404228629655-Create-private-organization-plugins) and Figma-owned resources (such as the Jira and Asana widgets) have access to this.** + * To enable this behavior, you need to specify `enablePrivatePluginApi` in your `manifest.json`. + */ + readonly fileKey: string | undefined + /** + * When enabled, causes all node properties and methods to skip over invisible nodes (and their descendants) inside {@link InstanceNode | instances}. + * This makes operations like document traversal much faster. + * + * Note: Defaults to true in Figma Dev Mode and false in Figma and FigJam + * + * @remarks + * + * Accessing and modifying invisible nodes and their descendants inside instances can be slow with the plugin API. + * This is especially true in large documents with tens of thousands of nodes where a call to {@link ChildrenMixin.findAll} might come across many of these invisible instance children. + * + * If your plugin does not need access to these nodes, we recommend setting `figma.skipInvisibleInstanceChildren = true` as that often makes document traversal significantly faster. + * + * When this flag is enabled, it will not be possible to access invisible nodes (and their descendants) inside instances. This has the following effects: + * + * - {@link ChildrenMixin.children} and methods such as {@link ChildrenMixin.findAll} will exclude these nodes. + * - {@link PluginAPI.getNodeByIdAsync} will return a promise containing null. + * - {@link PluginAPI.getNodeById} will return null. + * - Accessing a property on an existing node object for an invisible node will throw an error. + * + * For example, suppose that a portion of the document tree looks like this: + * + * Frame (visible) → Instance (visible) → Frame (invisible) → Text (visible) + * + * The last two frame and text nodes cannot be accessed after setting `figma.skipInvisibleInstanceChildren = true`. + * + * The benefit of enabling this flag is that document traversal methods, {@link ChildrenMixin.findAll} and {@link ChildrenMixin.findOne}, can be up to several times faster in large documents that have invisible instance children. + * {@link ChildrenMixin.findAllWithCriteria} can be up to hundreds of times faster in large documents. + */ + skipInvisibleInstanceChildren: boolean + /** + * Note: This API is only available in FigJam + * + * This property contains methods used to read, set, and modify the built in FigJam timer. + * + * Read more in the [timer section](https://developers.figma.com/docs/plugins/api/figma-timer). + */ + readonly timer?: TimerAPI + /** + * This property contains methods used to read and set the viewport, the user-visible area of the current page. + * + * Read more in the [viewport section](https://developers.figma.com/docs/plugins/api/figma-viewport). + */ + readonly viewport: ViewportAPI + /** + * Note: `currentuser` must be specified in the permissions array in `manifest.json` to access this property. + * + * This property contains details about the current user. + */ + readonly currentUser: User | null + /** + * Note: This API is only available in FigJam. + * + * `activeusers` must be specified in the permissions array in `manifest.json` to access this property. + * + * This property contains details about the active users in the file. `figma.activeUsers[0]` will match `figma.currentUser` for the `id`, `name`, `photoUrl`, `color`, and `sessionId` properties. + */ + readonly activeUsers: ActiveUser[] + /** + * Note: `textreview` must be specified in the capabilities array in `manifest.json` to access this property. + * + * This property contains methods that enable text review features in your plugin. + */ + readonly textreview?: TextReviewAPI + /** + * This property contains methods used to integrate with the Dev Mode codegen functionality. + * + * Read more in the [codegen section](https://developers.figma.com/docs/plugins/api/figma-codegen). + */ + readonly codegen: CodegenAPI + /** + * This property contains methods used to integrate with the Figma for VS Code extension. If `undefined`, the plugin is not running in VS Code. + * + * Read more in [Dev Mode plugins in Visual Studio Code](https://developers.figma.com/docs/plugins/working-in-dev-mode#dev-mode-plugins-in-visual-studio-code) + */ + readonly vscode?: VSCodeAPI + /** + * Caution: This is a private API only available to [Figma partners](https://www.figma.com/partners/) + */ + readonly devResources?: DevResourcesAPI + /** + * Note: `payments` must be specified in the permissions array in `manifest.json` to access this property. + * + * This property contains methods for plugins that require payment. + */ + readonly payments?: PaymentsAPI + /** + * Closes the plugin. You should always call this function once your plugin is done running. When called, any UI that's open will be closed and any `setTimeout` or `setInterval` timers will be cancelled. + * + * @param message - Optional -- display a visual bell toast with the message after the plugin closes. + * + * @remarks + * + * Calling `figma.closePlugin()` disables callbacks and Figma APIs. It does not, however, abort the plugin. Any lines of Javascript after this call will also run. For example, consider the following plugin that expects the user to have one layer selected: + * + * ```ts title="Simple closePlugin" + * if (figma.currentPage.selection.length !== 1) { + * figma.closePlugin() + * } + * figma.currentPage.selection[0].opacity = 0.5 + * ``` + * + * This will not work. The last line will still run, but will throw an exception because access to `figma.currentPage` has been disabled. As such, it is not recommended to run any code after calling `figma.closePlugin()`. + * + * A simple way to easily exit your plugin is to wrap your plugin in a function, instead of running code at the top-level, and always follow `figma.closePlugin()` with a `return` statement: + * + * ```ts title="Early return" + * function main() { + * if (figma.currentPage.selection.length !== 1) { + * figma.closePlugin() + * return + * } + * figma.currentPage.selection[0].opacity = 0.5 + * } + * main() + * ``` + * + * It's good practice to have all input validation done at the start of the plugin. However, there may be cases where the plugin may need to close after a chain of multiple function calls. If you expect to have to close the plugin deep within your code, but don't want to necessarily want the user to see an error, the example above will not be sufficient. + * + * One alternative is to use a top-level try-catch statement. However, you will need to be responsible for making sure that there are no usages of try-catch between the top-level try-catch and the call to `figma.closePlugin()`, or to pass along the close command if necessary. Example: + * + * ```ts title="Top-level try-catch" + * const CLOSE_PLUGIN_MSG = "_CLOSE_PLUGIN_" + * function someNestedFunctionCallThatClosesThePlugin() { + * throw CLOSE_PLUGIN_MSG + * } + * + * function main() { + * someNestedFunctionCallThatClosesThePlugin() + * } + * + * try { + * main() + * } catch (e) { + * if (e === CLOSE_PLUGIN_MSG) { + * figma.closePlugin() + * } else { + * // >> DO NOT LEAVE THIS OUT << + * // If we caught any other kind of exception, + * // it's a real error and should be passed along. + * throw e + * } + * } + * ``` + */ + closePlugin(message?: string): void + /** + * Shows a notification on the bottom of the screen. + * + * @param message - The message to show. It is limited to 100 characters. Longer messages will be truncated. + * @param options - An optional argument with the following optional parameters: + * + * ```ts + * interface NotificationOptions { + * timeout?: number; + * error?: boolean; + * onDequeue?: (reason: NotifyDequeueReason) => void + * button?: { + * text: string + * action: () => boolean | void + * } + * } + * ``` + * + * - `timeout`: How long the notification stays up in milliseconds before closing. Defaults to 3 seconds when not specified. Set the timeout to `Infinity` to make the notification show indefinitely until the plugin is closed. + * - `error`: If true, display the notification as an error message, with a different color. + * - `onDequeue`: A function that will run when the notification is dequeued. This can happen due to the timeout being reached, the notification being dismissed by the user or Figma, or the user clicking the notification's `button`. + * - The function is passed a `NotifyDequeueReason`, which is defined as the following: + * ```ts + * type NotifyDequeueReason = 'timeout' | 'dismiss' | 'action_button_click' + * ``` + * - `button`: An object representing an action button that will be added to the notification. + * - `text`: The message to display on the action button. + * - `action`: The function to execute when the user clicks the button. If this function returns `false`, the message will remain when the button is clicked. Otherwise, clicking the action button dismisses the notify message. + * + * @remarks + * + * The `notify` API is a convenient way to show a message to the user. These messages can be queued. + * + * If the message includes a custom action button, it will be closed automatically when the plugin closes. + * + * Calling `figma.notify` returns a `NotificationHandler` object. This object contains a single `handler.cancel()` method that can be used to remove the notification before it times out by itself. This is useful if the notification becomes no longer relevant. + * + * ```ts + * interface NotificationHandler { + * cancel: () => void + * } + * ``` + * + * An alternative way to show a message to the user is to pass a message to the {@link PluginAPI.closePlugin} function. + */ + notify(message: string, options?: NotificationOptions): NotificationHandler + /** + * Commits actions to undo history. This does not trigger an undo. + * + * @remarks + * + * By default, plugin actions are not committed to undo history. Call `figma.commitUndo()` so that triggered + * undos can revert a subset of plugin actions. + * + * For example, after running the following plugin code, the first triggered undo will undo both the rectangle and the ellipse: + * ```ts + * figma.createRectangle(); + * figma.createEllipse(); + * figma.closePlugin(); + * ``` + * Whereas if we call `commitUndo()` in our plugin, the first triggered undo will only undo the ellipse: + * ```ts + * figma.createRectangle(); + * figma.commitUndo(); + * figma.createEllipse(); + * figma.closePlugin(); + * ``` + */ + commitUndo(): void + /** + * Triggers an undo action. Reverts to the last `commitUndo()` state. + */ + triggerUndo(): void + /** + * Saves a new version of the file and adds it to the version history of the file. Returns the new version id. + * @param title - The title of the version. This must be a non-empty string. + * @param description - An optional argument to describe the version. + * + * Calling `saveVersionHistoryAsync` returns a promise that resolves to `null` or an instance of `VersionHistoryResult`: + * + * ```ts + * interface VersionHistoryResult { + * id: string + * } + * ``` + * + * - `id`: The version id of this newly saved version. + * + * @remarks + * + * It is not guaranteed that all changes made before this method is used will be saved to version history. + * For example, + * ```ts title="Changes may not all be saved" + * async function example() { + * await figma.createRectangle(); + * await figma.saveVersionHistoryAsync('v1'); + * figma.closePlugin(); + * } + * example().catch((e) => figma.closePluginWithFailure(e)) + * ``` + * The newly created rectangle may not be included in the v1 version. As a work around, you can wait before calling `saveVersionHistoryAsync()`. For example, + * ```ts title="Wait to save" + * async function example() { + * await figma.createRectangle(); + * await new Promise(r => setTimeout(r, 1000)); // wait for 1 second + * await figma.saveVersionHistoryAsync('v1'); + * figma.closePlugin(); + * } + * ``` + * Typically, manual changes that precede the execution of `saveVersionHistoryAsync()` will be included. If you want to use `saveVersionHistoryAsync()` before the plugin makes + * additional changes, make sure to use the method with an async/await or a Promise. + */ + saveVersionHistoryAsync(title: string, description?: string): Promise + /** + * Open a url in a new tab. + * + * @remarks + * + * In the VS Code Extension, this API is required to open a url in the browser. Read more in [Dev Mode plugins in Visual Studio Code](https://developers.figma.com/docs/plugins/working-in-dev-mode#dev-mode-plugins-in-visual-studio-code). + */ + openExternal(url: string): void + /** + * Enables you to render UI to interact with the user, or simply to access browser APIs. This function creates a modal dialog with an `