Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
---
|
||||
name: edit-figma-design
|
||||
description: Create or update Figma designs directly from a written product or UI description using the Figma MCP authoring tools. Use when the user wants a mockup, wireframe, screen, component, flow, or concept designed in Figma from text, or wants to iterate on an existing Figma file from textual feedback. Despite the name, this skill can start from a new blank file or edit an existing one. Do not use for capture-based workflows that turn a running page into Figma; use `figma-generate-design` for those, and use `implement-design` for code implementation requests. Requires Figma MCP server connection.
|
||||
metadata:
|
||||
mcp-server: figma
|
||||
---
|
||||
|
||||
# Edit Figma Design
|
||||
|
||||
## Overview
|
||||
|
||||
This skill creates or updates Figma designs directly from a natural-language description. It combines Figma library search with direct file authoring, and uses Warp's broader agent capabilities only when they are needed to make the design more product-aware or codebase-aware.
|
||||
|
||||
## When to use this skill
|
||||
|
||||
Use this skill when the user wants you to:
|
||||
|
||||
- design a new screen, flow, component, or mockup in Figma from a written description
|
||||
- refine or extend an existing Figma file from text feedback
|
||||
- create a first-pass wireframe or higher-fidelity design directly in Figma
|
||||
- align a Figma design to an existing design system or product vocabulary
|
||||
|
||||
Do not use this skill when:
|
||||
|
||||
- the user wants production code from a design — use `implement-design`
|
||||
- the user wants to capture a running page or app into Figma — use `figma-generate-design`
|
||||
- the user only wants to inspect or pull existing Figma context — use `pull-figma-content`
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Figma MCP server must be connected and accessible.
|
||||
- Verify that `search_design_system`, `create_new_file`, and `use_figma` are available.
|
||||
- Gather the minimum information needed to proceed:
|
||||
- what should be designed
|
||||
- whether to use an existing Figma file or create a new one
|
||||
- whether the result should align to an existing design system or codebase
|
||||
- Ask clarifying questions only when the user has not already given enough detail to start. Keep them short and batch them into one message when possible.
|
||||
|
||||
## Required Workflow
|
||||
|
||||
**Follow these steps in order. Do not skip steps.**
|
||||
|
||||
### Step 1: Confirm this is a Figma-authoring request
|
||||
|
||||
If the user is actually asking for implementation, stop and consult `implement-design`.
|
||||
|
||||
If the user wants a screenshot-to-Figma or webpage capture flow, stop and consult `figma-generate-design`. That skill is for capture-based workflows; this skill is for text-to-design authoring.
|
||||
|
||||
### Step 2: Resolve the destination file first
|
||||
|
||||
Both `search_design_system` and `use_figma` need a `fileKey`, so determine the destination before searching or editing.
|
||||
|
||||
**If the user provided an existing Figma URL or file key:**
|
||||
|
||||
- Extract and use that `fileKey`.
|
||||
- Reuse the provided URL when you respond.
|
||||
|
||||
**If the user wants a new file:**
|
||||
|
||||
1. Decide on a clear file name from the request.
|
||||
2. If the user already provided a `planKey`, use it.
|
||||
3. Otherwise call the Figma MCP `whoami` tool to inspect the authenticated Figma user and available plans. This is not the shell `whoami` command.
|
||||
4. If there is exactly one plan, use its `key`.
|
||||
5. If there are multiple plans, ask the user which team or organization to use.
|
||||
6. Call `create_new_file(editorType="design", fileName=..., planKey=...)`.
|
||||
7. Save the returned `fileKey` and URL. Share the URL once the first usable draft is ready.
|
||||
|
||||
### Step 3: Gather the right context, but only when it is needed
|
||||
|
||||
Decide how much non-Figma context is actually necessary.
|
||||
|
||||
**Stay inside Figma MCP only** when the user wants an exploratory concept, wireframe, or mockup and does not ask for codebase alignment.
|
||||
|
||||
**Use Warp agent context selectively** when the user wants the design to match an existing product or design system:
|
||||
|
||||
- read project rules from `AGENTS.md` and/or `WARP.md` if they exist
|
||||
- use semantic codebase search, grep, and file reads to find relevant components, product vocabulary, layout patterns, and design-token sources
|
||||
- use other MCP sources or web search only when the prompt directly depends on them, such as product requirements in another system or explicit inspiration requests
|
||||
- do not edit code, run REPL commands, or use computer use as part of this skill's normal workflow
|
||||
|
||||
### Step 4: Search the design system before authoring
|
||||
|
||||
Call `search_design_system` with the resolved `fileKey` before creating new components or styles.
|
||||
|
||||
Search for the most reusable assets first:
|
||||
|
||||
- components and component sets
|
||||
- variables and token-like values
|
||||
- styles for color, typography, spacing, or effects
|
||||
|
||||
Start with the user's domain terms and any names discovered from project rules or codebase search.
|
||||
|
||||
If needed, narrow follow-up searches with returned library keys rather than immediately broadening the search.
|
||||
|
||||
Prefer reusing and importing matches over recreating them from scratch.
|
||||
|
||||
### Step 5: Prepare `use_figma` safely
|
||||
|
||||
Before the first `use_figma` call, plan the edit sequence and follow the tool's required Plugin API constraints.
|
||||
|
||||
Keep the authoring plan incremental:
|
||||
|
||||
1. create page and frame structure
|
||||
2. establish layout and major sections
|
||||
3. reuse or import design-system assets
|
||||
4. apply variables, styles, and typography
|
||||
5. add content and polish
|
||||
6. make targeted revisions based on what the file now contains
|
||||
|
||||
### Step 6: Edit the design in small `use_figma` steps
|
||||
|
||||
Use multiple small `use_figma` calls instead of one giant script.
|
||||
|
||||
Good step boundaries:
|
||||
|
||||
- create a page and top-level frames
|
||||
- lay out a header, sidebar, hero, or content region
|
||||
- import or place one family of reusable components
|
||||
- bind colors, text styles, or spacing variables
|
||||
- update copy, states, or alignment for a specific section
|
||||
|
||||
After each step, inspect the result and only continue once the previous step succeeded.
|
||||
|
||||
When creating anything component-like, prefer imported library assets discovered in Step 4.
|
||||
|
||||
### Step 7: Hand back the design and next options
|
||||
|
||||
When the first usable draft is ready:
|
||||
|
||||
- return the Figma URL if you have it
|
||||
- summarize what you created or updated at a high level
|
||||
- ask whether the user wants revisions in Figma
|
||||
|
||||
If the user asks to implement the approved design in code, stop using this skill and consult `implement-design`.
|
||||
|
||||
## Warp-agent guidance
|
||||
|
||||
Use Warp's broader capabilities to reduce manual prompting, not to add unnecessary work.
|
||||
|
||||
**Good uses of Warp agent capabilities in this skill:**
|
||||
|
||||
- finding existing component names or design tokens in the repo
|
||||
- reading project rules that constrain layout, naming, or branding
|
||||
- pulling product requirements from other connected systems when the user explicitly relies on them
|
||||
|
||||
**Usually unnecessary for this skill:**
|
||||
|
||||
- shell commands or REPL access
|
||||
- code edits
|
||||
- computer-use validation
|
||||
- broad web research without a specific user request
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: New file from a product description
|
||||
|
||||
User says: "Design a billing overview screen in Figma for our desktop app. Use our existing design system and create a new file."
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Confirm this is Figma authoring, not code implementation.
|
||||
2. Resolve the destination by calling `whoami` if needed, then `create_new_file`.
|
||||
3. Read `AGENTS.md` or `WARP.md`, or search the codebase only if needed to understand billing terminology and existing components.
|
||||
4. Call `search_design_system` with billing-related queries.
|
||||
5. Build the screen in small `use_figma` steps.
|
||||
6. Return the new Figma file URL and offer to revise.
|
||||
|
||||
### Example 2: Update an existing Figma file
|
||||
|
||||
User says: "Add an onboarding checklist to this Figma file: https://figma.com/design/FILEKEY/Product?node-id=1-2"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Extract the `fileKey` from the existing URL.
|
||||
2. Search the design system for checklist, card, badge, and progress assets before creating anything new.
|
||||
3. Use incremental `use_figma` calls to add the new section.
|
||||
4. Return the same Figma URL and summarize the change.
|
||||
|
||||
### Example 3: Pure exploratory concept
|
||||
|
||||
User says: "Create a first-pass mobile workout planner mockup in Figma. It doesn't need to match my codebase yet."
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Create a new file if needed.
|
||||
2. Skip codebase search and project-rule inspection.
|
||||
3. Use `search_design_system` only to reuse any relevant Figma library assets.
|
||||
4. Build the concept directly in Figma with small `use_figma` steps.
|
||||
5. Share the file link and ask what to refine next.
|
||||
|
||||
## Common issues and responses
|
||||
|
||||
### Issue: The user hasn't said whether to use an existing file or a new one
|
||||
|
||||
Ask one direct question that resolves the destination. Do not start `search_design_system` or `use_figma` until you have a `fileKey`.
|
||||
|
||||
### Issue: Multiple Figma plans are available for `create_new_file`
|
||||
|
||||
Ask the user which team or organization to use. Do not guess.
|
||||
|
||||
### Issue: The user wants the design to match existing product conventions, but the request is vague
|
||||
|
||||
Read the project's rules first. Then use targeted codebase search to gather only the components and conventions relevant to the requested surface.
|
||||
|
||||
### Issue: The user asks for both a Figma design and implementation
|
||||
|
||||
Create or update the Figma design first only if the user's request is primarily about authoring in Figma. If the request is primarily about implementation, consult `implement-design` instead. After the design is approved, implementation can follow in a separate step.
|
||||
|
||||
### Issue: `use_figma` fails or the script is getting large
|
||||
|
||||
Break the task into smaller `use_figma` calls. Prefer structure first, then styling, then targeted revisions.
|
||||
|
||||
## Additional resources
|
||||
|
||||
- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/)
|
||||
- [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/)
|
||||
@@ -0,0 +1,350 @@
|
||||
---
|
||||
name: figma-code-connect-components
|
||||
description: Connects Figma design components to code components using Code Connect mapping tools. Use when user says "code connect", "connect this component to code", "map this component", "link component to code", "create code connect mapping", or wants to establish mappings between Figma designs and code implementations. For canvas writes via `use_figma`, use `figma-use`.
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Code Connect Components
|
||||
|
||||
## Overview
|
||||
|
||||
This skill helps you connect Figma design components to their corresponding code implementations using Figma's Code Connect feature. It analyzes the Figma design structure, searches your codebase for matching components, and establishes mappings that maintain design-code consistency.
|
||||
|
||||
## Skill Boundaries
|
||||
|
||||
- Use this skill for `get_code_connect_suggestions` + `send_code_connect_mappings` workflows.
|
||||
- If the task requires writing to the Figma canvas with Plugin API scripts, switch to [figma-use](../figma-use/SKILL.md).
|
||||
- If the task is building or updating a full-page screen in Figma from code or a description, switch to [figma-generate-design](../figma-generate-design/SKILL.md).
|
||||
- If the task is implementing product code from Figma, switch to [figma-implement-design](../figma-implement-design/SKILL.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Figma MCP server must be connected and accessible
|
||||
- User must provide a Figma URL with node ID: `https://figma.com/design/:fileKey/:fileName?node-id=1-2`
|
||||
- **IMPORTANT:** The Figma URL must include the `node-id` parameter. Code Connect mapping will fail without it.
|
||||
- **OR** when using `figma-desktop` MCP: User can select a node directly in the Figma desktop app (no URL required)
|
||||
- **IMPORTANT:** The Figma component must be published to a team library. Code Connect only works with published components or component sets.
|
||||
- **IMPORTANT:** Code Connect is only available on Organization and Enterprise plans.
|
||||
- Access to the project codebase for component scanning
|
||||
|
||||
## Required Workflow
|
||||
|
||||
**Follow these steps in order. Do not skip steps.**
|
||||
|
||||
### Step 1: Get Code Connect Suggestions
|
||||
|
||||
Call `get_code_connect_suggestions` to identify all unmapped components in a single operation. This tool automatically:
|
||||
|
||||
- Fetches component info from the Figma scenegraph
|
||||
- Identifies published components in the selection
|
||||
- Checks existing Code Connect mappings and filters out already-connected components
|
||||
- Returns component names, properties, and thumbnail images for each unmapped component
|
||||
|
||||
#### Option A: Using `figma-desktop` MCP (no URL provided)
|
||||
|
||||
If the `figma-desktop` MCP server is connected and the user has NOT provided a Figma URL, immediately call `get_code_connect_suggestions`. No URL parsing is needed — the desktop MCP server automatically uses the currently selected node from the open Figma file.
|
||||
|
||||
**Note:** The user must have the Figma desktop app open with a node selected. `fileKey` is not passed as a parameter — the server uses the currently open file.
|
||||
|
||||
#### Option B: When a Figma URL is provided
|
||||
|
||||
Parse the URL to extract `fileKey` and `nodeId`, then call `get_code_connect_suggestions`.
|
||||
|
||||
**IMPORTANT:** When extracting the node ID from a Figma URL, convert the format:
|
||||
|
||||
- URL format uses hyphens: `node-id=1-2`
|
||||
- Tool expects colons: `nodeId=1:2`
|
||||
|
||||
**Parse the Figma URL:**
|
||||
|
||||
- URL format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2`
|
||||
- Extract file key: `:fileKey` (segment after `/design/`)
|
||||
- Extract node ID: `1-2` from URL, then convert to `1:2` for the tool
|
||||
|
||||
```
|
||||
get_code_connect_suggestions(fileKey=":fileKey", nodeId="1:2")
|
||||
```
|
||||
|
||||
**Handle the response:**
|
||||
|
||||
- If the tool returns **"No published components found in this selection"** → inform the user and stop. The components may need to be published to a team library first.
|
||||
- If the tool returns **"All component instances in this selection are already connected to code via Code Connect"** → inform the user that everything is already mapped.
|
||||
- Otherwise, the response contains a list of unmapped components, each with:
|
||||
- Component name
|
||||
- Node ID
|
||||
- Component properties (JSON with prop names and values)
|
||||
- A thumbnail image of the component (for visual inspection)
|
||||
|
||||
### Step 2: Scan Codebase for Matching Components
|
||||
|
||||
For each unmapped component returned by `get_code_connect_suggestions`, search the codebase for a matching code component.
|
||||
|
||||
**What to look for:**
|
||||
|
||||
- Component names that match or are similar to the Figma component name
|
||||
- Component structure that aligns with the Figma hierarchy
|
||||
- Props that correspond to Figma properties (variants, text, styles)
|
||||
- Files in typical component directories (`src/components/`, `components/`, `ui/`, etc.)
|
||||
|
||||
**Search strategy:**
|
||||
|
||||
1. Search for component files with matching names
|
||||
2. Read candidate files to check structure and props
|
||||
3. Compare the code component's props with the Figma component properties returned in Step 1
|
||||
4. Detect the programming language (TypeScript, JavaScript) and framework (React, Vue, etc.)
|
||||
5. Identify the best match based on structural similarity, weighing:
|
||||
- Prop names and their correspondence to Figma properties
|
||||
- Default values that match Figma defaults
|
||||
- CSS classes or style objects
|
||||
- Descriptive comments that clarify intent
|
||||
6. If multiple candidates are equally good, pick the one with the closest prop-interface match and document your reasoning in a 1-2 sentence comment before your tool call
|
||||
|
||||
**Example search patterns:**
|
||||
|
||||
- If Figma component is "PrimaryButton", search for `Button.tsx`, `PrimaryButton.tsx`, `Button.jsx`
|
||||
- Check common component paths: `src/components/`, `app/components/`, `lib/ui/`
|
||||
- Look for variant props like `variant`, `size`, `color` that match Figma variants
|
||||
|
||||
### Step 3: Present Matches to User
|
||||
|
||||
Present your findings and let the user choose which mappings to create. The user can accept all, some, or none of the suggested mappings.
|
||||
|
||||
**Present matches in this format:**
|
||||
|
||||
```
|
||||
The following components match the design:
|
||||
- [ComponentName](path/to/component): DesignComponentName at nodeId [nodeId](figmaUrl?node-id=X-Y)
|
||||
- [AnotherComponent](path/to/another): AnotherDesign at nodeId [nodeId2](figmaUrl?node-id=X-Y)
|
||||
|
||||
Would you like to connect these components? You can accept all, select specific ones, or skip.
|
||||
```
|
||||
|
||||
**If no exact match is found for a component:**
|
||||
|
||||
- Show the 2 closest candidates
|
||||
- Explain the differences
|
||||
- Ask the user to confirm which component to use or provide the correct path
|
||||
|
||||
**If the user declines all mappings**, inform them and stop. No further tool calls are needed.
|
||||
|
||||
### Step 4: Create Code Connect Mappings
|
||||
|
||||
Once the user confirms their selections, call `send_code_connect_mappings` with only the accepted mappings. This tool handles batch creation of all mappings in a single call.
|
||||
|
||||
**Example:**
|
||||
|
||||
```
|
||||
send_code_connect_mappings(
|
||||
fileKey=":fileKey",
|
||||
nodeId="1:2",
|
||||
mappings=[
|
||||
{ nodeId: "1:2", componentName: "Button", source: "src/components/Button.tsx", label: "React" },
|
||||
{ nodeId: "1:5", componentName: "Card", source: "src/components/Card.tsx", label: "React" }
|
||||
]
|
||||
)
|
||||
```
|
||||
|
||||
**Key parameters for each mapping:**
|
||||
|
||||
- `nodeId`: The Figma node ID (with colon format: `1:2`)
|
||||
- `componentName`: Name of the component to connect (e.g., "Button", "Card")
|
||||
- `source`: Path to the code component file (relative to project root)
|
||||
- `label`: The framework or language label for this Code Connect mapping. Valid values include:
|
||||
- Web: 'React', 'Web Components', 'Vue', 'Svelte', 'Storybook', 'Javascript'
|
||||
- iOS: 'Swift UIKit', 'Objective-C UIKit', 'SwiftUI'
|
||||
- Android: 'Compose', 'Java', 'Kotlin', 'Android XML Layout'
|
||||
- Cross-platform: 'Flutter'
|
||||
- Docs: 'Markdown'
|
||||
|
||||
**After the call:**
|
||||
|
||||
- On success: the tool confirms the mappings were created
|
||||
- On error: the tool reports which specific mappings failed and why (e.g., "Component is already mapped to code", "Published component not found", "Insufficient permissions")
|
||||
|
||||
**Provide a summary** after processing:
|
||||
|
||||
```
|
||||
Code Connect Summary:
|
||||
- Successfully connected: 3
|
||||
- Button (1:2) → src/components/Button.tsx
|
||||
- Card (1:5) → src/components/Card.tsx
|
||||
- Input (1:8) → src/components/Input.tsx
|
||||
- Could not connect: 1
|
||||
- CustomWidget (1:10) - No matching component found in codebase
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Connecting a Button Component
|
||||
|
||||
User says: "Connect this Figma button to my code: https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Parse URL: fileKey=`kL9xQn2VwM8pYrTb4ZcHjF`, nodeId=`42-15` → convert to `42:15`
|
||||
2. Run `get_code_connect_suggestions(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42:15")`
|
||||
3. Response shows: Button component (unmapped) with `variant` (primary/secondary) and `size` (sm/md/lg) properties, plus a thumbnail image
|
||||
4. Search codebase for Button components: Find `src/components/Button.tsx`
|
||||
5. Read `Button.tsx` and confirm it has `variant` and `size` props
|
||||
6. Present to user:
|
||||
```
|
||||
I found a match:
|
||||
- [Button](src/components/Button.tsx): Button at nodeId [42:15](https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15)
|
||||
|
||||
Would you like to connect this component?
|
||||
```
|
||||
|
||||
7. User confirms: "Yes"
|
||||
8. Detect that it's a TypeScript React component
|
||||
9. Run `send_code_connect_mappings(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42:15", mappings=[{ nodeId: "42:15", componentName: "Button", source: "src/components/Button.tsx", label: "React" }])`
|
||||
|
||||
**Result:** Figma button component is now connected to the code Button component.
|
||||
|
||||
### Example 2: Multiple Components with Partial Selection
|
||||
|
||||
User says: "Connect components in this frame: https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Components?node-id=10-50"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Parse URL: fileKey=`pR8mNv5KqXzGwY2JtCfL4D`, nodeId=`10-50` → convert to `10:50`
|
||||
2. Run `get_code_connect_suggestions(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10:50")`
|
||||
3. Response shows 3 unmapped components: ProductCard, Badge, and CustomWidget
|
||||
4. Search codebase:
|
||||
- ProductCard: Found `src/components/ProductCard.tsx` (props match)
|
||||
- Badge: Found `src/components/Badge.tsx` (props match)
|
||||
- CustomWidget: No matching component found
|
||||
5. Present to user:
|
||||
```
|
||||
The following components match the design:
|
||||
- [ProductCard](src/components/ProductCard.tsx): ProductCard at nodeId [10:51](https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Components?node-id=10-51)
|
||||
- [Badge](src/components/Badge.tsx): Badge at nodeId [10:52](https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Components?node-id=10-52)
|
||||
|
||||
I couldn't find a match for CustomWidget (10:53).
|
||||
|
||||
Would you like to connect these components? You can accept all, select specific ones, or skip.
|
||||
```
|
||||
|
||||
6. User: "Just connect ProductCard, skip Badge for now"
|
||||
7. Run `send_code_connect_mappings(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10:50", mappings=[{ nodeId: "10:51", componentName: "ProductCard", source: "src/components/ProductCard.tsx", label: "React" }])`
|
||||
|
||||
**Result:** Only ProductCard is connected, per the user's selection.
|
||||
|
||||
### Example 3: Component Needs Creation
|
||||
|
||||
User says: "Connect this icon: https://figma.com/design/8yJDMeWDyBz71EnMOSuUiw/Icons?node-id=5-20"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Parse URL: fileKey=`8yJDMeWDyBz71EnMOSuUiw`, nodeId=`5-20` → convert to `5:20`
|
||||
2. Run `get_code_connect_suggestions(fileKey="8yJDMeWDyBz71EnMOSuUiw", nodeId="5:20")`
|
||||
3. Response shows: CheckIcon component (unmapped) with color and size properties
|
||||
4. Search codebase for CheckIcon: No matches found
|
||||
5. Search for generic Icon components: Find `src/icons/` directory with other icons
|
||||
6. Report to user: "I couldn't find a CheckIcon component, but I found an icons directory at src/icons/. Would you like to:
|
||||
- Create a new CheckIcon.tsx component first, then connect it
|
||||
- Connect to a different existing icon
|
||||
- Provide the path to the CheckIcon if it exists elsewhere"
|
||||
7. User provides path: "src/icons/CheckIcon.tsx"
|
||||
8. Detect language and framework from the file
|
||||
9. Run `send_code_connect_mappings(fileKey="8yJDMeWDyBz71EnMOSuUiw", nodeId="5:20", mappings=[{ nodeId: "5:20", componentName: "CheckIcon", source: "src/icons/CheckIcon.tsx", label: "React" }])`
|
||||
|
||||
**Result:** CheckIcon component is successfully connected to the Figma design.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Proactive Component Discovery
|
||||
|
||||
Don't just ask the user for the file path — actively search their codebase to find matching components. This provides a better experience and catches potential mapping opportunities.
|
||||
|
||||
### Accurate Structure Matching
|
||||
|
||||
When comparing Figma components to code components, look beyond just names. Check that:
|
||||
|
||||
- Props align (variant types, size options, etc.)
|
||||
- Component hierarchy matches (nested elements)
|
||||
- The component serves the same purpose
|
||||
|
||||
### Clear Communication
|
||||
|
||||
When offering to create a mapping, clearly explain:
|
||||
|
||||
- What you found
|
||||
- Why it's a good match
|
||||
- What the mapping will do
|
||||
- How props will be connected
|
||||
|
||||
### Handle Ambiguity
|
||||
|
||||
If multiple components could match, present options rather than guessing. Let the user make the final decision about which component to connect.
|
||||
|
||||
### Graceful Degradation
|
||||
|
||||
If you can't find an exact match, provide helpful next steps:
|
||||
|
||||
- Show close candidates
|
||||
- Suggest component creation
|
||||
- Ask for user guidance
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: "No published components found in this selection"
|
||||
|
||||
**Cause:** The Figma component is not published to a team library. Code Connect only works with published components.
|
||||
**Solution:** The user needs to publish the component to a team library in Figma:
|
||||
|
||||
1. In Figma, select the component or component set
|
||||
2. Right-click and choose "Publish to library" or use the Team Library publish modal
|
||||
3. Publish the component
|
||||
4. Once published, retry the Code Connect mapping with the same node ID
|
||||
|
||||
### Issue: "Code Connect is only available on Organization and Enterprise plans"
|
||||
|
||||
**Cause:** The user's Figma plan does not include Code Connect access.
|
||||
**Solution:** The user needs to upgrade to an Organization or Enterprise plan, or contact their administrator.
|
||||
|
||||
### Issue: No matching component found in codebase
|
||||
|
||||
**Cause:** The codebase search did not find a component with a matching name or structure.
|
||||
**Solution:** Ask the user if the component exists under a different name or in a different location. They may need to create the component first, or it might be located in an unexpected directory.
|
||||
|
||||
### Issue: "Published component not found" (CODE_CONNECT_ASSET_NOT_FOUND)
|
||||
|
||||
**Cause:** The source file path is incorrect, the component doesn't exist at that location, or the componentName doesn't match the actual export.
|
||||
**Solution:** Verify the source path is correct and relative to the project root. Check that the component is properly exported from the file with the exact componentName specified.
|
||||
|
||||
### Issue: "Component is already mapped to code" (CODE_CONNECT_MAPPING_ALREADY_EXISTS)
|
||||
|
||||
**Cause:** A Code Connect mapping already exists for this component.
|
||||
**Solution:** The component is already connected. If the user wants to update the mapping, they may need to remove the existing one first in Figma.
|
||||
|
||||
### Issue: "Insufficient permissions to create mapping" (CODE_CONNECT_INSUFFICIENT_PERMISSIONS)
|
||||
|
||||
**Cause:** The user does not have edit permissions on the Figma file or library.
|
||||
**Solution:** The user needs edit access to the file containing the component. Contact the file owner or team admin.
|
||||
|
||||
### Issue: Code Connect mapping fails with URL errors
|
||||
|
||||
**Cause:** The Figma URL format is incorrect or missing the `node-id` parameter.
|
||||
**Solution:** Verify the URL follows the required format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2`. The `node-id` parameter is required. Also ensure you convert `1-2` to `1:2` when calling tools.
|
||||
|
||||
### Issue: Multiple similar components found
|
||||
|
||||
**Cause:** The codebase contains multiple components that could match the Figma component.
|
||||
**Solution:** Present all candidates to the user with their file paths and let them choose which one to connect. Different components might be used in different contexts (e.g., `Button.tsx` vs `LinkButton.tsx`).
|
||||
|
||||
## Understanding Code Connect
|
||||
|
||||
Code Connect establishes a bidirectional link between design and code:
|
||||
|
||||
**For designers:** See which code component implements a Figma component
|
||||
**For developers:** Navigate from Figma designs directly to the code that implements them
|
||||
**For teams:** Maintain a single source of truth for component mappings
|
||||
|
||||
The mapping you create helps keep design and code in sync by making these connections explicit and discoverable.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For more information about Code Connect:
|
||||
|
||||
- [Code Connect Documentation](https://help.figma.com/hc/en-us/articles/23920389749655-Code-Connect)
|
||||
- [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/)
|
||||
@@ -0,0 +1,538 @@
|
||||
---
|
||||
name: figma-create-design-system-rules
|
||||
description: Generates custom design system rules for the user's codebase. Use when user says "create design system rules", "generate rules for my project", "set up design rules", "customize design system guidelines", or wants to establish project-specific conventions for Figma-to-code workflows. Requires Figma MCP server connection.
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Create Design System Rules
|
||||
|
||||
## Overview
|
||||
|
||||
This skill helps you generate custom design system rules tailored to your project's specific needs. These rules guide AI coding agents to produce consistent, high-quality code when implementing Figma designs, ensuring that your team's conventions, component patterns, and architectural decisions are followed automatically.
|
||||
|
||||
### Supported Rule Files
|
||||
|
||||
| Agent | Rule File |
|
||||
|-------|-----------|
|
||||
| Claude Code | `CLAUDE.md` |
|
||||
| Codex CLI | `AGENTS.md` |
|
||||
| Cursor | `.cursor/rules/figma-design-system.mdc` |
|
||||
|
||||
## What Are Design System Rules?
|
||||
|
||||
Design system rules are project-level instructions that encode the "unwritten knowledge" of your codebase - the kind of expertise that experienced developers know and would pass on to new team members:
|
||||
|
||||
- Which layout primitives and components to use
|
||||
- Where component files should be located
|
||||
- How components should be named and structured
|
||||
- What should never be hardcoded
|
||||
- How to handle design tokens and styling
|
||||
- Project-specific architectural patterns
|
||||
|
||||
Once defined, these rules dramatically reduce repetitive prompting and ensure consistent output across all Figma implementation tasks.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Figma MCP server must be connected and accessible
|
||||
- Access to the project codebase for analysis
|
||||
- Understanding of your team's component conventions (or willingness to establish them)
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
|
||||
- Starting a new project that will use Figma designs
|
||||
- Onboarding an AI coding agent to an existing project with established patterns
|
||||
- Standardizing Figma-to-code workflows across your team
|
||||
- Updating or refining existing design system conventions
|
||||
- Users explicitly request: "create design system rules", "set up Figma guidelines", "customize rules for my project"
|
||||
|
||||
## Required Workflow
|
||||
|
||||
**Follow these steps in order. Do not skip steps.**
|
||||
|
||||
### Step 1: Run the Create Design System Rules Tool
|
||||
|
||||
Call the Figma MCP server's `create_design_system_rules` tool to get the foundational prompt and template.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `clientLanguages`: Comma-separated list of languages used in the project (e.g., "typescript,javascript", "python", "javascript")
|
||||
- `clientFrameworks`: Framework being used (e.g., "react", "vue", "svelte", "angular", "unknown")
|
||||
|
||||
This tool returns guidance and a template for creating design system rules.
|
||||
|
||||
Structure your design system rules following the template format provided in the tool's response.
|
||||
|
||||
### Step 2: Analyze the Codebase
|
||||
|
||||
Before finalizing rules, analyze the project to understand existing patterns:
|
||||
|
||||
**Component Organization:**
|
||||
|
||||
- Where are UI components located? (e.g., `src/components/`, `app/ui/`, `lib/components/`)
|
||||
- Is there a dedicated design system directory?
|
||||
- How are components organized? (by feature, by type, flat structure)
|
||||
|
||||
**Styling Approach:**
|
||||
|
||||
- What CSS framework or approach is used? (Tailwind, CSS Modules, styled-components, etc.)
|
||||
- Where are design tokens defined? (CSS variables, theme files, config files)
|
||||
- Are there existing color, typography, or spacing tokens?
|
||||
|
||||
**Component Patterns:**
|
||||
|
||||
- What naming conventions are used? (PascalCase, kebab-case, prefixes)
|
||||
- How are component props typically structured?
|
||||
- Are there common composition patterns?
|
||||
|
||||
**Architecture Decisions:**
|
||||
|
||||
- How is state management handled?
|
||||
- What routing system is used?
|
||||
- Are there specific import patterns or path aliases?
|
||||
|
||||
### Step 3: Generate Project-Specific Rules
|
||||
|
||||
Based on your codebase analysis, create a comprehensive set of rules. Include:
|
||||
|
||||
#### General Component Rules
|
||||
|
||||
```markdown
|
||||
- IMPORTANT: Always use components from `[YOUR_PATH]` when possible
|
||||
- Place new UI components in `[COMPONENT_DIRECTORY]`
|
||||
- Follow `[NAMING_CONVENTION]` for component names
|
||||
- Components must export as `[EXPORT_PATTERN]`
|
||||
```
|
||||
|
||||
#### Styling Rules
|
||||
|
||||
```markdown
|
||||
- Use `[CSS_FRAMEWORK/APPROACH]` for styling
|
||||
- Design tokens are defined in `[TOKEN_LOCATION]`
|
||||
- IMPORTANT: Never hardcode colors - always use tokens from `[TOKEN_FILE]`
|
||||
- Spacing values must use the `[SPACING_SYSTEM]` scale
|
||||
- Typography follows the scale defined in `[TYPOGRAPHY_LOCATION]`
|
||||
```
|
||||
|
||||
#### Figma MCP Integration Rules
|
||||
|
||||
```markdown
|
||||
## Figma MCP Integration Rules
|
||||
|
||||
These rules define how to translate Figma inputs into code for this project and must be followed for every Figma-driven change.
|
||||
|
||||
### Required Flow (do not skip)
|
||||
|
||||
1. Run get_design_context first to fetch the structured representation for the exact node(s)
|
||||
2. If the response is too large or truncated, run get_metadata to get the high-level node map, then re-fetch only the required node(s) with get_design_context
|
||||
3. Run get_screenshot for a visual reference of the node variant being implemented
|
||||
4. Only after you have both get_design_context and get_screenshot, download any assets needed and start implementation
|
||||
5. Translate the output (usually React + Tailwind) into this project's conventions, styles, and framework
|
||||
6. Validate against Figma for 1:1 look and behavior before marking complete
|
||||
|
||||
### Implementation Rules
|
||||
|
||||
- Treat the Figma MCP output (React + Tailwind) as a representation of design and behavior, not as final code style
|
||||
- Replace Tailwind utility classes with `[YOUR_STYLING_APPROACH]` when applicable
|
||||
- Reuse existing components from `[COMPONENT_PATH]` instead of duplicating functionality
|
||||
- Use the project's color system, typography scale, and spacing tokens consistently
|
||||
- Respect existing routing, state management, and data-fetch patterns
|
||||
- Strive for 1:1 visual parity with the Figma design
|
||||
- Validate the final UI against the Figma screenshot for both look and behavior
|
||||
```
|
||||
|
||||
#### Asset Handling Rules
|
||||
|
||||
```markdown
|
||||
## Asset Handling
|
||||
|
||||
- The Figma MCP server provides an assets endpoint which can serve image and SVG assets
|
||||
- IMPORTANT: If the Figma MCP server returns a localhost source for an image or SVG, use that source directly
|
||||
- IMPORTANT: DO NOT import/add new icon packages - all assets should be in the Figma payload
|
||||
- IMPORTANT: DO NOT use or create placeholders if a localhost source is provided
|
||||
- Store downloaded assets in `[ASSET_DIRECTORY]`
|
||||
```
|
||||
|
||||
#### Project-Specific Conventions
|
||||
|
||||
```markdown
|
||||
## Project-Specific Conventions
|
||||
|
||||
- [Add any unique architectural patterns]
|
||||
- [Add any special import requirements]
|
||||
- [Add any testing requirements]
|
||||
- [Add any accessibility standards]
|
||||
- [Add any performance considerations]
|
||||
```
|
||||
|
||||
### Step 4: Save Rules to the Appropriate Rule File
|
||||
|
||||
Detect which AI coding agent the user is working with and save the generated rules to the corresponding file:
|
||||
|
||||
| Agent | Rule File | Notes |
|
||||
|-------|-----------|-------|
|
||||
| Claude Code | `CLAUDE.md` in project root | Markdown format. Can also use `.claude/rules/figma-design-system.md` for modular organization. |
|
||||
| Codex CLI | `AGENTS.md` in project root | Markdown format. Append as a new section if file already exists. 32 KiB combined size limit. |
|
||||
| Cursor | `.cursor/rules/figma-design-system.mdc` | Markdown with YAML frontmatter (`description`, `globs`, `alwaysApply`). |
|
||||
|
||||
If unsure which agent the user is working with, check for existing rule files in the project or ask the user.
|
||||
|
||||
For Cursor, wrap the rules with YAML frontmatter:
|
||||
|
||||
```markdown
|
||||
---
|
||||
description: Rules for implementing Figma designs using the Figma MCP server. Covers component organization, styling conventions, design tokens, asset handling, and the required Figma-to-code workflow.
|
||||
globs: "src/components/**"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
[Generated rules here]
|
||||
```
|
||||
|
||||
Customize the `globs` pattern to match the directories where Figma-derived code will live in the project (e.g., `"src/**/*.tsx"` or `["src/components/**", "src/pages/**"]`).
|
||||
|
||||
After saving, the rules will be automatically loaded by the agent and applied to all Figma implementation tasks.
|
||||
|
||||
### Step 5: Validate and Iterate
|
||||
|
||||
After creating rules:
|
||||
|
||||
1. Test with a simple Figma component implementation
|
||||
2. Verify the agent follows the rules correctly
|
||||
3. Refine any rules that aren't working as expected
|
||||
4. Share with team members for feedback
|
||||
5. Update rules as the project evolves
|
||||
|
||||
## Rule Categories and Examples
|
||||
|
||||
### Essential Rules (Always Include)
|
||||
|
||||
**Component Discovery:**
|
||||
|
||||
```markdown
|
||||
- UI components are located in `src/components/ui/`
|
||||
- Feature components are in `src/components/features/`
|
||||
- Layout primitives are in `src/components/layout/`
|
||||
```
|
||||
|
||||
**Design Token Usage:**
|
||||
|
||||
```markdown
|
||||
- Colors are defined as CSS variables in `src/styles/tokens.css`
|
||||
- Never hardcode hex colors - use `var(--color-*)` tokens
|
||||
- Spacing uses the 4px base scale: `--space-1` (4px), `--space-2` (8px), etc.
|
||||
```
|
||||
|
||||
**Styling Approach:**
|
||||
|
||||
```markdown
|
||||
- Use Tailwind utility classes for styling
|
||||
- Custom styles go in component-level CSS modules
|
||||
- Theme customization is in `tailwind.config.js`
|
||||
```
|
||||
|
||||
### Recommended Rules (Highly Valuable)
|
||||
|
||||
**Component Patterns:**
|
||||
|
||||
```markdown
|
||||
- All components must accept a `className` prop for composition
|
||||
- Variant props should use union types: `variant: 'primary' | 'secondary'`
|
||||
- Icon components should accept `size` and `color` props
|
||||
```
|
||||
|
||||
**Import Conventions:**
|
||||
|
||||
```markdown
|
||||
- Use path aliases: `@/components`, `@/styles`, `@/utils`
|
||||
- Group imports: React, third-party, internal, types
|
||||
- No relative imports beyond parent directory
|
||||
```
|
||||
|
||||
**Code Quality:**
|
||||
|
||||
```markdown
|
||||
- Add JSDoc comments for exported components
|
||||
- Include PropTypes or TypeScript types for all props
|
||||
- Extract magic numbers to named constants
|
||||
```
|
||||
|
||||
### Optional Rules (Project-Specific)
|
||||
|
||||
**Accessibility:**
|
||||
|
||||
```markdown
|
||||
- All interactive elements must have aria-labels
|
||||
- Color contrast must meet WCAG AA standards
|
||||
- Keyboard navigation required for all interactions
|
||||
```
|
||||
|
||||
**Performance:**
|
||||
|
||||
```markdown
|
||||
- Lazy load images with the `Image` component from `@/components/Image`
|
||||
- Use React.memo for components that receive complex props
|
||||
- Icons should be SVG components, not icon fonts
|
||||
```
|
||||
|
||||
**Testing:**
|
||||
|
||||
```markdown
|
||||
- Include unit tests for new components in `__tests__/` directory
|
||||
- Use Testing Library queries (getByRole, getByLabelText)
|
||||
- Test all interactive states and variants
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: React + Tailwind Project
|
||||
|
||||
User says: "Create design system rules for my React project"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Run `create_design_system_rules(clientLanguages="typescript,javascript", clientFrameworks="react")`
|
||||
2. Analyze codebase structure
|
||||
3. Generate rules:
|
||||
|
||||
```markdown
|
||||
# Figma MCP Integration Rules
|
||||
|
||||
## Component Organization
|
||||
|
||||
- UI components are in `src/components/ui/`
|
||||
- Page components are in `src/app/`
|
||||
- Use Tailwind for styling
|
||||
|
||||
## Figma Implementation Flow
|
||||
|
||||
1. Run get_design_context for the node
|
||||
2. Run get_screenshot for visual reference
|
||||
3. Map Figma colors to Tailwind colors defined in `tailwind.config.js`
|
||||
4. Reuse components from `src/components/ui/` when possible
|
||||
5. Validate against screenshot before completing
|
||||
|
||||
## Styling Rules
|
||||
|
||||
- IMPORTANT: Use Tailwind utility classes, not inline styles
|
||||
- Colors are defined in `tailwind.config.js` theme.colors
|
||||
- Spacing uses Tailwind's default scale
|
||||
- Custom components go in `src/components/ui/`
|
||||
|
||||
## Asset Rules
|
||||
|
||||
- IMPORTANT: Use localhost sources from Figma MCP server directly
|
||||
- Store static assets in `public/assets/`
|
||||
- DO NOT install new icon libraries
|
||||
```
|
||||
|
||||
4. Save to the appropriate rule file for the user's agent
|
||||
5. Test with a simple button implementation
|
||||
|
||||
**Result:** The agent now consistently follows the project's conventions when implementing Figma designs.
|
||||
|
||||
### Example 2: Vue + Custom CSS Project
|
||||
|
||||
User says: "Set up Figma rules for my Vue app"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Run `create_design_system_rules(clientLanguages="typescript,javascript", clientFrameworks="vue")`
|
||||
2. Discover that project uses CSS modules and a custom design token system
|
||||
3. Generate rules:
|
||||
|
||||
```markdown
|
||||
# Figma MCP Integration Rules
|
||||
|
||||
## Component Organization
|
||||
|
||||
- Components are in `src/components/`
|
||||
- Composables are in `src/composables/`
|
||||
- Vue SFC structure: <script setup>, <template>, <style scoped>
|
||||
|
||||
## Design Tokens
|
||||
|
||||
- IMPORTANT: All colors defined in `src/styles/tokens.css` as CSS variables
|
||||
- Use `var(--color-primary)`, `var(--color-secondary)`, etc.
|
||||
- Spacing: `var(--space-xs)` through `var(--space-xl)`
|
||||
- Typography: `var(--text-sm)` through `var(--text-2xl)`
|
||||
|
||||
## Figma Implementation Flow
|
||||
|
||||
1. Run get_design_context and get_screenshot
|
||||
2. Translate React output to Vue 3 Composition API
|
||||
3. Map Figma colors to CSS variables in `src/styles/tokens.css`
|
||||
4. Use CSS Modules for component styles
|
||||
5. Check for existing components in `src/components/` before creating new ones
|
||||
|
||||
## Styling Rules
|
||||
|
||||
- Use CSS Modules (`.module.css` files)
|
||||
- IMPORTANT: Reference design tokens, never hardcode values
|
||||
- Scoped styles with CSS modules
|
||||
```
|
||||
|
||||
4. Save to the appropriate rule file for the user's agent
|
||||
5. Validate with a card component
|
||||
|
||||
**Result:** The agent now correctly translates Figma designs to Vue components using the project's design token system.
|
||||
|
||||
### Example 3: Design System Team
|
||||
|
||||
User says: "Generate rules for our design system library"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Run `create_design_system_rules(clientLanguages="typescript", clientFrameworks="react")`
|
||||
2. Analyze the monorepo structure with design system package
|
||||
3. Generate comprehensive rules:
|
||||
|
||||
```markdown
|
||||
# Design System Rules
|
||||
|
||||
## Repository Structure
|
||||
|
||||
- Design system components: `packages/design-system/src/components/`
|
||||
- Documentation: `packages/docs/`
|
||||
- Design tokens: `packages/tokens/src/`
|
||||
|
||||
## Component Development
|
||||
|
||||
- IMPORTANT: All components must be in `packages/design-system/src/components/`
|
||||
- Component file structure: `ComponentName/index.tsx`, `ComponentName.stories.tsx`, `ComponentName.test.tsx`
|
||||
- Export all components from `packages/design-system/src/index.ts`
|
||||
|
||||
## Design Tokens
|
||||
|
||||
- Colors: `packages/tokens/src/colors.ts`
|
||||
- Typography: `packages/tokens/src/typography.ts`
|
||||
- Spacing: `packages/tokens/src/spacing.ts`
|
||||
- IMPORTANT: Never hardcode values - import from tokens package
|
||||
|
||||
## Documentation Requirements
|
||||
|
||||
- Add Storybook story for every component
|
||||
- Include JSDoc with @example
|
||||
- Document all props with descriptions
|
||||
- Add accessibility notes
|
||||
|
||||
## Figma Integration
|
||||
|
||||
1. Get design context and screenshot from Figma
|
||||
2. Map Figma tokens to design system tokens
|
||||
3. Create or extend component in design system package
|
||||
4. Add Storybook stories showing all variants
|
||||
5. Validate against Figma screenshot
|
||||
6. Update documentation
|
||||
```
|
||||
|
||||
4. Save to the appropriate rule file and share with team
|
||||
5. Add to team documentation
|
||||
|
||||
**Result:** Entire team follows consistent patterns when adding components from Figma to the design system.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Start Simple, Iterate
|
||||
|
||||
Don't try to capture every rule upfront. Start with the most important conventions and add rules as you encounter inconsistencies.
|
||||
|
||||
### Be Specific
|
||||
|
||||
Instead of: "Use the design system"
|
||||
Write: "Always use Button components from `src/components/ui/Button.tsx` with variant prop ('primary' | 'secondary' | 'ghost')"
|
||||
|
||||
### Make Rules Actionable
|
||||
|
||||
Each rule should tell the agent exactly what to do, not just what to avoid.
|
||||
|
||||
Good: "Colors are defined in `src/theme/colors.ts` - import and use these constants"
|
||||
Bad: "Don't hardcode colors"
|
||||
|
||||
### Use IMPORTANT for Critical Rules
|
||||
|
||||
Prefix rules that must never be violated with "IMPORTANT:" to ensure the agent prioritizes them.
|
||||
|
||||
```markdown
|
||||
- IMPORTANT: Never expose API keys in client-side code
|
||||
- IMPORTANT: Always sanitize user input before rendering
|
||||
```
|
||||
|
||||
### Document the Why
|
||||
|
||||
When rules seem arbitrary, explain the reasoning:
|
||||
|
||||
```markdown
|
||||
- Place all data-fetching in server components (reduces client bundle size and improves performance)
|
||||
- Use absolute imports with `@/` alias (makes refactoring easier and prevents broken relative paths)
|
||||
```
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: The agent isn't following the rules
|
||||
|
||||
**Cause:** Rules may be too vague or not properly loaded by the agent.
|
||||
**Solution:**
|
||||
|
||||
- Make rules more specific and actionable
|
||||
- Verify rules are saved in the correct configuration file
|
||||
- Restart your agent or IDE to reload rules
|
||||
- Add "IMPORTANT:" prefix to critical rules
|
||||
|
||||
### Issue: Rules conflict with each other
|
||||
|
||||
**Cause:** Contradictory or overlapping rules.
|
||||
**Solution:**
|
||||
|
||||
- Review all rules for conflicts
|
||||
- Establish a clear priority hierarchy
|
||||
- Remove redundant rules
|
||||
- Consolidate related rules into single, clear statements
|
||||
|
||||
### Issue: Too many rules increase latency
|
||||
|
||||
**Cause:** Excessive rules increase context size and processing time.
|
||||
**Solution:**
|
||||
|
||||
- Focus on the 20% of rules that solve 80% of consistency issues
|
||||
- Remove overly specific rules that rarely apply
|
||||
- Combine related rules
|
||||
- Use progressive disclosure (basic rules first, advanced rules in linked files)
|
||||
|
||||
### Issue: Rules become outdated as project evolves
|
||||
|
||||
**Cause:** Codebase changes but rules don't.
|
||||
**Solution:**
|
||||
|
||||
- Schedule periodic rule reviews (monthly or quarterly)
|
||||
- Update rules when architectural decisions change
|
||||
- Version control your rule files
|
||||
- Document rule changes in commit messages
|
||||
|
||||
## Understanding Design System Rules
|
||||
|
||||
Design system rules transform how AI coding agents work with your Figma designs:
|
||||
|
||||
**Before rules:**
|
||||
|
||||
- The agent makes assumptions about component structure
|
||||
- Inconsistent styling approaches across implementations
|
||||
- Hardcoded values that don't match design tokens
|
||||
- Components created in random locations
|
||||
- Repetitive explanations of project conventions
|
||||
|
||||
**After rules:**
|
||||
|
||||
- The agent automatically follows your conventions
|
||||
- Consistent component structure and styling
|
||||
- Proper use of design tokens from the start
|
||||
- Components organized correctly
|
||||
- Zero repetitive prompting
|
||||
|
||||
The time invested in creating good rules pays off exponentially across every Figma implementation task.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/)
|
||||
- [Figma Variables and Design Tokens](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma)
|
||||
@@ -0,0 +1,69 @@
|
||||
---
|
||||
name: figma-create-new-file
|
||||
description: Create a new blank Figma file. Use when the user wants to create a new Figma design or FigJam file, or when you need a new file before calling use_figma. Handles plan resolution via whoami if needed. Usage — /figma-create-new-file [editorType] [fileName] (e.g. /figma-create-new-file figjam My Whiteboard)
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# create_new_file — Create a New Figma File
|
||||
|
||||
Use the `create_new_file` MCP tool to create a new blank Figma file in the user's drafts folder. This is typically used before `use_figma` when you need a fresh file to work with.
|
||||
|
||||
## Skill Arguments
|
||||
|
||||
This skill accepts optional arguments: `/figma-create-new-file [editorType] [fileName]`
|
||||
|
||||
- **editorType**: `design` (default) or `figjam`
|
||||
- **fileName**: Name for the new file (defaults to "Untitled")
|
||||
|
||||
Examples:
|
||||
- `/figma-create-new-file` — creates a design file named "Untitled"
|
||||
- `/figma-create-new-file figjam My Whiteboard` — creates a FigJam file named "My Whiteboard"
|
||||
- `/figma-create-new-file design My New Design` — creates a design file named "My New Design"
|
||||
|
||||
Parse the arguments from the skill invocation. If editorType is not provided, default to `"design"`. If fileName is not provided, default to `"Untitled"`.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1: Resolve the planKey
|
||||
|
||||
The `create_new_file` tool requires a `planKey` parameter. Follow this decision tree:
|
||||
|
||||
1. **User already provided a planKey** (e.g. from a previous `whoami` call or in their prompt) → use it directly, skip to Step 2.
|
||||
|
||||
2. **No planKey available** → call the `whoami` tool. The response contains a `plans` array. Each plan has a `key`, `name`, `seat`, and `tier`.
|
||||
|
||||
- **Single plan**: use its `key` field automatically.
|
||||
- **Multiple plans**: ask the user which team or organization they want to create the file in, then use the corresponding plan's `key`.
|
||||
|
||||
### Step 2: Call create_new_file
|
||||
|
||||
Call the `create_new_file` tool with:
|
||||
|
||||
| Parameter | Required | Description |
|
||||
|-------------|----------|-------------|
|
||||
| `planKey` | Yes | The plan key from Step 1 |
|
||||
| `fileName` | Yes | Name for the new file |
|
||||
| `editorType`| Yes | `"design"` or `"figjam"` |
|
||||
|
||||
Example:
|
||||
```json
|
||||
{
|
||||
"planKey": "team:123456",
|
||||
"fileName": "My New Design",
|
||||
"editorType": "design"
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3: Use the result
|
||||
|
||||
The tool returns:
|
||||
- `file_key` — the key of the newly created file
|
||||
- `file_url` — a direct URL to open the file in Figma
|
||||
|
||||
Use the `file_key` for subsequent tool calls like `use_figma`.
|
||||
|
||||
## Important Notes
|
||||
|
||||
- The file is created in the user's **drafts folder** for the selected plan.
|
||||
- Only `"design"` and `"figjam"` editor types are supported.
|
||||
- If `use_figma` is your next step, load the `figma-use` skill before calling it.
|
||||
@@ -0,0 +1,341 @@
|
||||
---
|
||||
name: figma-generate-design
|
||||
description: "Use this skill alongside figma-use when the task involves translating an application page, view, or multi-section layout into Figma. Triggers: 'write to Figma', 'create in Figma from code', 'push page to Figma', 'take this app/page and build it in Figma', 'create a screen', 'build a landing page in Figma', 'update the Figma screen to match code'. This is the preferred workflow skill whenever the user wants to build or update a full page, screen, or view in Figma from code or a description. Discovers design system components, variables, and styles via search_design_system, imports them, and assembles screens incrementally section-by-section using design system tokens instead of hardcoded values."
|
||||
---
|
||||
|
||||
# Build / Update Screens from Design System
|
||||
|
||||
Use this skill to create or update full-page screens in Figma by **reusing the published design system** — components, variables, and styles — rather than drawing primitives with hardcoded values. The key insight: the Figma file likely has a published design system with components, color/spacing variables, and text/effect styles that correspond to the codebase's UI components and tokens. Find and use those instead of drawing boxes with hex colors.
|
||||
|
||||
**MANDATORY**: You MUST also load [figma-use](../figma-use/SKILL.md) before any `use_figma` call. That skill contains critical rules (color ranges, font loading, etc.) that apply to every script you write.
|
||||
|
||||
**Always pass `skillNames: "figma-generate-design"` when calling `use_figma` as part of this skill.** This is a logging parameter — it does not affect execution.
|
||||
|
||||
## Skill Boundaries
|
||||
|
||||
- Use this skill when the deliverable is a **Figma screen** (new or updated) composed of design system component instances.
|
||||
- If the user wants to generate **code from a Figma design**, switch to [figma-implement-design](../figma-implement-design/SKILL.md).
|
||||
- If the user wants to create **new reusable components or variants**, use [figma-use](../figma-use/SKILL.md) directly.
|
||||
- If the user wants to write **Code Connect mappings**, switch to [figma-code-connect-components](../figma-code-connect-components/SKILL.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Figma MCP server must be connected
|
||||
- The target Figma file must have a published design system with components (or access to a team library)
|
||||
- User should provide either:
|
||||
- A Figma file URL / file key to work in
|
||||
- Or context about which file to target (the agent can discover pages)
|
||||
- Source code or description of the screen to build/update
|
||||
|
||||
## Parallel Workflow with generate_figma_design (Web Apps Only)
|
||||
|
||||
When building a screen from a **web app** that can be rendered in a browser, the best results come from running both approaches in parallel:
|
||||
|
||||
1. **In parallel:**
|
||||
- Start building the screen using this skill's workflow (use_figma + design system components)
|
||||
- Run `generate_figma_design` to capture a pixel-perfect screenshot of the running web app
|
||||
2. **Once both complete:** Update the use_figma output to match the pixel-perfect layout from the `generate_figma_design` capture. The capture provides the exact spacing, sizing, and visual treatment to aim for, while your use_figma output has proper component instances linked to the design system.
|
||||
3. **Once confirmed looking good:** Delete the `generate_figma_design` output — it was only used as a visual reference.
|
||||
|
||||
This combines the best of both: `generate_figma_design` gives pixel-perfect layout accuracy, while use_figma gives proper design system component instances that stay linked and updatable.
|
||||
|
||||
**This workflow only applies to web apps** where `generate_figma_design` can capture the running page. For non-web apps (iOS, Android, etc.) or when updating existing screens, use the standard workflow below.
|
||||
|
||||
## Required Workflow
|
||||
|
||||
**Follow these steps in order. Do not skip steps.**
|
||||
|
||||
### Step 1: Understand the Screen
|
||||
|
||||
Before touching Figma, understand what you're building:
|
||||
|
||||
1. If building from code, read the relevant source files to understand the page structure, sections, and which components are used.
|
||||
2. Identify the major sections of the screen (e.g., Header, Hero, Content Panels, Pricing Grid, FAQ Accordion, Footer).
|
||||
3. For each section, list the UI components involved (buttons, inputs, cards, navigation pills, accordions, etc.).
|
||||
|
||||
### Step 2: Discover Design System — Components, Variables, and Styles
|
||||
|
||||
You need three things from the design system: **components** (buttons, cards, etc.), **variables** (colors, spacing, radii), and **styles** (text styles, effect styles like shadows). Don't hardcode hex colors or pixel values when design system tokens exist.
|
||||
|
||||
#### 2a: Discover components
|
||||
|
||||
**Preferred: inspect existing screens first.** If the target file already contains screens using the same design system, skip `search_design_system` and inspect existing instances directly. A single `use_figma` call that walks an existing frame's instances gives you an exact, authoritative component map:
|
||||
|
||||
```js
|
||||
const frame = figma.currentPage.findOne(n => n.name === "Existing Screen");
|
||||
const uniqueSets = new Map();
|
||||
frame.findAll(n => n.type === "INSTANCE").forEach(inst => {
|
||||
const mc = inst.mainComponent;
|
||||
const cs = mc?.parent?.type === "COMPONENT_SET" ? mc.parent : null;
|
||||
const key = cs ? cs.key : mc?.key;
|
||||
const name = cs ? cs.name : mc?.name;
|
||||
if (key && !uniqueSets.has(key)) {
|
||||
uniqueSets.set(key, { name, key, isSet: !!cs, sampleVariant: mc.name });
|
||||
}
|
||||
});
|
||||
return [...uniqueSets.values()];
|
||||
```
|
||||
|
||||
Only fall back to `search_design_system` when the file has no existing screens to reference. When using it, **search broadly** — try multiple terms and synonyms (e.g., "button", "input", "nav", "card", "accordion", "header", "footer", "tag", "avatar", "toggle", "icon", etc.). Use `includeComponents: true` to focus on components.
|
||||
|
||||
**Include component properties** in your map — you need to know which TEXT properties each component exposes for text overrides. Create a temporary instance, read its `componentProperties` (and those of nested instances), then remove the temp instance.
|
||||
|
||||
Example component map with property info:
|
||||
|
||||
```
|
||||
Component Map:
|
||||
- Button → key: "abc123", type: COMPONENT_SET
|
||||
Properties: { "Label#2:0": TEXT, "Has Icon#4:64": BOOLEAN }
|
||||
- PricingCard → key: "ghi789", type: COMPONENT_SET
|
||||
Properties: { "Device": VARIANT, "Variant": VARIANT }
|
||||
Nested "Text Heading" has: { "Text#2104:5": TEXT }
|
||||
Nested "Button" has: { "Label#2:0": TEXT }
|
||||
```
|
||||
|
||||
#### 2b: Discover variables (colors, spacing, radii)
|
||||
|
||||
**Inspect existing screens first** (same as components). Or use `search_design_system` with `includeVariables: true`.
|
||||
|
||||
> **WARNING: Two different variable discovery methods — do not confuse them.**
|
||||
>
|
||||
> - `use_figma` with `figma.variables.getLocalVariableCollectionsAsync()` — returns **only local variables defined in the current file**. If this returns empty, it does **not** mean no variables exist. Remote/published library variables are invisible to this API.
|
||||
> - `search_design_system` with `includeVariables: true` — searches across **all linked libraries**, including remote and published ones. This is the correct tool for discovering design system variables.
|
||||
>
|
||||
> **Never conclude "no variables exist" based solely on `getLocalVariableCollectionsAsync()` returning empty.** Always also run `search_design_system` with `includeVariables: true` to check for library variables before deciding to create your own.
|
||||
|
||||
**Query strategy:** `search_design_system` matches against **variable names** (e.g., "Gray/gray-9", "core/gray/100", "space/400"), not categories. Run multiple short, simple queries in parallel rather than one compound query:
|
||||
|
||||
- **Primitive colors:** "gray", "red", "blue", "green", "white", "brand"
|
||||
- **Semantic colors:** "background", "foreground", "border", "surface", "text"
|
||||
- **Spacing/sizing:** "space", "radius", "gap", "padding"
|
||||
|
||||
If initial searches return empty, try shorter fragments or different naming conventions — libraries vary widely ("grey" vs "gray", "spacing" vs "space", "color/bg" vs "background").
|
||||
|
||||
Inspect an existing screen's bound variables for the most authoritative results:
|
||||
|
||||
```js
|
||||
const frame = figma.currentPage.findOne(n => n.name === "Existing Screen");
|
||||
const varMap = new Map();
|
||||
frame.findAll(() => true).forEach(node => {
|
||||
const bv = node.boundVariables;
|
||||
if (!bv) return;
|
||||
for (const [prop, binding] of Object.entries(bv)) {
|
||||
const bindings = Array.isArray(binding) ? binding : [binding];
|
||||
for (const b of bindings) {
|
||||
if (b?.id && !varMap.has(b.id)) {
|
||||
const v = await figma.variables.getVariableByIdAsync(b.id);
|
||||
if (v) varMap.set(b.id, { name: v.name, id: v.id, key: v.key, type: v.resolvedType, remote: v.remote });
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return [...varMap.values()];
|
||||
```
|
||||
|
||||
For library variables (remote = true), import them by key with `figma.variables.importVariableByKeyAsync(key)`. For local variables, use `figma.variables.getVariableByIdAsync(id)` directly.
|
||||
|
||||
See [variable-patterns.md](../figma-use/references/variable-patterns.md) for binding patterns.
|
||||
|
||||
#### 2c: Discover styles (text styles, effect styles)
|
||||
|
||||
Search for styles using `search_design_system` with `includeStyles: true` and terms like "heading", "body", "shadow", "elevation". Or inspect what an existing screen uses:
|
||||
|
||||
```js
|
||||
const frame = figma.currentPage.findOne(n => n.name === "Existing Screen");
|
||||
const styles = { text: new Map(), effect: new Map() };
|
||||
frame.findAll(() => true).forEach(node => {
|
||||
if ('textStyleId' in node && node.textStyleId) {
|
||||
const s = figma.getStyleById(node.textStyleId);
|
||||
if (s) styles.text.set(s.id, { name: s.name, id: s.id, key: s.key });
|
||||
}
|
||||
if ('effectStyleId' in node && node.effectStyleId) {
|
||||
const s = figma.getStyleById(node.effectStyleId);
|
||||
if (s) styles.effect.set(s.id, { name: s.name, id: s.id, key: s.key });
|
||||
}
|
||||
});
|
||||
return {
|
||||
textStyles: [...styles.text.values()],
|
||||
effectStyles: [...styles.effect.values()]
|
||||
};
|
||||
```
|
||||
|
||||
Import library styles with `figma.importStyleByKeyAsync(key)`, then apply with `node.textStyleId = style.id` or `node.effectStyleId = style.id`.
|
||||
|
||||
See [text-style-patterns.md](../figma-use/references/text-style-patterns.md) and [effect-style-patterns.md](../figma-use/references/effect-style-patterns.md) for details.
|
||||
|
||||
### Step 3: Create the Page Wrapper Frame First
|
||||
|
||||
**Do NOT build sections as top-level page children and reparent them later** — moving nodes across `use_figma` calls with `appendChild()` silently fails and produces orphaned frames. Instead, create the wrapper first, then build each section directly inside it.
|
||||
|
||||
Create the page wrapper in its own `use_figma` call. Position it away from existing content and return its ID:
|
||||
|
||||
```js
|
||||
// Find clear space
|
||||
let maxX = 0;
|
||||
for (const child of figma.currentPage.children) {
|
||||
maxX = Math.max(maxX, child.x + child.width);
|
||||
}
|
||||
|
||||
const wrapper = figma.createFrame();
|
||||
wrapper.name = "Homepage";
|
||||
wrapper.layoutMode = "VERTICAL";
|
||||
wrapper.primaryAxisAlignItems = "CENTER";
|
||||
wrapper.counterAxisAlignItems = "CENTER";
|
||||
wrapper.resize(1440, 100);
|
||||
wrapper.layoutSizingHorizontal = "FIXED";
|
||||
wrapper.layoutSizingVertical = "HUG";
|
||||
wrapper.x = maxX + 200;
|
||||
wrapper.y = 0;
|
||||
|
||||
return { success: true, wrapperId: wrapper.id };
|
||||
```
|
||||
|
||||
### Step 4: Build Each Section Inside the Wrapper
|
||||
|
||||
**This is the most important step.** Build one section at a time, each in its own `use_figma` call. At the start of each script, fetch the wrapper by ID and append new content directly to it.
|
||||
|
||||
```js
|
||||
const createdNodeIds = [];
|
||||
const wrapper = await figma.getNodeByIdAsync("WRAPPER_ID_FROM_STEP_3");
|
||||
|
||||
// Import design system components by key
|
||||
const buttonSet = await figma.importComponentSetByKeyAsync("BUTTON_SET_KEY");
|
||||
const primaryButton = buttonSet.children.find(c =>
|
||||
c.type === "COMPONENT" && c.name.includes("variant=primary")
|
||||
) || buttonSet.defaultVariant;
|
||||
|
||||
// Import design system variables for colors and spacing
|
||||
const bgColorVar = await figma.variables.importVariableByKeyAsync("BG_COLOR_VAR_KEY");
|
||||
const spacingVar = await figma.variables.importVariableByKeyAsync("SPACING_VAR_KEY");
|
||||
|
||||
// Build section frame with variable bindings (not hardcoded values)
|
||||
const section = figma.createFrame();
|
||||
section.name = "Header";
|
||||
section.layoutMode = "HORIZONTAL";
|
||||
section.setBoundVariable("paddingLeft", spacingVar);
|
||||
section.setBoundVariable("paddingRight", spacingVar);
|
||||
const bgPaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgColorVar
|
||||
);
|
||||
section.fills = [bgPaint];
|
||||
|
||||
// Import and apply text/effect styles
|
||||
const shadowStyle = await figma.importStyleByKeyAsync("SHADOW_STYLE_KEY");
|
||||
section.effectStyleId = shadowStyle.id;
|
||||
|
||||
// Create component instances inside the section
|
||||
const btnInstance = primaryButton.createInstance();
|
||||
section.appendChild(btnInstance);
|
||||
createdNodeIds.push(btnInstance.id);
|
||||
|
||||
// Append section to wrapper
|
||||
wrapper.appendChild(section);
|
||||
section.layoutSizingHorizontal = "FILL"; // AFTER appending
|
||||
|
||||
createdNodeIds.push(section.id);
|
||||
return { success: true, createdNodeIds };
|
||||
```
|
||||
|
||||
After each section, validate with `get_screenshot` before moving on. Look closely for cropped/clipped text (line heights cutting off content) and overlapping elements — these are the most common issues and easy to miss at a glance.
|
||||
|
||||
#### Override instance text with setProperties()
|
||||
|
||||
Component instances ship with placeholder text ("Title", "Heading", "Button"). Use the component property keys you discovered in Step 2 to override them with `setProperties()` — this is more reliable than direct `node.characters` manipulation. See [component-patterns.md](../figma-use/references/component-patterns.md#overriding-text-in-a-component-instance) for the full pattern.
|
||||
|
||||
For nested instances that expose their own TEXT properties, call `setProperties()` on the nested instance:
|
||||
|
||||
```js
|
||||
const nestedHeading = cardInstance.findOne(n => n.type === "INSTANCE" && n.name === "Text Heading");
|
||||
if (nestedHeading) {
|
||||
nestedHeading.setProperties({ "Text#2104:5": "Actual heading from source code" });
|
||||
}
|
||||
```
|
||||
|
||||
Only fall back to direct `node.characters` for text that is NOT managed by any component property.
|
||||
|
||||
#### Read source code defaults carefully
|
||||
|
||||
When translating code components to Figma instances, check the component's default prop values in the source code, not just what's explicitly passed. For example, `<Button size="small">Register</Button>` with no variant prop — check the component definition to find `variant = "primary"` as the default. Selecting the wrong variant (e.g., Neutral instead of Primary) produces a visually incorrect result that's easy to miss.
|
||||
|
||||
#### What to build manually vs. import from design system
|
||||
|
||||
| Build manually | Import from design system |
|
||||
|----------------|--------------------------|
|
||||
| Page wrapper frame | **Components**: buttons, cards, inputs, nav, etc. |
|
||||
| Section container frames | **Variables**: colors (fills, strokes), spacing (padding, gap), radii |
|
||||
| Layout grids (rows, columns) | **Text styles**: heading, body, caption, etc. |
|
||||
| | **Effect styles**: shadows, blurs, etc. |
|
||||
|
||||
**Never hardcode hex colors or pixel spacing** when a design system variable exists. Use `setBoundVariable` for spacing/radii and `setBoundVariableForPaint` for colors. Apply text styles with `node.textStyleId` and effect styles with `node.effectStyleId`.
|
||||
|
||||
### Step 5: Validate the Full Screen
|
||||
|
||||
After composing all sections, call `get_screenshot` on the full page frame and compare against the source. Fix any issues with targeted `use_figma` calls — don't rebuild the entire screen.
|
||||
|
||||
**Screenshot individual sections, not just the full page.** A full-page screenshot at reduced resolution hides text truncation, wrong colors, and placeholder text that hasn't been overridden. Take a screenshot of each section by node ID to catch:
|
||||
- **Cropped/clipped text** — line heights or frame sizing cutting off descenders, ascenders, or entire lines
|
||||
- **Overlapping content** — elements stacking on top of each other due to incorrect sizing or missing auto-layout
|
||||
- Placeholder text still showing ("Title", "Heading", "Button")
|
||||
- Truncated content from layout sizing bugs
|
||||
- Wrong component variants (e.g., Neutral vs Primary button)
|
||||
|
||||
### Step 6: Updating an Existing Screen
|
||||
|
||||
When updating rather than creating from scratch:
|
||||
|
||||
1. Use `get_metadata` to inspect the existing screen structure.
|
||||
2. Identify which sections need updating and which can stay.
|
||||
3. For each section that needs changes:
|
||||
- Locate the existing nodes by ID or name
|
||||
- Swap component instances if the design system component changed
|
||||
- Update text content, variant properties, or layout as needed
|
||||
- Remove deprecated sections
|
||||
- Add new sections
|
||||
4. Validate with `get_screenshot` after each modification.
|
||||
|
||||
```js
|
||||
// Example: Swap a button variant in an existing screen
|
||||
const existingButton = await figma.getNodeByIdAsync("EXISTING_BUTTON_INSTANCE_ID");
|
||||
if (existingButton && existingButton.type === "INSTANCE") {
|
||||
// Import the updated component
|
||||
const buttonSet = await figma.importComponentSetByKeyAsync("BUTTON_SET_KEY");
|
||||
const newVariant = buttonSet.children.find(c =>
|
||||
c.name.includes("variant=primary") && c.name.includes("size=lg")
|
||||
) || buttonSet.defaultVariant;
|
||||
existingButton.swapComponent(newVariant);
|
||||
}
|
||||
return { success: true, mutatedNodeIds: [existingButton.id] };
|
||||
```
|
||||
|
||||
## Reference Docs
|
||||
|
||||
For detailed API patterns and gotchas, load these from the [figma-use](../figma-use/SKILL.md) references as needed:
|
||||
|
||||
- [component-patterns.md](../figma-use/references/component-patterns.md) — importing by key, finding variants, setProperties, text overrides, working with instances
|
||||
- [variable-patterns.md](../figma-use/references/variable-patterns.md) — creating/binding variables, importing library variables, scopes, aliasing, discovering existing variables
|
||||
- [text-style-patterns.md](../figma-use/references/text-style-patterns.md) — creating/applying text styles, importing library text styles, type ramps
|
||||
- [effect-style-patterns.md](../figma-use/references/effect-style-patterns.md) — creating/applying effect styles (shadows), importing library effect styles
|
||||
- [gotchas.md](../figma-use/references/gotchas.md) — layout pitfalls (HUG/FILL interactions, counterAxisAlignItems, sizing order), paint/color issues, page context resets
|
||||
|
||||
## Error Recovery
|
||||
|
||||
Follow the error recovery process from [figma-use](../figma-use/SKILL.md#6-error-recovery--self-correction):
|
||||
|
||||
1. **STOP** on error — do not retry immediately.
|
||||
2. **Read the error message carefully** to understand what went wrong.
|
||||
3. If the error is unclear, call `get_metadata` or `get_screenshot` to inspect the current file state.
|
||||
4. **Fix the script** based on the error message.
|
||||
5. **Retry** the corrected script — this is safe because failed scripts are atomic (nothing is created if a script errors).
|
||||
|
||||
Because this skill works incrementally (one section per call), errors are naturally scoped to a single section. Previous sections from successful calls remain intact.
|
||||
|
||||
## Best Practices
|
||||
|
||||
- **Always search before building.** The design system likely has the component, variable, or style you need. Manual construction and hardcoded values should be the exception, not the rule.
|
||||
- **Search broadly.** Try synonyms and partial terms. A "NavigationPill" might be found under "pill", "nav", "tab", or "chip". For variables, search "color", "spacing", "radius", etc.
|
||||
- **Prefer design system tokens over hardcoded values.** Use variable bindings for colors, spacing, and radii. Use text styles for typography. Use effect styles for shadows. This keeps the screen linked to the design system.
|
||||
- **Prefer component instances over manual builds.** Instances stay linked to the source component and update automatically when the design system evolves.
|
||||
- **Work section by section.** Never build more than one major section per `use_figma` call.
|
||||
- **Return node IDs from every call.** You'll need them to compose sections and for error recovery.
|
||||
- **Validate visually after each section.** Use `get_screenshot` to catch issues early.
|
||||
- **Match existing conventions.** If the file already has screens, match their naming, sizing, and layout patterns.
|
||||
@@ -0,0 +1,315 @@
|
||||
---
|
||||
name: figma-generate-library
|
||||
description: "Build or update a professional-grade design system in Figma from a codebase. Use when the user wants to create variables/tokens, build component libraries, set up theming (light/dark modes), document foundations, or reconcile gaps between code and Figma. This skill teaches WHAT to build and in WHAT ORDER — it complements the `figma-use` skill which teaches HOW to call the Plugin API. Both skills should be loaded together."
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Design System Builder — Figma MCP Skill
|
||||
|
||||
Build professional-grade design systems in Figma that match code. This skill orchestrates multi-phase workflows across 20–100+ `use_figma` calls, enforcing quality patterns from real-world design systems (Material 3, Polaris, Figma UI3, Simple DS).
|
||||
|
||||
**Prerequisites**: The `figma-use` skill MUST also be loaded for every `use_figma` call. It provides Plugin API syntax rules (return pattern, page reset, ID return, font loading, color range). This skill provides design system domain knowledge and workflow orchestration.
|
||||
|
||||
**Always pass `skillNames: "figma-generate-library"` when calling `use_figma` as part of this skill.** This is a logging parameter — it does not affect execution.
|
||||
|
||||
---
|
||||
|
||||
## 1. The One Rule That Matters Most
|
||||
|
||||
**This is NEVER a one-shot task.** Building a design system requires 20–100+ `use_figma` calls across multiple phases, with mandatory user checkpoints between them. Any attempt to create everything in one call WILL produce broken, incomplete, or unrecoverable results. Break every operation to the smallest useful unit, validate, get feedback, proceed.
|
||||
|
||||
---
|
||||
|
||||
## 2. Mandatory Workflow
|
||||
|
||||
Every design system build follows this phase order. Skipping or reordering phases causes structural failures that are expensive to undo.
|
||||
|
||||
```
|
||||
Phase 0: DISCOVERY (always first — no use_figma writes yet)
|
||||
0a. Analyze codebase → extract tokens, components, naming conventions
|
||||
0b. Inspect Figma file → pages, variables, components, styles, existing conventions
|
||||
0c. Search subscribed libraries → use search_design_system for reusable assets
|
||||
0d. Lock v1 scope → agree on exact token set + component list before any creation
|
||||
0e. Map code → Figma → resolve conflicts (code and Figma disagree = ask user)
|
||||
✋ USER CHECKPOINT: present full plan, await explicit approval
|
||||
|
||||
Phase 1: FOUNDATIONS (tokens first — always before components)
|
||||
1a. Create variable collections and modes
|
||||
1b. Create primitive variables (raw values, 1 mode)
|
||||
1c. Create semantic variables (aliased to primitives, mode-aware)
|
||||
1d. Set scopes on ALL variables
|
||||
1e. Set code syntax on ALL variables
|
||||
1f. Create effect styles (shadows) and text styles (typography)
|
||||
→ Exit criteria: every token from the agreed plan exists, all scopes set, all code syntax set
|
||||
✋ USER CHECKPOINT: show variable summary, await approval
|
||||
|
||||
Phase 2: FILE STRUCTURE (before components)
|
||||
2a. Create page skeleton: Cover → Getting Started → Foundations → --- → Components → --- → Utilities
|
||||
2b. Create foundations documentation pages (color swatches, type specimens, spacing bars)
|
||||
→ Exit criteria: all planned pages exist, foundations docs are navigable
|
||||
✋ USER CHECKPOINT: show page list + screenshot, await approval
|
||||
|
||||
Phase 3: COMPONENTS (one at a time — never batch)
|
||||
For EACH component (in dependency order: atoms before molecules):
|
||||
3a. Create dedicated page
|
||||
3b. Build base component with auto-layout + full variable bindings
|
||||
3c. Create all variant combinations (combineAsVariants + grid layout)
|
||||
3d. Add component properties (TEXT, BOOLEAN, INSTANCE_SWAP)
|
||||
3e. Link properties to child nodes
|
||||
3f. Add page documentation (title, description, usage notes)
|
||||
3g. Validate: get_metadata (structure) + get_screenshot (visual)
|
||||
3h. Optional: lightweight Code Connect mapping while context is fresh
|
||||
→ Exit criteria: variant count correct, all bindings verified, screenshot looks right
|
||||
✋ USER CHECKPOINT per component: show screenshot, await approval before next component
|
||||
|
||||
Phase 4: INTEGRATION + QA (final pass)
|
||||
4a. Finalize all Code Connect mappings
|
||||
4b. Accessibility audit (contrast, min touch targets, focus visibility)
|
||||
4c. Naming audit (no duplicates, no unnamed nodes, consistent casing)
|
||||
4d. Unresolved bindings audit (no hardcoded fills/strokes remaining)
|
||||
4e. Final review screenshots of every page
|
||||
✋ USER CHECKPOINT: complete sign-off
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Critical Rules
|
||||
|
||||
**Plugin API basics** (from use_figma skill — enforced here too):
|
||||
- Use `return` to send data back (auto-serialized). Do NOT wrap in IIFE or call closePlugin.
|
||||
- Return ALL created/mutated node IDs in every return value
|
||||
- Page context resets each call — always `await figma.setCurrentPageAsync(page)` at start
|
||||
- `figma.notify()` throws — never use it
|
||||
- Colors are 0–1 range, not 0–255
|
||||
- Font MUST be loaded before any text write: `await figma.loadFontAsync({family, style})`
|
||||
|
||||
**Design system rules**:
|
||||
1. **Variables BEFORE components** — components bind to variables. No token = no component.
|
||||
2. **Inspect before creating** — run read-only `use_figma` to discover existing conventions. Match them.
|
||||
3. **One page per component** *(default)* — exception: tightly related families (e.g., Input + helpers) may share a page with clear section separation.
|
||||
4. **Bind visual properties to variables** *(default)* — fills, strokes, padding, radius, gap. Exceptions: intentionally fixed geometry (icon pixel-grid sizes, static dividers).
|
||||
5. **Scopes on every variable** — NEVER leave as `ALL_SCOPES`. Background: `FRAME_FILL, SHAPE_FILL`. Text: `TEXT_FILL`. Border: `STROKE_COLOR`. Spacing: `GAP`. Radii: `CORNER_RADIUS`. Primitives: `[]` (hidden).
|
||||
6. **Code syntax on every variable** — WEB syntax MUST use the `var()` wrapper: `var(--color-bg-primary)`, not `--color-bg-primary`. Use the actual CSS variable name from the codebase. ANDROID/iOS do NOT use a wrapper.
|
||||
7. **Alias semantics to primitives** — `{ type: 'VARIABLE_ALIAS', id: primitiveVar.id }`. Never duplicate raw values in semantic layer.
|
||||
8. **Position variants after combineAsVariants** — they stack at (0,0). Manually grid-layout + resize.
|
||||
9. **INSTANCE_SWAP for icons** — never create a variant per icon. Cap variant matrices: if Size × Style × State > 30 combinations, split into sub-component.
|
||||
10. **Deterministic naming** — use consistent, unique node names for idempotent cleanup and resumability. Track created node IDs via return values and the state ledger.
|
||||
11. **No destructive cleanup** — cleanup scripts identify nodes by name convention or returned IDs, not by guessing.
|
||||
12. **Validate before proceeding** — never build on unvalidated work. `get_metadata` after every create, `get_screenshot` after each component.
|
||||
13. **NEVER parallelize `use_figma` calls** — Figma state mutations must be strictly sequential. Even if your tool supports parallel calls, never run two use_figma calls simultaneously.
|
||||
14. **Never hallucinate Node IDs** — always read IDs from the state ledger returned by previous calls. Never reconstruct or guess an ID from memory.
|
||||
15. **Use the helper scripts** — embed scripts from `scripts/` into your use_figma calls. Don't write 200-line inline scripts from scratch.
|
||||
16. **Explicit phase approval** — at each checkpoint, name the next phase explicitly. "looks good" is not approval to proceed to Phase 3 if you asked about Phase 1.
|
||||
|
||||
---
|
||||
|
||||
## 4. State Management (Required for Long Workflows)
|
||||
|
||||
> **`getPluginData()` / `setPluginData()` are NOT supported in `use_figma`.** Use `getSharedPluginData()` / `setSharedPluginData()` instead (these ARE supported), or use name-based lookups and the state ledger (returned IDs).
|
||||
|
||||
| Entity type | Idempotency key | How to check existence |
|
||||
|-------------|----------------|----------------------|
|
||||
| Scene nodes (pages, frames, components) | `setSharedPluginData('dsb', 'key', value)` or unique name | `node.getSharedPluginData('dsb', 'key')` or `page.findOne(n => n.name === 'Button')` |
|
||||
| Variables | Name within collection | `(await figma.variables.getLocalVariablesAsync()).find(v => v.name === name && v.variableCollectionId === collId)` |
|
||||
| Styles | Name | `getLocalTextStyles().find(s => s.name === name)` |
|
||||
|
||||
Tag every created **scene node** immediately after creation:
|
||||
```javascript
|
||||
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // identifies this build run
|
||||
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created it
|
||||
node.setSharedPluginData('dsb', 'key', 'component/button');// unique logical key
|
||||
```
|
||||
|
||||
**State persistence**: Do NOT rely solely on conversation context for the state ledger. Write it to disk:
|
||||
```
|
||||
/tmp/dsb-state-{RUN_ID}.json
|
||||
```
|
||||
Re-read this file at the start of every turn. In long workflows, conversation context will be truncated — the file is the source of truth.
|
||||
|
||||
Maintain a state ledger tracking:
|
||||
```json
|
||||
{
|
||||
"runId": "ds-build-2024-001",
|
||||
"phase": "phase3",
|
||||
"step": "component-button",
|
||||
"entities": {
|
||||
"collections": { "primitives": "id:...", "color": "id:..." },
|
||||
"variables": { "color/bg/primary": "id:...", "spacing/sm": "id:..." },
|
||||
"pages": { "Cover": "id:...", "Button": "id:..." },
|
||||
"components": { "Button": "id:..." }
|
||||
},
|
||||
"pendingValidations": ["Button:screenshot"],
|
||||
"completedSteps": ["phase0", "phase1", "phase2", "component-avatar"]
|
||||
}
|
||||
```
|
||||
|
||||
**Idempotency check** before every create: query by name + state ledger ID. If exists, skip or update — never duplicate.
|
||||
|
||||
**Resume protocol**: at session start or after context truncation, run a read-only `use_figma` to scan all pages, components, variables, and styles by name to reconstruct the `{key → id}` map. Then re-read the state file from disk if available.
|
||||
|
||||
**Continuation prompt** (give this to the user when resuming in a new chat):
|
||||
> "I'm continuing a design system build. Run ID: {RUN_ID}. Load the figma-generate-library skill and resume from the last completed step."
|
||||
|
||||
---
|
||||
|
||||
## 5. search_design_system — Reuse Decision Matrix
|
||||
|
||||
Search FIRST in Phase 0, then again immediately before each component creation.
|
||||
|
||||
```
|
||||
search_design_system({ query, fileKey, includeComponents: true, includeVariables: true, includeStyles: true })
|
||||
```
|
||||
|
||||
**Reuse if** all of these are true:
|
||||
- Component property API matches your needs (same variant axes, compatible types)
|
||||
- Token binding model is compatible (uses same or aliasable variables)
|
||||
- Naming conventions match the target file
|
||||
- Component is editable (not locked in a remote library you don't own)
|
||||
|
||||
**Rebuild if** any of these:
|
||||
- API incompatibility (different property names, wrong variant model)
|
||||
- Token model incompatible (hardcoded values, different variable schema)
|
||||
- Ownership issue (can't modify the library)
|
||||
|
||||
**Wrap if** visual match but API incompatible:
|
||||
- Import the library component as a nested instance inside a new wrapper component
|
||||
- Expose a clean API on the wrapper
|
||||
|
||||
**Three-way priority**: local existing → subscribed library import → create new.
|
||||
|
||||
---
|
||||
|
||||
## 6. User Checkpoints
|
||||
|
||||
Mandatory. Design decisions require human judgment.
|
||||
|
||||
| After | Required artifacts | Ask |
|
||||
|-------|-------------------|-----|
|
||||
| Discovery + scope lock | Token list, component list, gap analysis | "Here's my plan. Approve before I create anything?" |
|
||||
| Foundations | Variable summary (N collections, M vars, K modes), style list | "All tokens created. Review before file structure?" |
|
||||
| File structure | Page list + screenshot | "Pages set up. Review before components?" |
|
||||
| Each component | get_screenshot of component page | "Here's [Component] with N variants. Correct?" |
|
||||
| Each conflict (code ≠ Figma) | Show both versions | "Code says X, Figma has Y. Which wins?" |
|
||||
| Final QA | Per-page screenshots + audit report | "Complete. Sign off?" |
|
||||
|
||||
**If user rejects**: fix before moving on. Never build on rejected work.
|
||||
|
||||
---
|
||||
|
||||
## 7. Naming Conventions
|
||||
|
||||
Match existing file conventions. If starting fresh:
|
||||
|
||||
**Variables** (slash-separated):
|
||||
```
|
||||
color/bg/primary color/text/secondary color/border/default
|
||||
spacing/xs spacing/sm spacing/md spacing/lg spacing/xl spacing/2xl
|
||||
radius/none radius/sm radius/md radius/lg radius/full
|
||||
typography/body/font-size typography/heading/line-height
|
||||
```
|
||||
|
||||
**Primitives**: `blue/50` → `blue/900`, `gray/50` → `gray/900`
|
||||
|
||||
**Component names**: `Button`, `Input`, `Card`, `Avatar`, `Badge`, `Checkbox`, `Toggle`
|
||||
|
||||
**Variant names**: `Property=Value, Property=Value` — e.g., `Size=Medium, Style=Primary, State=Default`
|
||||
|
||||
**Page separators**: `---` (most common) or `——— COMPONENTS ———`
|
||||
|
||||
> Full naming reference: [naming-conventions.md](references/naming-conventions.md)
|
||||
|
||||
---
|
||||
|
||||
## 8. Token Architecture
|
||||
|
||||
| Complexity | Pattern |
|
||||
|-----------|---------|
|
||||
| < 50 tokens | Single collection, 2 modes (Light/Dark) |
|
||||
| 50–200 tokens | **Standard**: Primitives (1 mode) + Color semantic (Light/Dark) + Spacing (1 mode) + Typography (1 mode) |
|
||||
| 200+ tokens | **Advanced**: Multiple semantic collections, 4–8 modes (Light/Dark × Contrast × Brand). See M3 pattern in [token-creation.md](references/token-creation.md) |
|
||||
|
||||
Standard pattern (recommended starting point):
|
||||
```
|
||||
Collection: "Primitives" modes: ["Value"]
|
||||
blue/500 = #3B82F6, gray/900 = #111827, ...
|
||||
|
||||
Collection: "Color" modes: ["Light", "Dark"]
|
||||
color/bg/primary → Light: alias Primitives/white, Dark: alias Primitives/gray-900
|
||||
color/text/primary → Light: alias Primitives/gray-900, Dark: alias Primitives/white
|
||||
|
||||
Collection: "Spacing" modes: ["Value"]
|
||||
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Per-Phase Anti-Patterns
|
||||
|
||||
**Phase 0 anti-patterns:**
|
||||
- ❌ Starting to create anything before scope is locked with user
|
||||
- ❌ Ignoring existing file conventions and imposing new ones
|
||||
- ❌ Skipping `search_design_system` before planning component creation
|
||||
|
||||
**Phase 1 anti-patterns:**
|
||||
- ❌ Using `ALL_SCOPES` on any variable
|
||||
- ❌ Duplicating raw values in semantic layer instead of aliasing
|
||||
- ❌ Not setting code syntax (breaks Dev Mode and round-tripping)
|
||||
- ❌ Creating component tokens before agreeing on token taxonomy
|
||||
|
||||
**Phase 2 anti-patterns:**
|
||||
- ❌ Skipping the cover page or foundations docs
|
||||
- ❌ Putting multiple unrelated components on one page
|
||||
|
||||
**Phase 3 anti-patterns:**
|
||||
- ❌ Creating components before foundations exist
|
||||
- ❌ Hardcoding any fill/stroke/spacing/radius value in a component
|
||||
- ❌ Creating a variant per icon (use INSTANCE_SWAP instead)
|
||||
- ❌ Not positioning variants after combineAsVariants (they all stack at 0,0)
|
||||
- ❌ Building variant matrix > 30 without splitting (variant explosion)
|
||||
- ❌ Importing remote components then immediately detaching them
|
||||
|
||||
**General anti-patterns:**
|
||||
- ❌ Retrying a failed script without understanding the error first
|
||||
- ❌ Using name-prefix matching for cleanup (deletes user-owned nodes)
|
||||
- ❌ Building on unvalidated work from the previous step
|
||||
- ❌ Skipping user checkpoints to "save time"
|
||||
- ❌ Parallelizing use_figma calls (always sequential)
|
||||
- ❌ Guessing/hallucinating node IDs from memory (always read from state ledger)
|
||||
- ❌ Writing massive inline scripts instead of using the provided helper scripts
|
||||
- ❌ Starting Phase 3 because the user said "build the button" without completing Phases 0-2
|
||||
|
||||
---
|
||||
|
||||
## 10. Reference Docs
|
||||
|
||||
Load on demand — each reference is authoritative for its phase:
|
||||
|
||||
Use your file reading tool to read these docs when needed. Do not assume their contents from the filename.
|
||||
|
||||
| Doc | Phase | Required / Optional | Load when |
|
||||
|-----|-------|---------------------|-----------|
|
||||
| [discovery-phase.md](references/discovery-phase.md) | 0 | **Required** | Starting any build — codebase analysis + Figma inspection |
|
||||
| [token-creation.md](references/token-creation.md) | 1 | **Required** | Creating variables, collections, modes, styles |
|
||||
| [documentation-creation.md](references/documentation-creation.md) | 2 | Required | Creating cover page, foundations docs, swatches |
|
||||
| [component-creation.md](references/component-creation.md) | 3 | **Required** | Creating any component or variant |
|
||||
| [code-connect-setup.md](references/code-connect-setup.md) | 3–4 | Required | Setting up Code Connect or variable code syntax |
|
||||
| [naming-conventions.md](references/naming-conventions.md) | Any | Optional | Naming anything — variables, pages, variants, styles |
|
||||
| [error-recovery.md](references/error-recovery.md) | Any | **Required on error** | Script fails, multi-step workflow recovery, cleanup of abandoned workflow state |
|
||||
|
||||
---
|
||||
|
||||
## 11. Scripts
|
||||
|
||||
Reusable Plugin API helper functions. Embed in `use_figma` calls:
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| [inspectFileStructure.js](scripts/inspectFileStructure.js) | Discover all pages, components, variables, styles; returns full inventory |
|
||||
| [createVariableCollection.js](scripts/createVariableCollection.js) | Create a named collection with modes; returns `{collectionId, modeIds}` |
|
||||
| [createSemanticTokens.js](scripts/createSemanticTokens.js) | Create aliased semantic variables from a token map |
|
||||
| [createComponentWithVariants.js](scripts/createComponentWithVariants.js) | Build a component set from a variant matrix; handles grid layout |
|
||||
| [bindVariablesToComponent.js](scripts/bindVariablesToComponent.js) | Bind design tokens to all component visual properties |
|
||||
| [createDocumentationPage.js](scripts/createDocumentationPage.js) | Create a page with title + description + section structure |
|
||||
| [validateCreation.js](scripts/validateCreation.js) | Verify created nodes match expected counts, names, structure |
|
||||
| [cleanupOrphans.js](scripts/cleanupOrphans.js) | Remove orphaned nodes by name convention or state ledger IDs |
|
||||
| [rehydrateState.js](scripts/rehydrateState.js) | Scan file for all pages, components, variables by name; returns full `{key → nodeId}` map for state reconstruction |
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Code Connect Setup Reference
|
||||
|
||||
This reference covers all Code Connect tooling available to the figma-generate-library agent: the `add_code_connect_map` tool, `get_code_connect_map` for verification, `send_code_connect_mappings` for bulk application, variable code syntax, framework labels, and the decision of when to map per-component vs. in a final pass.
|
||||
|
||||
---
|
||||
|
||||
## 1. What Code Connect Does
|
||||
|
||||
Code Connect links a Figma component node to its code implementation so that:
|
||||
|
||||
- **Dev Mode** shows a real code snippet (from your codebase) instead of an auto-generated approximation when a developer inspects a component.
|
||||
- **MCP `get_design_context`** returns `componentName`, `source`, and a rendered snippet alongside design tokens, enabling accurate AI-assisted code generation.
|
||||
- **`search_design_system`** can return code references alongside Figma component metadata.
|
||||
|
||||
---
|
||||
|
||||
## 2. The Three MCP Tools
|
||||
|
||||
### 2a. add_code_connect_map — single mapping
|
||||
|
||||
Maps one Figma node to one code component.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Notes |
|
||||
|-----------|------|----------|-------|
|
||||
| `nodeId` | string | Yes (remote) / Optional (desktop) | Format `123:456`. Must be a published component or component set. |
|
||||
| `fileKey` | string | Yes (remote) | The Figma file key. |
|
||||
| `source` | string | Yes | Path in the codebase (e.g. `src/components/Button.tsx`) or a URL. |
|
||||
| `componentName` | string | Yes | The code component name (e.g. `Button`). |
|
||||
| `label` | enum | Yes | Framework label — see Section 4 for valid values. |
|
||||
| `template` | string | Optional | Executable JS template code. Providing this creates a **figmadoc** (template) mapping instead of a simple **component_browser** mapping. Requires the `pixie_mcp_enable_writing_code_connect_templates` feature flag. |
|
||||
| `templateDataJson` | string | Optional | JSON string with optional fields: `isParserless`, `imports`, `nestable`, `props`. |
|
||||
|
||||
**Two mapping tiers:**
|
||||
|
||||
1. **Simple mapping (component_browser):** Only `source`, `componentName`, and `label` provided. Associates the Figma component with a code path + name. Dev Mode generates a basic JSX snippet from Figma prop names. This is the default — use it first.
|
||||
|
||||
2. **Template mapping (figmadoc):** `template` is also provided. The template is executed in a sandboxed QuickJS environment and dynamically renders the snippet based on the actual instance's property values. Use this when precise prop-level Code Connect is required by the user.
|
||||
|
||||
**Common error codes:**
|
||||
|
||||
| Error | Meaning | Fix |
|
||||
|-------|---------|-----|
|
||||
| `CODE_CONNECT_MAPPING_ALREADY_EXISTS` | Component is already mapped | Disconnect existing mapping in Figma UI first |
|
||||
| `CODE_CONNECT_ASSET_NOT_FOUND` | Published component not found | Ensure the component is published to the library |
|
||||
| `CODE_CONNECT_INSUFFICIENT_PERMISSIONS` | No edit access | Request edit permission on the file |
|
||||
| `CODE_CONNECT_NO_LIBRARY_FOUND` | File is not published as a library | Publish the file as a Figma library first |
|
||||
|
||||
**Usage example:**
|
||||
|
||||
```
|
||||
Tool: add_code_connect_map
|
||||
Args: {
|
||||
nodeId: "123:456",
|
||||
fileKey: "abc123",
|
||||
source: "src/components/Button.tsx",
|
||||
componentName: "Button",
|
||||
label: "React"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2b. get_code_connect_map — verification
|
||||
|
||||
Retrieves the current Code Connect mapping for a node. Use this immediately after `add_code_connect_map` to confirm the mapping was saved, and before `send_code_connect_mappings` to audit existing state.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Notes |
|
||||
|-----------|------|----------|-------|
|
||||
| `nodeId` | string | Optional | The node to check. Omit to get all mappings in the file. |
|
||||
| `fileKey` | string | Yes (remote) | The Figma file key. |
|
||||
| `codeConnectLabel` | string | Optional | Filter results to a specific framework label. |
|
||||
|
||||
**Returns:** A map of `nodeId -> { componentName, source, label, snippet, snippetImports }`.
|
||||
|
||||
**How to verify:**
|
||||
|
||||
```
|
||||
1. Call add_code_connect_map with the node.
|
||||
2. Immediately call get_code_connect_map(nodeId, fileKey).
|
||||
3. Confirm the returned object has the expected componentName and source.
|
||||
4. If the mapping is missing, check for error codes from step 1.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2c. send_code_connect_mappings — bulk application
|
||||
|
||||
Applies multiple Code Connect mappings in one call. Use after `get_code_connect_suggestions` returns a batch of unmapped components, or when doing a final-pass bulk mapping at the end of Phase 4.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
| Parameter | Type | Required | Notes |
|
||||
|-----------|------|----------|-------|
|
||||
| `nodeId` | string | Optional | Context node for design fallback if mappings array is empty. |
|
||||
| `fileKey` | string | Yes (remote) | The Figma file key. |
|
||||
| `mappings` | array | Yes | Array of mapping objects. |
|
||||
|
||||
**Each mapping object:**
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|-------|------|----------|-------|
|
||||
| `nodeId` | string | Yes | The Figma node identifier. |
|
||||
| `componentName` | string | Yes | Code component name. |
|
||||
| `source` | string | Yes | Path in the codebase. |
|
||||
| `label` | enum | Yes | Framework label. |
|
||||
| `template` | string | Optional | JS template code for figmadoc mapping. |
|
||||
| `templateDataJson` | string | Optional | JSON template metadata. |
|
||||
|
||||
**Behavior:**
|
||||
|
||||
- All mappings are processed in parallel via POSTs to the backend.
|
||||
- If any mapping fails, errors are reported per mapping — the rest succeed.
|
||||
- On full success, `get_design_context` is called for the nodes and fresh design context is returned.
|
||||
|
||||
**Bulk workflow:**
|
||||
|
||||
```
|
||||
1. Collect all {nodeId, componentName, source, label} pairs.
|
||||
2. Call send_code_connect_mappings({ fileKey, mappings: [...all pairs...] }).
|
||||
3. Review reported errors and call add_code_connect_map individually for any failures.
|
||||
4. Call get_code_connect_map on a sample of nodes to spot-check.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Variable Code Syntax (Token Round-Tripping)
|
||||
|
||||
Setting code syntax on variables creates the bidirectional link between Figma tokens and the codebase token system. This is what enables Dev Mode to show `var(--color-bg-primary)` next to a design value instead of a raw hex.
|
||||
|
||||
**The three platforms:**
|
||||
|
||||
```javascript
|
||||
// In use_figma:
|
||||
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
|
||||
variable.setVariableCodeSyntax('ANDROID', 'Theme.colorBgPrimary');
|
||||
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
|
||||
```
|
||||
|
||||
- `WEB` — used for CSS custom properties, design token JSON, and any web framework.
|
||||
- `ANDROID` — used for Jetpack Compose theme references and Android resource names.
|
||||
- `iOS` — used for SwiftUI Color extensions and UIKit color methods.
|
||||
|
||||
**Derivation rules (in priority order):**
|
||||
|
||||
1. **Best:** Use the exact token name from the codebase. Search the codebase for CSS custom properties (`--`), Swift color extensions, or Kotlin theme references and use those exact strings.
|
||||
2. **Good:** Derive from the Figma variable name with a consistent transformation: replace `/` and spaces with `-`, prefix with `var(--` and suffix with `)`.
|
||||
- Example: `color/bg/primary` → `var(--color-bg-primary)`
|
||||
3. **Avoid:** Guessing or inventing names that don't exist in the codebase.
|
||||
|
||||
**Consistency rule:** The transformation must be uniform. If you use `var(--color-bg-primary)` for one variable, use the same `var(--{path-with-hyphens})` pattern for all variables in that collection.
|
||||
|
||||
**WEB syntax bulk example:**
|
||||
|
||||
```javascript
|
||||
// In use_figma — set WEB code syntax on all variables in a collection
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
for (const coll of collections) {
|
||||
if (coll.name !== 'Color') continue;
|
||||
for (const varId of coll.variableIds) {
|
||||
const v = await figma.variables.getVariableByIdAsync(varId);
|
||||
if (!v) continue;
|
||||
// Derive: "color/bg/primary" → "var(--color-bg-primary)"
|
||||
const cssName = 'var(--' + v.name.toLowerCase().replace(/\//g, '-').replace(/\s+/g, '-') + ')';
|
||||
v.setVariableCodeSyntax('WEB', cssName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Framework Labels
|
||||
|
||||
The following labels are valid for all Code Connect MCP operations. Use the label that matches your codebase framework.
|
||||
|
||||
| Label | Use for |
|
||||
|-------|---------|
|
||||
| `React` | React / JSX / TSX components |
|
||||
| `Web Components` | Native Web Components, Lit, FAST |
|
||||
| `Vue` | Vue 2 and Vue 3 SFCs |
|
||||
| `Svelte` | Svelte components |
|
||||
| `Storybook` | Storybook stories with Code Connect integration |
|
||||
| `Javascript` | Plain JavaScript, framework-agnostic |
|
||||
| `Swift` | Swift / UIKit |
|
||||
| `Swift UIKit` | UIKit specifically |
|
||||
| `Objective-C UIKit` | Objective-C with UIKit |
|
||||
| `SwiftUI` | SwiftUI view components |
|
||||
| `Compose` | Jetpack Compose (Android) |
|
||||
| `Java` | Java Android components |
|
||||
| `Kotlin` | Kotlin Android (non-Compose) |
|
||||
| `Android XML Layout` | Android XML layout files |
|
||||
| `Flutter` | Flutter / Dart widgets |
|
||||
| `Markdown` | Documentation or MDX components |
|
||||
|
||||
**HTML note:** The label `HTML` is used by the Code Connect CLI's HTML parser (for Angular, Vue, and Web Components without a framework-specific parser), but the MCP tools use `Web Components` or `Vue` directly. Check the codebase framework before selecting.
|
||||
|
||||
---
|
||||
|
||||
## 5. Per-Component vs. Final-Pass Strategy
|
||||
|
||||
### Per-component (preferred for new builds)
|
||||
|
||||
Map Code Connect immediately after creating a component, while the context is fresh (Phase 3, step 3h in the SKILL.md workflow):
|
||||
|
||||
**Advantages:**
|
||||
- The node ID is already in hand from the creation script.
|
||||
- You know exactly which code component this Figma component corresponds to (you just designed it to match).
|
||||
- Errors surface early, before building dependent components.
|
||||
|
||||
**When to use:** Any time you create a Figma component that has a clear 1:1 match with an existing code component.
|
||||
|
||||
### Final pass (for bulk mapping at Phase 4)
|
||||
|
||||
Collect all unmapped components and map them in one `send_code_connect_mappings` call:
|
||||
|
||||
**Advantages:**
|
||||
- One bulk call instead of N individual calls.
|
||||
- Can use `get_code_connect_suggestions` to discover unmapped components automatically.
|
||||
- Better for importing existing Figma files where you didn't control creation.
|
||||
|
||||
**When to use:** Retrofitting Code Connect onto an existing file, or when the codebase mapping requires research that is better done after all components are created.
|
||||
|
||||
### Hybrid (recommended for large systems)
|
||||
|
||||
- Map atoms (Button, Input, Badge, Avatar) **per-component** during Phase 3.
|
||||
- Map molecules and organisms in a **final pass** during Phase 4 after all atoms are mapped, since molecule snippets reference atom Code Connect IDs.
|
||||
|
||||
---
|
||||
|
||||
## 6. Verification in Dev Mode
|
||||
|
||||
After mapping:
|
||||
|
||||
1. Open the Figma file in the browser or desktop app.
|
||||
2. Switch to Dev Mode (the `</>` icon in the toolbar).
|
||||
3. Select a component instance (not the main component — an instance placed on a page).
|
||||
4. In the Inspect panel, the code snippet should show the Code Connect output instead of auto-generated code.
|
||||
5. If the snippet is missing or shows `[auto-generated]`, run `get_code_connect_map` via MCP to confirm the mapping exists, then check that the component is published.
|
||||
|
||||
**Via MCP (faster during agent workflows):**
|
||||
|
||||
```
|
||||
get_code_connect_map(nodeId: "<the component set node ID>", fileKey: "<file key>")
|
||||
```
|
||||
|
||||
The response should include `componentName`, `source`, `label`, and a non-empty `snippet`.
|
||||
|
||||
---
|
||||
|
||||
## 7. Important Constraints
|
||||
|
||||
- **Published components only:** `add_code_connect_map` requires the component to be published to a library. If the file is not yet published, the mapping will fail with `CODE_CONNECT_NO_LIBRARY_FOUND`.
|
||||
- **One mapping per label per node:** A node can have multiple mappings (one per framework label), but only one per label. Attempting to add a second React mapping to the same node returns `CODE_CONNECT_MAPPING_ALREADY_EXISTS`.
|
||||
- **Template mappings are gated:** The `template` parameter requires the `pixie_mcp_enable_writing_code_connect_templates` feature flag. Use simple mappings unless the user explicitly requests template-level Code Connect.
|
||||
- **Start simple, escalate:** Always begin with simple mappings (`source` + `componentName` + `label`). Add `template` only if the user needs precise prop-level snippet rendering.
|
||||
+972
@@ -0,0 +1,972 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Component Creation Reference
|
||||
|
||||
Complete guide for Phase 3: building components with variant matrices, variable bindings, component properties, and documentation.
|
||||
|
||||
---
|
||||
|
||||
## 1. Component Architecture
|
||||
|
||||
### Dependency Ordering: Atoms Before Molecules
|
||||
|
||||
Always build in dependency order. A molecule that contains an atom instance cannot exist until the atom is published. Suggested ordering:
|
||||
|
||||
```
|
||||
Tier 0 (atoms): Icon, Avatar, Badge, Spinner
|
||||
Tier 1 (molecules): Button, Checkbox, Toggle, Input, Select
|
||||
Tier 2 (organisms): Card, Dialog, Menu, Navigation, Form
|
||||
```
|
||||
|
||||
If a component embeds an instance of another component, the embedded component must be created first. Build your dependency graph during Phase 0 and encode the creation order in the plan.
|
||||
|
||||
### Building Blocks Sub-Components (M3 Pattern)
|
||||
|
||||
For complex components with independent sub-element state machines, extract the sub-element into its own component set prefixed with `Building Blocks/` (public) or `.Building Blocks/` (hidden from assets panel). The dot-prefix is a Figma convention for suppressing a component from the public assets panel.
|
||||
|
||||
**When to use Building Blocks:**
|
||||
- The sub-element has its own variant axes (state, selection) that would cause combinatorial explosion in the parent
|
||||
- The sub-element repeats (nav items, table cells, calendar cells, segmented button segments)
|
||||
- The sub-element has different variant axes than the parent
|
||||
|
||||
**Example (M3 Segmented Button):**
|
||||
```
|
||||
Building Blocks/Segmented button/Button segment (start) [27 variants: Config × State × Selected]
|
||||
Building Blocks/Segmented button/Button segment (middle) [27 variants]
|
||||
Building Blocks/Segmented button/Button segment (end) [27 variants]
|
||||
|
||||
Segmented button [16 variants: Segments=2-5 × Density=0/-1/-2/-3]
|
||||
Each variant contains instances of the appropriate Building Block segment components.
|
||||
```
|
||||
|
||||
The parent manages composition and configuration; the Building Block manages its own interaction states.
|
||||
|
||||
### Private Components (`__` Prefix)
|
||||
|
||||
Use the `__` prefix for internal helper components that should not appear in the team library (Shop Minis pattern). Use `_` for documentation-only components (UI3 pattern).
|
||||
|
||||
```
|
||||
__asset // private icon/asset holder
|
||||
_Label/Direction // documentation annotation helper
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Creating the Component Page
|
||||
|
||||
Each component lives on its own dedicated page (one page per component is the default). The page contains: a documentation frame at top-left and the component set positioned to its right or below.
|
||||
|
||||
```javascript
|
||||
// Create or find the component page
|
||||
let page = figma.root.children.find(p => p.name === 'Button');
|
||||
if (!page) {
|
||||
page = figma.createPage();
|
||||
page.name = 'Button';
|
||||
}
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
// Documentation frame — positioned at (40, 40)
|
||||
const docFrame = figma.createFrame();
|
||||
docFrame.name = 'Button / Documentation';
|
||||
docFrame.x = 40;
|
||||
docFrame.y = 40;
|
||||
docFrame.resize(600, 400);
|
||||
docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
||||
docFrame.layoutMode = 'VERTICAL';
|
||||
docFrame.primaryAxisSizingMode = 'AUTO';
|
||||
docFrame.counterAxisSizingMode = 'FIXED';
|
||||
docFrame.paddingTop = 40;
|
||||
docFrame.paddingBottom = 40;
|
||||
docFrame.paddingLeft = 40;
|
||||
docFrame.paddingRight = 40;
|
||||
docFrame.itemSpacing = 16;
|
||||
|
||||
// Title text node
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
const title = figma.createText();
|
||||
title.fontName = { family: 'Inter', style: 'Bold' };
|
||||
title.fontSize = 32;
|
||||
title.characters = 'Button';
|
||||
docFrame.appendChild(title);
|
||||
|
||||
// Description text node
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
const desc = figma.createText();
|
||||
desc.fontName = { family: 'Inter', style: 'Regular' };
|
||||
desc.fontSize = 14;
|
||||
desc.characters = 'Buttons allow users to take actions and make choices with a single tap.';
|
||||
docFrame.appendChild(desc);
|
||||
|
||||
// Tag docFrame with sharedPluginData for idempotency
|
||||
docFrame.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
docFrame.setSharedPluginData('dsb', 'key', 'doc/button');
|
||||
|
||||
return { docFrameId: docFrame.id, pageId: page.id };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Base Component: Auto-Layout, Child Nodes, Variable Bindings
|
||||
|
||||
The base component is the template from which all variants are cloned. It must have:
|
||||
1. Auto-layout (not manual positioning)
|
||||
2. All child nodes present
|
||||
3. ALL visual properties bound to variables (no hardcoded values)
|
||||
|
||||
### Complete Button Base Component Example
|
||||
|
||||
```javascript
|
||||
const RUN_ID = 'ds-build-2024-001'; // replace with your actual run ID
|
||||
await figma.setCurrentPageAsync(
|
||||
figma.root.children.find(p => p.name === 'Button')
|
||||
);
|
||||
|
||||
// Rehydrate variables from IDs stored in state ledger
|
||||
const bgVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary');
|
||||
const textVar = await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary');
|
||||
const paddingVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md');
|
||||
const radiusVar = await figma.variables.getVariableByIdAsync('VAR_ID_radius_md');
|
||||
const gapVar = await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm');
|
||||
|
||||
// --- Base component frame ---
|
||||
const comp = figma.createComponent();
|
||||
comp.name = 'Size=Medium, Style=Primary, State=Default';
|
||||
comp.layoutMode = 'HORIZONTAL';
|
||||
comp.primaryAxisSizingMode = 'AUTO';
|
||||
comp.counterAxisSizingMode = 'AUTO';
|
||||
comp.counterAxisAlignItems = 'CENTER';
|
||||
comp.primaryAxisAlignItems = 'CENTER';
|
||||
|
||||
// Padding — bound to spacing variables
|
||||
comp.setBoundVariable('paddingTop', paddingVar);
|
||||
comp.setBoundVariable('paddingBottom', paddingVar);
|
||||
comp.setBoundVariable('paddingLeft', paddingVar);
|
||||
comp.setBoundVariable('paddingRight', paddingVar);
|
||||
comp.setBoundVariable('itemSpacing', gapVar);
|
||||
|
||||
// Corner radius — bound to radius variable
|
||||
comp.setBoundVariable('topLeftRadius', radiusVar);
|
||||
comp.setBoundVariable('topRightRadius', radiusVar);
|
||||
comp.setBoundVariable('bottomLeftRadius', radiusVar);
|
||||
comp.setBoundVariable('bottomRightRadius', radiusVar);
|
||||
|
||||
// Background fill — bound to color variable
|
||||
const bgPaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } },
|
||||
'color',
|
||||
bgVar
|
||||
);
|
||||
comp.fills = [bgPaint];
|
||||
|
||||
// --- Label text node ---
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
const label = figma.createText();
|
||||
label.name = 'label';
|
||||
label.fontName = { family: 'Inter', style: 'Medium' };
|
||||
label.fontSize = 14;
|
||||
label.characters = 'Button';
|
||||
label.layoutSizingHorizontal = 'HUG';
|
||||
label.layoutSizingVertical = 'HUG';
|
||||
|
||||
// Text fill — bound to color variable
|
||||
const textPaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } },
|
||||
'color',
|
||||
textVar
|
||||
);
|
||||
label.fills = [textPaint];
|
||||
comp.appendChild(label);
|
||||
|
||||
// --- Icon placeholder (Rectangle for now — will be INSTANCE_SWAP) ---
|
||||
const iconBox = figma.createFrame();
|
||||
iconBox.name = 'icon';
|
||||
iconBox.resize(16, 16);
|
||||
iconBox.fills = [];
|
||||
iconBox.layoutSizingHorizontal = 'FIXED';
|
||||
iconBox.layoutSizingVertical = 'FIXED';
|
||||
comp.appendChild(iconBox);
|
||||
|
||||
// Tag for idempotency
|
||||
comp.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
comp.setSharedPluginData('dsb', 'phase', 'phase3');
|
||||
comp.setSharedPluginData('dsb', 'key', 'component/button/base');
|
||||
|
||||
return { baseCompId: comp.id };
|
||||
```
|
||||
|
||||
**ALL of these must be variable-bound (never hardcoded):**
|
||||
|
||||
| Property | Variable type | API method |
|
||||
|---|---|---|
|
||||
| Fill color | COLOR | `setBoundVariableForPaint(..., 'color', var)` |
|
||||
| Stroke color | COLOR | `setBoundVariableForPaint(..., 'color', var)` |
|
||||
| Text fill | COLOR | `setBoundVariableForPaint(..., 'color', var)` |
|
||||
| Padding (all 4 sides) | FLOAT | `comp.setBoundVariable('paddingTop', var)` |
|
||||
| Gap / itemSpacing | FLOAT | `comp.setBoundVariable('itemSpacing', var)` |
|
||||
| Corner radius (all 4) | FLOAT | `comp.setBoundVariable('topLeftRadius', var)` etc. |
|
||||
| Stroke weight | FLOAT | `comp.setBoundVariable('strokeWeight', var)` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Variant Matrix
|
||||
|
||||
### Defining Axes
|
||||
|
||||
For each component, identify its variant axes before writing any code. Standard axes:
|
||||
|
||||
```
|
||||
Button:
|
||||
Size → [Small, Medium, Large]
|
||||
Style → [Primary, Secondary, Outline, Ghost]
|
||||
State → [Default, Hover, Focused, Pressed, Disabled]
|
||||
Total = 3 × 4 × 5 = 60 combinations — exceeds 30 limit → split by Style
|
||||
```
|
||||
|
||||
### The 30-Combination Cap and Split Strategy
|
||||
|
||||
When the product of all variant axes exceeds 30 combinations, split the matrix. Options:
|
||||
|
||||
1. **Split by a primary axis**: Create separate component sets, one per Style (Primary Button, Secondary Button, etc.)
|
||||
2. **Use INSTANCE_SWAP**: Remove a visual axis (like Icon) from the variant matrix entirely and expose it as an INSTANCE_SWAP property instead
|
||||
3. **Use Building Blocks**: Extract sub-elements with their own state axes into Building Block component sets
|
||||
|
||||
For Button with Size × State = 15 combinations, add Style as a variant axis only if Style ≤ 2 options (15 × 2 = 30). For more Styles, split.
|
||||
|
||||
### Creating All Variants with use_figma
|
||||
|
||||
Build each variant by cloning the base component and adjusting the variable bindings that differ per variant. Pass in the base component ID from the previous call's state.
|
||||
|
||||
```javascript
|
||||
const RUN_ID = 'ds-build-2024-001';
|
||||
const BASE_COMP_ID = 'BASE_ID_FROM_STATE'; // from state ledger
|
||||
|
||||
await figma.setCurrentPageAsync(
|
||||
figma.root.children.find(p => p.name === 'Button')
|
||||
);
|
||||
|
||||
const base = await figma.getNodeByIdAsync(BASE_COMP_ID);
|
||||
|
||||
// Variable IDs from state ledger
|
||||
const vars = {
|
||||
// Primary style
|
||||
bg_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_primary'),
|
||||
text_primary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_on_primary'),
|
||||
// Secondary style
|
||||
bg_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_secondary'),
|
||||
text_secondary: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_secondary'),
|
||||
// Disabled
|
||||
bg_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_bg_disabled'),
|
||||
text_disabled: await figma.variables.getVariableByIdAsync('VAR_ID_color_text_disabled'),
|
||||
// Sizes
|
||||
padding_sm: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_sm'),
|
||||
padding_md: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_md'),
|
||||
padding_lg: await figma.variables.getVariableByIdAsync('VAR_ID_spacing_lg'),
|
||||
};
|
||||
|
||||
const axes = {
|
||||
Size: ['Small', 'Medium', 'Large'],
|
||||
Style: ['Primary', 'Secondary'],
|
||||
State: ['Default', 'Hover', 'Disabled'],
|
||||
};
|
||||
|
||||
const paddingBySize = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg };
|
||||
|
||||
const components = [];
|
||||
|
||||
for (const size of axes.Size) {
|
||||
for (const style of axes.Style) {
|
||||
for (const state of axes.State) {
|
||||
const clone = base.clone();
|
||||
clone.name = `Size=${size}, Style=${style}, State=${state}`;
|
||||
|
||||
// Bind padding by size
|
||||
clone.setBoundVariable('paddingTop', paddingBySize[size]);
|
||||
clone.setBoundVariable('paddingBottom', paddingBySize[size]);
|
||||
clone.setBoundVariable('paddingLeft', paddingBySize[size]);
|
||||
clone.setBoundVariable('paddingRight', paddingBySize[size]);
|
||||
|
||||
// Bind fill by style + state
|
||||
const isDisabled = state === 'Disabled';
|
||||
const bgVar = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary);
|
||||
const txtVar = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary);
|
||||
|
||||
const bgPaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgVar
|
||||
);
|
||||
clone.fills = [bgPaint];
|
||||
|
||||
const labelNode = clone.findOne(n => n.name === 'label');
|
||||
const textPaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txtVar
|
||||
);
|
||||
labelNode.fills = [textPaint];
|
||||
|
||||
clone.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`);
|
||||
|
||||
components.push(clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { variantIds: components.map(c => c.id) };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. `combineAsVariants` + Grid Layout
|
||||
|
||||
After all variant components exist, combine them into a ComponentSet and position them in a grid. This MUST be a separate `use_figma` call — you must pass in all variant IDs from the previous call's return value.
|
||||
|
||||
### Grid Design Conventions
|
||||
|
||||
Professional design systems lay out variants in a readable grid where:
|
||||
- **Columns** = the property users interact with most (typically **State**: Default, Hover, Focused, Pressed, Disabled)
|
||||
- **Rows** = structural axes grouped together (typically **Size × Style**, where Size varies fastest)
|
||||
- **Gap** = 16–40px between variants (20px is a safe default; match existing file if one exists)
|
||||
- **Padding** = 40px around the grid inside the ComponentSet frame
|
||||
|
||||
```
|
||||
Visual structure:
|
||||
Default Hover Focused Pressed Disabled
|
||||
┌──────────────────────────────────────────────────────────────────┐
|
||||
│ Small/Primary [comp] [comp] [comp] [comp] [comp] │
|
||||
│ Small/Secondary [comp] [comp] [comp] [comp] [comp] │
|
||||
│ Medium/Primary [comp] [comp] [comp] [comp] [comp] │
|
||||
│ Medium/Secondary[comp] [comp] [comp] [comp] [comp] │
|
||||
│ Large/Primary [comp] [comp] [comp] [comp] [comp] │
|
||||
│ Large/Secondary [comp] [comp] [comp] [comp] [comp] │
|
||||
└──────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Why State on columns?** State is the axis designers scan horizontally to verify interaction consistency. Size/Style define the "identity" of each row. This matches how professional design systems (M3, Polaris, Simple DS) organize their grids.
|
||||
|
||||
### Adding Row/Column Header Labels
|
||||
|
||||
After laying out the grid, add text labels OUTSIDE the ComponentSet to help navigation. These are siblings of the ComponentSet on the page — not children of it:
|
||||
|
||||
```javascript
|
||||
// Add column headers above the component set
|
||||
const colLabels = ['Default', 'Hover', 'Focused', 'Pressed', 'Disabled'];
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
for (let i = 0; i < colLabels.length; i++) {
|
||||
const label = figma.createText();
|
||||
label.fontName = { family: 'Inter', style: 'Medium' };
|
||||
label.characters = colLabels[i];
|
||||
label.fontSize = 11;
|
||||
label.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
||||
label.x = cs.x + padding + i * (childWidth + gap);
|
||||
label.y = cs.y - 20;
|
||||
}
|
||||
|
||||
// Add row headers to the left of the component set
|
||||
const rowLabels = ['Small / Primary', 'Small / Secondary', 'Med / Primary', ...];
|
||||
for (let i = 0; i < rowLabels.length; i++) {
|
||||
const label = figma.createText();
|
||||
label.fontName = { family: 'Inter', style: 'Medium' };
|
||||
label.characters = rowLabels[i];
|
||||
label.fontSize = 11;
|
||||
label.fills = [{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } }];
|
||||
label.x = cs.x - 120;
|
||||
label.y = cs.y + padding + i * (childHeight + gap) + childHeight / 2 - 6;
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** These labels are documentation aids, not part of the component itself. They help designers navigate the variant grid.
|
||||
|
||||
### Grid layout code
|
||||
|
||||
```javascript
|
||||
const VARIANT_IDS = ['ID1', 'ID2', '...']; // from state ledger
|
||||
const PAGE_ID = 'PAGE_ID'; // from state ledger
|
||||
|
||||
await figma.setCurrentPageAsync(await figma.getNodeByIdAsync(PAGE_ID));
|
||||
|
||||
// Collect component nodes
|
||||
const components = await Promise.all(
|
||||
VARIANT_IDS.map(id => figma.getNodeByIdAsync(id))
|
||||
);
|
||||
|
||||
// Combine as variants
|
||||
const cs = figma.combineAsVariants(components, figma.currentPage);
|
||||
cs.name = 'Button';
|
||||
|
||||
// Grid layout: position each variant based on its property values
|
||||
// Determine column axis (State) and row axes (Size × Style)
|
||||
const axes = {
|
||||
Size: ['Small', 'Medium', 'Large'],
|
||||
Style: ['Primary', 'Secondary'],
|
||||
State: ['Default', 'Hover', 'Disabled'],
|
||||
};
|
||||
const COL_AXIS = 'State'; // columns
|
||||
const ROW_AXES = ['Size', 'Style']; // rows (Size changes fastest)
|
||||
|
||||
const gap = 16;
|
||||
const padding = 40;
|
||||
|
||||
// Measure child dimensions (all should be same height within Size tier)
|
||||
// Use the first child as reference for column width
|
||||
const childWidth = 120; // approximate; refine after first screenshot
|
||||
const childHeight = 40;
|
||||
|
||||
cs.children.forEach(child => {
|
||||
const props = {};
|
||||
child.name.split(', ').forEach(part => {
|
||||
const [k, v] = part.split('=');
|
||||
props[k] = v;
|
||||
});
|
||||
|
||||
const colIdx = axes[COL_AXIS].indexOf(props[COL_AXIS]);
|
||||
// Row = Size index * number of styles + Style index
|
||||
const rowIdx = axes.Size.indexOf(props.Size) * axes.Style.length
|
||||
+ axes.Style.indexOf(props.Style);
|
||||
|
||||
child.x = padding + colIdx * (childWidth + gap);
|
||||
child.y = padding + rowIdx * (childHeight + gap);
|
||||
});
|
||||
|
||||
// Resize component set to fit all children + padding
|
||||
let maxX = 0, maxY = 0;
|
||||
for (const child of cs.children) {
|
||||
maxX = Math.max(maxX, child.x + child.width);
|
||||
maxY = Math.max(maxY, child.y + child.height);
|
||||
}
|
||||
cs.resizeWithoutConstraints(maxX + padding, maxY + padding);
|
||||
|
||||
// Style the component set frame
|
||||
cs.fills = [{ type: 'SOLID', color: { r: 0.95, g: 0.95, b: 0.98 } }];
|
||||
cs.cornerRadius = 8;
|
||||
|
||||
// Position component set on page (to the right of doc frame)
|
||||
cs.x = 680;
|
||||
cs.y = 40;
|
||||
|
||||
cs.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
|
||||
cs.setSharedPluginData('dsb', 'key', 'componentset/button');
|
||||
|
||||
return { componentSetId: cs.id };
|
||||
```
|
||||
|
||||
**Critical rules for combineAsVariants:**
|
||||
- `components` must be a non-empty array containing ONLY `ComponentNode` objects (not frames, not groups)
|
||||
- After combining, children are placed at (0,0) and overlap — you MUST manually position them
|
||||
- `resizeWithoutConstraints` is required after positioning to make the component set frame fit its contents
|
||||
- There is no `figma.createComponentSet()` — you cannot create an empty component set
|
||||
|
||||
---
|
||||
|
||||
## 6. Component Properties
|
||||
|
||||
Add TEXT, BOOLEAN, and INSTANCE_SWAP properties to the ComponentSet (not to individual variants). The return value of `addComponentProperty` is the actual property key (it gets a `#id:id` suffix appended) — save this key and use it immediately when setting `componentPropertyReferences`.
|
||||
|
||||
### TEXT Properties
|
||||
|
||||
Expose editable text in instances:
|
||||
|
||||
```javascript
|
||||
// On the ComponentSetNode (cs):
|
||||
const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button');
|
||||
// labelKey is now something like "Label#0:1"
|
||||
|
||||
// Wire to the label child in each variant:
|
||||
for (const child of cs.children) {
|
||||
const labelNode = child.findOne(n => n.name === 'label');
|
||||
if (labelNode) {
|
||||
labelNode.componentPropertyReferences = { characters: labelKey };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### BOOLEAN Properties
|
||||
|
||||
Toggle child node visibility:
|
||||
|
||||
```javascript
|
||||
const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true);
|
||||
|
||||
for (const child of cs.children) {
|
||||
const iconNode = child.findOne(n => n.name === 'icon');
|
||||
if (iconNode) {
|
||||
iconNode.componentPropertyReferences = { visible: showIconKey };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### INSTANCE_SWAP Properties
|
||||
|
||||
Allow swapping a nested component instance (e.g., swap the icon):
|
||||
|
||||
```javascript
|
||||
// defaultIconCompId is the ID of the default icon component (from state ledger)
|
||||
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_COMP_ID);
|
||||
|
||||
for (const child of cs.children) {
|
||||
const iconSlot = child.findOne(n => n.name === 'icon');
|
||||
if (iconSlot && iconSlot.type === 'INSTANCE') {
|
||||
iconSlot.componentPropertyReferences = { mainComponent: iconKey };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Use INSTANCE_SWAP instead of creating a variant per icon.** Never add "Icon=ChevronRight, Icon=ChevronLeft, ..." as VARIANT axes — that causes combinatorial explosion. One INSTANCE_SWAP property covers all icons.
|
||||
|
||||
### Creating Icon Components for INSTANCE_SWAP
|
||||
|
||||
INSTANCE_SWAP needs a real Component ID as its default value. Before wiring INSTANCE_SWAP, you need at least one icon component. Here's how to create icons from SVG:
|
||||
|
||||
```javascript
|
||||
// Create a simple icon component from SVG
|
||||
const svgNode = figma.createNodeFromSvg(
|
||||
'<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">' +
|
||||
'<path d="M9 18l6-6-6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>' +
|
||||
'</svg>'
|
||||
);
|
||||
|
||||
// Wrap in a component
|
||||
const iconComp = figma.createComponent();
|
||||
iconComp.name = 'Icon/ChevronRight';
|
||||
iconComp.resize(24, 24);
|
||||
iconComp.clipsContent = true;
|
||||
|
||||
// Move SVG children into the component
|
||||
for (const child of [...svgNode.children]) {
|
||||
iconComp.appendChild(child);
|
||||
}
|
||||
svgNode.remove();
|
||||
|
||||
// Bind the icon fill to a color variable (so it respects themes)
|
||||
// Find vector children and bind their fills
|
||||
iconComp.findAll(n => n.type === 'VECTOR').forEach(vec => {
|
||||
// For stroke-based icons:
|
||||
if (vec.strokes.length > 0) {
|
||||
const strokePaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', iconColorVar
|
||||
);
|
||||
vec.strokes = [strokePaint];
|
||||
}
|
||||
});
|
||||
|
||||
iconComp.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
iconComp.setSharedPluginData('dsb', 'key', 'icon/chevron-right');
|
||||
|
||||
return { iconCompId: iconComp.id };
|
||||
```
|
||||
|
||||
**Then use the returned `iconCompId` as the default value for INSTANCE_SWAP:**
|
||||
```javascript
|
||||
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', ICON_COMP_ID);
|
||||
```
|
||||
|
||||
**Constraining swap options with `preferredValues`:**
|
||||
After adding the INSTANCE_SWAP property, you can optionally limit which components appear in the swap picker:
|
||||
```javascript
|
||||
// Get the property definitions to find the exact key
|
||||
const props = cs.componentPropertyDefinitions;
|
||||
const iconPropKey = Object.keys(props).find(k => k.startsWith('Icon'));
|
||||
|
||||
// Set preferred values (array of component keys or instance IDs)
|
||||
cs.editComponentProperty(iconPropKey, {
|
||||
preferredValues: [
|
||||
{ type: 'COMPONENT', key: chevronRightComp.key },
|
||||
{ type: 'COMPONENT', key: chevronLeftComp.key },
|
||||
{ type: 'COMPONENT', key: closeComp.key },
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
**Icon library tip:** Create all icon components on a dedicated `Icons` page before building any UI components. Then reference their IDs when wiring INSTANCE_SWAP properties.
|
||||
|
||||
### `componentPropertyReferences` mapping
|
||||
|
||||
The `componentPropertyReferences` object maps a node's own property to a component property key:
|
||||
|
||||
| Node property | Component property type | Used for |
|
||||
|---|---|---|
|
||||
| `characters` | TEXT | Editable text content |
|
||||
| `visible` | BOOLEAN | Show/hide toggle |
|
||||
| `mainComponent` | INSTANCE_SWAP | Swap nested instances |
|
||||
|
||||
---
|
||||
|
||||
## 7. `sharedPluginData` Tagging for Idempotency
|
||||
|
||||
Tag EVERY created node immediately after creation. This enables safe cleanup, resumability, and idempotency checks.
|
||||
|
||||
```javascript
|
||||
// After creating any node:
|
||||
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // identifies the build run
|
||||
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created it
|
||||
node.setSharedPluginData('dsb', 'key', KEY); // unique logical key for this entity
|
||||
|
||||
// Reading back:
|
||||
const runId = node.getSharedPluginData('dsb', 'run_id'); // '' if not set
|
||||
const key = node.getSharedPluginData('dsb', 'key');
|
||||
```
|
||||
|
||||
**Key naming convention:** use `/`-separated logical paths that mirror the entity hierarchy:
|
||||
```
|
||||
'component/button/base'
|
||||
'component/button/variant/Medium/Primary/Default'
|
||||
'componentset/button'
|
||||
'doc/button'
|
||||
'page/button'
|
||||
```
|
||||
|
||||
**Idempotency check before creating:** before creating a node, scan the current page for an existing node with the same `key`:
|
||||
|
||||
```javascript
|
||||
const existing = figma.currentPage.findAll(n =>
|
||||
n.getSharedPluginData('dsb', 'key') === 'componentset/button'
|
||||
);
|
||||
if (existing.length > 0) {
|
||||
// Skip creation — already done. Return existing node's ID.
|
||||
return { componentSetId: existing[0].id };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Documentation
|
||||
|
||||
### Page title + description frame
|
||||
|
||||
The documentation frame (see Section 2) should contain:
|
||||
1. Component name as a large title (32px+ Bold)
|
||||
2. 1–3 sentence description of what the component is and when to use it
|
||||
3. Spec notes (sizes, spacing values, accessibility notes)
|
||||
|
||||
### Component `description` property
|
||||
|
||||
Set the description on the ComponentSet — it appears in the Figma properties panel and is exported as documentation:
|
||||
|
||||
```javascript
|
||||
cs.description = 'Buttons allow users to take actions and make choices. Use Primary for the highest-emphasis action on a page.';
|
||||
```
|
||||
|
||||
### `documentationLinks`
|
||||
|
||||
Link to external documentation (Storybook, design spec, tokens reference):
|
||||
|
||||
```javascript
|
||||
cs.documentationLinks = [
|
||||
{ uri: 'https://your-storybook.com/button' }
|
||||
];
|
||||
```
|
||||
|
||||
### Node names and organization
|
||||
|
||||
- ComponentSet: plain component name — `'Button'`
|
||||
- Individual variants: `'Property=Value, Property=Value'` format (match the file's existing casing)
|
||||
- Child nodes: semantic names — `'label'`, `'icon'`, `'container'`, `'state-layer'`
|
||||
- Documentation frames: `'ComponentName / Documentation'`
|
||||
|
||||
---
|
||||
|
||||
## 9. Validation
|
||||
|
||||
Always validate after creating or modifying a component before proceeding to the next one.
|
||||
|
||||
### `get_metadata` structural checks
|
||||
|
||||
After creating the component set, call `get_metadata` on the ComponentSet node and verify:
|
||||
- `variantGroupProperties` lists the expected axes with the correct value arrays
|
||||
- `componentPropertyDefinitions` contains the expected TEXT/BOOLEAN/INSTANCE_SWAP properties
|
||||
- `children.length` equals the expected variant count (e.g., 18 for 3×2×3)
|
||||
- No children are named `'Component 1'` (unnamed components are a sign of a bug)
|
||||
|
||||
### `get_screenshot` — Visual Validation (Critical)
|
||||
|
||||
`get_screenshot` returns an **image** of the specified node. Call it on the **component page node** (not the component set) to see the full page including documentation and grid labels.
|
||||
|
||||
```
|
||||
Tool: get_screenshot
|
||||
Args: { nodeId: "PAGE_NODE_ID", fileKey: "FILE_KEY" }
|
||||
```
|
||||
|
||||
**How to use the screenshot:**
|
||||
|
||||
1. **Display it to the user** — this is the primary purpose. Show the screenshot as part of the user checkpoint: "Here's the Button component. Does it look right?"
|
||||
2. **Analyze it yourself** — if you have vision capabilities, check the visual checklist below. If you don't (text-only agent), fall back to structural validation only via `get_metadata` and describe what you created textually.
|
||||
|
||||
**Visual validation checklist** (check each item when viewing the screenshot):
|
||||
|
||||
| # | Check | What "good" looks like | What "broken" looks like |
|
||||
|---|-------|----------------------|------------------------|
|
||||
| 1 | **Grid layout** | Variants in neat rows and columns with consistent spacing | All variants piled at top-left (0,0 stacking bug) |
|
||||
| 2 | **Color fills** | Components show distinct, correct colors per style variant | All components are black or same color (variable binding failed) |
|
||||
| 3 | **Size differentiation** | Small variants are visibly smaller than Large variants | All variants are the same size (height/padding not bound to variables) |
|
||||
| 4 | **Text readability** | Labels are visible with correct font and color | Text is invisible (white on white), missing, or shows "undefined" |
|
||||
| 5 | **Spacing/padding** | Interior padding visible, components aren't "shrink-wrapped" | Components look cramped or have no visible internal space |
|
||||
| 6 | **State differentiation** | Hover/Pressed variants have visible color differences from Default | All states look identical (state-specific fills not applied) |
|
||||
| 7 | **Disabled state** | Lower opacity or muted colors compared to active states | Disabled looks identical to Default |
|
||||
| 8 | **Documentation frame** | Title + description text visible above or beside the component grid | No documentation, or it overlaps the component set |
|
||||
| 9 | **Grid labels** | Row/column headers visible around the component set (if added) | Labels overlap the grid or are missing |
|
||||
| 10 | **Component set boundary** | Gray background frame wraps all variants with even padding | Frame is too small (variants clipped) or way too large |
|
||||
|
||||
**Screenshot → diagnosis → fix mapping:**
|
||||
|
||||
| Screenshot shows | Diagnosis | Fix script |
|
||||
|-----------------|-----------|------------|
|
||||
| All variants stacked top-left | Grid layout wasn't applied after `combineAsVariants` | Re-run the grid layout script (§5) |
|
||||
| Everything black/same color | Variable bindings failed or variables don't have values for the active mode | Re-run variable binding, check mode values |
|
||||
| No text visible | Font wasn't loaded, or text fill is same color as background | Check `loadFontAsync` was called; bind text fill to `color/text/*` variable |
|
||||
| Variants all same size | Padding/height not bound to size variables | Re-run `bindVariablesToComponent` with size-specific tokens |
|
||||
| Component set frame tiny | `resizeWithoutConstraints` wasn't called or used wrong dimensions | Re-calculate bounds from children and resize |
|
||||
| Doc frame overlaps components | Component set positioned at same x,y as doc frame | Move component set: `cs.x = docFrame.x + docFrame.width + 60` |
|
||||
|
||||
**When visual analysis isn't available:**
|
||||
If your model can't process images (text-only mode), validate structurally instead:
|
||||
1. Call `get_metadata` on the component set — verify child count, property definitions, variant names
|
||||
2. Run an `use_figma` that samples key properties:
|
||||
```javascript
|
||||
const cs = await figma.getNodeByIdAsync(CS_ID);
|
||||
const sample = cs.children.slice(0, 3).map(c => ({
|
||||
name: c.name,
|
||||
width: c.width, height: c.height,
|
||||
x: c.x, y: c.y,
|
||||
fills: c.fills?.map(f => f.type === 'SOLID' ?
|
||||
{ r: f.color.r.toFixed(2), g: f.color.g.toFixed(2), b: f.color.b.toFixed(2), boundVar: f.boundVariables?.color?.id } : f.type
|
||||
),
|
||||
}));
|
||||
return { sampleVariants: sample, totalChildren: cs.children.length };
|
||||
```
|
||||
This gives you positions (grid working?), dimensions (size differentiation?), and fill info (bindings working?) without needing vision.
|
||||
|
||||
**When to take a screenshot:**
|
||||
- After EVERY completed component (mandatory — part of the user checkpoint)
|
||||
- After creating the foundations documentation page
|
||||
- After final QA (screenshot every page)
|
||||
- Do NOT screenshot after every intermediate step (wastes tool calls)
|
||||
|
||||
### Common issues
|
||||
|
||||
| Symptom | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| All variants stacked at (0,0) | `combineAsVariants` was called but children were never repositioned | Re-run grid layout script |
|
||||
| Variants show wrong colors | Variable bindings applied after `combineAsVariants` instead of before | Rebind on component set children |
|
||||
| Variant count wrong | Clone loop indexing error | Print `components.map(c => c.name)` before combining |
|
||||
| BOOLEAN property has no effect | `componentPropertyReferences` was set on the component set frame, not on the child node | Find the actual child node and set references there |
|
||||
| INSTANCE_SWAP shows no swap option | Default value was not a valid component ID | Pass a real existing component ID as `defaultValue` |
|
||||
| `combineAsVariants` throws | At least one node in the array is not a `ComponentNode` | Filter array: `nodes.filter(n => n.type === 'COMPONENT')` |
|
||||
| `addComponentProperty` returns unexpected key | Expected — the key gets a `#id:id` suffix | Save the returned value immediately: `const key = cs.addComponentProperty(...)` |
|
||||
|
||||
---
|
||||
|
||||
## 10. Complete Worked Example: Button Component
|
||||
|
||||
This shows the full sequence of `use_figma` calls for a Button component, including state passing between calls. Replace `RUN_ID` and variable IDs with your actual values from the state ledger.
|
||||
|
||||
### Call 1: Create the component page
|
||||
|
||||
**Goal:** Create (or find) the Button page.
|
||||
**State input:** None
|
||||
**State output:** `{ pageId }`
|
||||
|
||||
```javascript
|
||||
let page = figma.root.children.find(p => p.name === 'Button');
|
||||
if (!page) { page = figma.createPage(); page.name = 'Button'; }
|
||||
page.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
|
||||
page.setSharedPluginData('dsb', 'key', 'page/button');
|
||||
return { pageId: page.id };
|
||||
```
|
||||
|
||||
### Call 2: Create documentation frame
|
||||
|
||||
**Goal:** Add title + description frame.
|
||||
**State input:** `{ pageId }`
|
||||
**State output:** `{ docFrameId }`
|
||||
|
||||
```javascript
|
||||
const PAGE_ID = 'PAGE_ID_FROM_STATE';
|
||||
const page = await figma.getNodeByIdAsync(PAGE_ID);
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
// Idempotency check
|
||||
const existing = page.findAll(n => n.getSharedPluginData('dsb', 'key') === 'doc/button');
|
||||
if (existing.length > 0) {
|
||||
return { docFrameId: existing[0].id };
|
||||
}
|
||||
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
const docFrame = figma.createFrame();
|
||||
docFrame.name = 'Button / Documentation';
|
||||
docFrame.x = 40; docFrame.y = 40;
|
||||
docFrame.layoutMode = 'VERTICAL';
|
||||
docFrame.primaryAxisSizingMode = 'AUTO';
|
||||
docFrame.counterAxisSizingMode = 'FIXED';
|
||||
docFrame.resize(560, 100);
|
||||
docFrame.paddingTop = 40; docFrame.paddingBottom = 40;
|
||||
docFrame.paddingLeft = 40; docFrame.paddingRight = 40;
|
||||
docFrame.itemSpacing = 16;
|
||||
docFrame.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
||||
|
||||
const title = figma.createText();
|
||||
title.fontName = { family: 'Inter', style: 'Bold' };
|
||||
title.fontSize = 32;
|
||||
title.characters = 'Button';
|
||||
docFrame.appendChild(title);
|
||||
|
||||
const desc = figma.createText();
|
||||
desc.fontName = { family: 'Inter', style: 'Regular' };
|
||||
desc.fontSize = 14;
|
||||
desc.characters = 'Buttons allow users to take actions with a single tap. Use Primary for the highest-emphasis action on a page, Secondary for supporting actions.';
|
||||
desc.layoutSizingHorizontal = 'FILL';
|
||||
docFrame.appendChild(desc);
|
||||
|
||||
docFrame.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001');
|
||||
docFrame.setSharedPluginData('dsb', 'key', 'doc/button');
|
||||
|
||||
return { docFrameId: docFrame.id };
|
||||
```
|
||||
|
||||
### Call 3: Create base component
|
||||
|
||||
**Goal:** Create the base component with auto-layout and all variable bindings.
|
||||
**State input:** `{ pageId }` + variable IDs from Phase 1
|
||||
**State output:** `{ baseCompId }`
|
||||
|
||||
*(See Section 3 for full code — substituting the actual variable IDs from the state ledger.)*
|
||||
|
||||
### Call 4: Create all variants
|
||||
|
||||
**Goal:** Clone base and produce all 18 variants (3 Size × 2 Style × 3 State).
|
||||
**State input:** `{ pageId, baseCompId }` + variable IDs
|
||||
**State output:** `{ variantIds: ['id1', 'id2', ..., 'id18'] }`
|
||||
|
||||
```javascript
|
||||
const RUN_ID = 'ds-build-2024-001';
|
||||
const BASE_ID = 'BASE_COMP_ID_FROM_STATE';
|
||||
const PAGE_ID = 'PAGE_ID_FROM_STATE';
|
||||
// Variable IDs from state ledger:
|
||||
const VAR = {
|
||||
bg_primary: 'VAR_ID_1',
|
||||
text_primary: 'VAR_ID_2',
|
||||
bg_secondary: 'VAR_ID_3',
|
||||
text_secondary: 'VAR_ID_4',
|
||||
bg_disabled: 'VAR_ID_5',
|
||||
text_disabled: 'VAR_ID_6',
|
||||
padding_sm: 'VAR_ID_7',
|
||||
padding_md: 'VAR_ID_8',
|
||||
padding_lg: 'VAR_ID_9',
|
||||
};
|
||||
|
||||
const page = await figma.getNodeByIdAsync(PAGE_ID);
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
const base = await figma.getNodeByIdAsync(BASE_ID);
|
||||
|
||||
// Load all variables
|
||||
const vars = {};
|
||||
for (const [k, v] of Object.entries(VAR)) {
|
||||
vars[k] = await figma.variables.getVariableByIdAsync(v);
|
||||
}
|
||||
|
||||
const axes = {
|
||||
Size: ['Small', 'Medium', 'Large'],
|
||||
Style: ['Primary', 'Secondary'],
|
||||
State: ['Default', 'Hover', 'Disabled'],
|
||||
};
|
||||
const paddingMap = { Small: vars.padding_sm, Medium: vars.padding_md, Large: vars.padding_lg };
|
||||
|
||||
const components = [];
|
||||
for (const size of axes.Size) {
|
||||
for (const style of axes.Style) {
|
||||
for (const state of axes.State) {
|
||||
const clone = base.clone();
|
||||
clone.name = `Size=${size}, Style=${style}, State=${state}`;
|
||||
|
||||
clone.setBoundVariable('paddingTop', paddingMap[size]);
|
||||
clone.setBoundVariable('paddingBottom', paddingMap[size]);
|
||||
clone.setBoundVariable('paddingLeft', paddingMap[size]);
|
||||
clone.setBoundVariable('paddingRight', paddingMap[size]);
|
||||
|
||||
const isDisabled = state === 'Disabled';
|
||||
const bgV = isDisabled ? vars.bg_disabled : (style === 'Primary' ? vars.bg_primary : vars.bg_secondary);
|
||||
const txV = isDisabled ? vars.text_disabled : (style === 'Primary' ? vars.text_primary : vars.text_secondary);
|
||||
|
||||
clone.fills = [figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', bgV
|
||||
)];
|
||||
|
||||
const labelNode = clone.findOne(n => n.name === 'label');
|
||||
labelNode.fills = [figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }, 'color', txV
|
||||
)];
|
||||
|
||||
clone.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
clone.setSharedPluginData('dsb', 'key', `component/button/variant/${size}/${style}/${state}`);
|
||||
components.push(clone);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { variantIds: components.map(c => c.id) };
|
||||
```
|
||||
|
||||
### Call 5: combineAsVariants + grid layout
|
||||
|
||||
**Goal:** Combine all 18 variants into a ComponentSet and lay them out in a grid.
|
||||
**State input:** `{ pageId, variantIds }` (18 IDs)
|
||||
**State output:** `{ componentSetId }`
|
||||
|
||||
*(See Section 5 for full code.)*
|
||||
|
||||
### Call 6: Add component properties
|
||||
|
||||
**Goal:** Add TEXT, BOOLEAN, INSTANCE_SWAP properties and wire them to child nodes.
|
||||
**State input:** `{ pageId, componentSetId }`
|
||||
**State output:** `{ componentSetId, properties: { labelKey, showIconKey, iconKey } }`
|
||||
|
||||
```javascript
|
||||
const CS_ID = 'CS_ID_FROM_STATE';
|
||||
const DEFAULT_ICON_ID = 'ICON_COMP_ID_FROM_STATE';
|
||||
const page = figma.root.children.find(p => p.name === 'Button');
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
const cs = await figma.getNodeByIdAsync(CS_ID);
|
||||
cs.description = 'Buttons allow users to take actions and make choices with a single tap.';
|
||||
cs.documentationLinks = [{ uri: 'https://your-storybook.com/button' }];
|
||||
|
||||
// Add properties — save returned keys
|
||||
const labelKey = cs.addComponentProperty('Label', 'TEXT', 'Button');
|
||||
const showIconKey = cs.addComponentProperty('Show Icon', 'BOOLEAN', true);
|
||||
const iconKey = cs.addComponentProperty('Icon', 'INSTANCE_SWAP', DEFAULT_ICON_ID);
|
||||
|
||||
// Wire to children
|
||||
for (const child of cs.children) {
|
||||
const labelNode = child.findOne(n => n.name === 'label');
|
||||
if (labelNode) labelNode.componentPropertyReferences = { characters: labelKey };
|
||||
|
||||
const iconNode = child.findOne(n => n.name === 'icon');
|
||||
if (iconNode) {
|
||||
iconNode.componentPropertyReferences = {
|
||||
visible: showIconKey,
|
||||
...(iconNode.type === 'INSTANCE' ? { mainComponent: iconKey } : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
componentSetId: cs.id,
|
||||
properties: { labelKey, showIconKey, iconKey },
|
||||
};
|
||||
```
|
||||
|
||||
### Call 7: Validate with get_metadata
|
||||
|
||||
**Goal:** Structural check — variant count, properties, axes.
|
||||
**Action:** Call `get_metadata` on the ComponentSet node ID (from state). Verify in the result:
|
||||
- `children.length === 18`
|
||||
- `variantGroupProperties` has `Size`, `Style`, `State` keys with correct value arrays
|
||||
- `componentPropertyDefinitions` has `Label`, `Show Icon`, `Icon` entries
|
||||
|
||||
### Call 8: Validate with get_screenshot
|
||||
|
||||
**Goal:** Visual check — layout, colors, text.
|
||||
**Action:** Call `get_screenshot` on the Button page. Inspect the screenshot. If variants are stacked, re-run Call 5. If colors look wrong, inspect variable bindings.
|
||||
|
||||
### Checkpoint
|
||||
|
||||
After Call 8: show the screenshot to the user. Ask: "Here's the Button component with 18 variants. Does this look correct?" Do not proceed to the next component until the user approves.
|
||||
+494
@@ -0,0 +1,494 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Discovery Phase Reference
|
||||
|
||||
This document covers everything needed for Phase 0 of a design system build: analyzing the codebase for tokens, inspecting the Figma file for existing conventions, searching subscribed libraries, building the plan, and resolving conflicts before any write operations begin.
|
||||
|
||||
---
|
||||
|
||||
## 1. Codebase Analysis — Finding Token Sources
|
||||
|
||||
### Search Priority Order
|
||||
|
||||
Look for token sources in this order. Stop as soon as you find a definitive source; multiple formats can coexist:
|
||||
|
||||
1. Design token files: `*.tokens.json`, `tokens/*.json`, `src/tokens/**`
|
||||
2. CSS variable files: `variables.css`, `tokens.css`, `theme.css`, `global.css`
|
||||
3. Tailwind config: `tailwind.config.js`, `tailwind.config.ts`
|
||||
4. CSS-in-JS theme objects: `theme.ts`, `createTheme`, `ThemeProvider`
|
||||
5. Platform-specific: iOS Asset catalogs (`.xcassets`), Android `themes.xml`, `colors.xml`
|
||||
|
||||
### CSS Custom Properties (Most Common for Web)
|
||||
|
||||
**What to search for:**
|
||||
|
||||
```
|
||||
:root { ... }
|
||||
@theme { ... } ← Tailwind v4
|
||||
--color-*, --spacing-*, --radius-*, --shadow-*, --font-*
|
||||
```
|
||||
|
||||
**Pattern:** `/--[\w-]+:\s*[^;]+/g`
|
||||
|
||||
**Common file locations:** `src/styles/tokens.css`, `src/styles/variables.css`, `src/theme/*.css`
|
||||
|
||||
**Extraction and naming translation:**
|
||||
|
||||
| CSS Property | Figma Variable Name | Figma Type | WEB Code Syntax |
|
||||
|---|---|---|---|
|
||||
| `--color-bg-primary: #fff` | `color/bg/primary` | COLOR | `var(--color-bg-primary)` |
|
||||
| `--color-text-secondary: #757575` | `color/text/secondary` | COLOR | `var(--color-text-secondary)` |
|
||||
| `--spacing-sm: 8px` | `spacing/sm` | FLOAT | `var(--spacing-sm)` |
|
||||
| `--radius-md: 8px` | `radius/md` | FLOAT | `var(--radius-md)` |
|
||||
| `--font-body: "Inter"` | `typography/body/font-family` | STRING | `var(--font-body)` |
|
||||
|
||||
**Naming rule:** Replace hyphens with slashes at category boundaries. Keep hyphens within the final path segment: `--color-bg-primary` → `color/bg/primary`, but `--color-bg-primary-hover` → `color/bg/primary-hover`.
|
||||
|
||||
**Always store the original CSS variable name** as the code syntax value — never derive it from the Figma variable name. If the codebase uses `--sds-color-background-brand-default`, use exactly that string in `setVariableCodeSyntax('WEB', '--sds-color-background-brand-default')`.
|
||||
|
||||
### Tailwind Configuration
|
||||
|
||||
**What to look for in `tailwind.config.js` or `tailwind.config.ts`:**
|
||||
|
||||
```javascript
|
||||
// theme.extend.colors → Figma color variables
|
||||
{ primary: { DEFAULT: '#3366FF', light: '#6699FF', dark: '#0033CC' } }
|
||||
// → color/primary/default, color/primary/light, color/primary/dark
|
||||
|
||||
// theme.extend.spacing → Figma FLOAT variables
|
||||
{ 'xs': '4px', 'sm': '8px', 'md': '16px' }
|
||||
// → spacing/xs = 4, spacing/sm = 8, spacing/md = 16
|
||||
|
||||
// theme.extend.borderRadius → Figma FLOAT variables
|
||||
{ 'sm': '4px', 'md': '8px', 'lg': '16px' }
|
||||
// → radius/sm = 4, radius/md = 8, radius/lg = 16
|
||||
```
|
||||
|
||||
Tailwind utility class names (`bg-blue-500`, `p-4`) are not tokens — extract values from the config object, not the class names.
|
||||
|
||||
### Design Token Community Group (DTCG) Format
|
||||
|
||||
**Pattern:** `*.tokens.json` or `tokens/*.json`. Find source files, not generated outputs from Style Dictionary or Tokens Studio.
|
||||
|
||||
```json
|
||||
{
|
||||
"color": {
|
||||
"bg": {
|
||||
"primary": { "$type": "color", "$value": "#ffffff" },
|
||||
"secondary": { "$type": "color", "$value": "#f5f5f5" }
|
||||
}
|
||||
},
|
||||
"spacing": {
|
||||
"sm": { "$type": "dimension", "$value": "8px" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Nested keys map to slash-separated Figma names: `color.bg.primary` → `color/bg/primary`.
|
||||
|
||||
### CSS-in-JS / Theme Objects
|
||||
|
||||
**What to search for:** `createTheme`, `ThemeProvider`, `theme = {}`, styled-components, Emotion, Stitches, vanilla-extract
|
||||
|
||||
```typescript
|
||||
// theme.colors.bg.primary → Figma variable: color/bg/primary
|
||||
// theme.spacing.sm → Figma variable: spacing/sm
|
||||
// Multiple theme objects (lightTheme, darkTheme) → modes in the same collection
|
||||
```
|
||||
|
||||
### iOS Token Sources
|
||||
|
||||
```swift
|
||||
// Asset catalog colors in .xcassets/Colors.xcassets
|
||||
// extension Color { static let bgPrimary = Color("bg-primary") }
|
||||
// Look for traitCollection.userInterfaceStyle for dark mode detection
|
||||
```
|
||||
|
||||
### Android Token Sources
|
||||
|
||||
```kotlin
|
||||
// res/values/colors.xml <color name="primary">#3366FF</color>
|
||||
// res/values-night/colors.xml (dark mode overrides)
|
||||
// MaterialTheme.colorScheme.primary in Compose
|
||||
// val Primary = Color(0xFF3366FF)
|
||||
```
|
||||
|
||||
### Detecting Dark Mode
|
||||
|
||||
| Platform | Signal |
|
||||
|---|---|
|
||||
| Web (CSS) | `@media (prefers-color-scheme: dark)`, `.dark { }`, `[data-theme="dark"]` |
|
||||
| Web (Tailwind) | `darkMode: 'class'` or `darkMode: 'media'` in config |
|
||||
| Web (JS) | Separate `darkTheme` object alongside `lightTheme` |
|
||||
| iOS | `Color(uiColor:)` with `traitCollection.userInterfaceStyle`, dual-appearance asset catalog |
|
||||
| Android | `themes.xml` with `Theme.*.Night`, `isSystemInDarkTheme()` in Compose, `values-night/` folder |
|
||||
|
||||
**Figma mapping:** If dark mode exists → minimum 2 modes (Light/Dark) in the semantic color collection. Primitive collections stay single-mode.
|
||||
|
||||
### Shadow/Elevation Extraction
|
||||
|
||||
Shadows cannot be Figma variables — they become **Effect Styles**.
|
||||
|
||||
```css
|
||||
/* Look for: box-shadow, --shadow-* */
|
||||
--shadow-sm: 0 1px 2px rgba(0,0,0,0.05);
|
||||
--shadow-md: 0 4px 6px -1px rgba(0,0,0,0.10);
|
||||
--shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.10);
|
||||
```
|
||||
|
||||
CSS `0 4px 6px -1px rgba(0,0,0,0.1)` → Figma:
|
||||
```
|
||||
{ type: "DROP_SHADOW", offset: {x:0, y:4}, radius: 6, spread: -1, color: {r:0, g:0, b:0, a:0.1} }
|
||||
```
|
||||
|
||||
### Typography Extraction
|
||||
|
||||
| Code token | Maps to |
|
||||
|---|---|
|
||||
| `font-size: 16px` | FLOAT variable (scope `FONT_SIZE`) or Text Style `fontSize` |
|
||||
| `line-height: 1.5` | Text Style `lineHeight: {value: 24, unit: "PIXELS"}` |
|
||||
| `font-weight: 600` | Text Style `fontName: {family: "Inter", style: "Semi Bold"}` |
|
||||
| `letter-spacing: -0.02em` | Text Style `letterSpacing: {value: -2, unit: "PERCENT"}` |
|
||||
| `font-family: "Inter"` | STRING variable (scope `FONT_FAMILY`) or Text Style `fontName.family` |
|
||||
|
||||
Composite text styles (all properties bundled) → Figma Text Styles. Individual properties → Figma variables with appropriate scopes.
|
||||
|
||||
### Component Extraction
|
||||
|
||||
For each component, extract:
|
||||
|
||||
1. **Name** → Figma component set name
|
||||
2. **Union-type props** → VARIANT properties
|
||||
3. **String content props** → TEXT properties
|
||||
4. **Boolean props** → BOOLEAN properties (and VARIANT State when combined with interaction states)
|
||||
5. **Child/slot props** → INSTANCE_SWAP properties
|
||||
|
||||
```typescript
|
||||
// React example:
|
||||
interface ButtonProps {
|
||||
size: 'sm' | 'md' | 'lg'; // → VARIANT: Size = sm|md|lg
|
||||
variant: 'primary' | 'secondary'; // → VARIANT: Style = primary|secondary
|
||||
disabled?: boolean; // → VARIANT: State (combine: default|hover|pressed|disabled)
|
||||
label: string; // → TEXT: Label
|
||||
icon?: ReactNode; // → INSTANCE_SWAP: Icon + BOOLEAN: Show Icon
|
||||
}
|
||||
// → Component Set "Button", variant count: 3 sizes × 2 styles × 4 states = 24
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Figma File Inspection
|
||||
|
||||
Run these `use_figma` snippets at the start of every build. All are read-only and safe to run before any user checkpoint.
|
||||
|
||||
### List All Pages
|
||||
|
||||
```javascript
|
||||
const pages = figma.root.children.map((p, i) => ({
|
||||
index: i,
|
||||
name: p.name,
|
||||
id: p.id,
|
||||
childCount: p.children.length
|
||||
}));
|
||||
return { pages };
|
||||
```
|
||||
|
||||
Interpret: note page names for naming convention (are they PascalCase? sentence case?), count separator pages (`---`), identify existing component pages vs foundations pages.
|
||||
|
||||
### List Variable Collections With Modes
|
||||
|
||||
```javascript
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const result = collections.map(c => ({
|
||||
id: c.id,
|
||||
name: c.name,
|
||||
modes: c.modes, // [{modeId, name}, ...]
|
||||
variableCount: c.variableIds.length,
|
||||
defaultModeId: c.defaultModeId
|
||||
}));
|
||||
return { collections: result };
|
||||
```
|
||||
|
||||
Interpret: identify existing primitive/semantic split, note mode names (do they use "Light/Dark" or "SDS Light/SDS Dark"?), count variables to understand scope.
|
||||
|
||||
### List Variables in a Collection (with names, types, scopes, and sample values)
|
||||
|
||||
```javascript
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const targetName = "Color"; // change to the collection you want to inspect
|
||||
const coll = collections.find(c => c.name === targetName);
|
||||
if (!coll) { return { error: `Collection "${targetName}" not found` }; }
|
||||
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const vars = allVars.filter(v => v.variableCollectionId === coll.id);
|
||||
|
||||
const result = vars.map(v => ({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
resolvedType: v.resolvedType,
|
||||
scopes: v.scopes,
|
||||
codeSyntax: v.codeSyntax,
|
||||
// First mode value only, for a sample
|
||||
sampleValue: v.valuesByMode[coll.defaultModeId]
|
||||
}));
|
||||
|
||||
return { collection: coll.name, variableCount: result.length, variables: result };
|
||||
```
|
||||
|
||||
Interpret: check if variables use `ALL_SCOPES` (bad), check naming convention (slash-separated hierarchy?), check if code syntax is set, identify alias chains.
|
||||
|
||||
### List Component Sets with Properties
|
||||
|
||||
```javascript
|
||||
await figma.setCurrentPageAsync(figma.currentPage); // ensures page context
|
||||
const componentSets = figma.currentPage.findAll(n => n.type === 'COMPONENT_SET');
|
||||
const result = componentSets.map(cs => ({
|
||||
id: cs.id,
|
||||
name: cs.name,
|
||||
variantCount: cs.children.length,
|
||||
properties: Object.entries(cs.componentPropertyDefinitions).map(([key, def]) => ({
|
||||
name: key,
|
||||
type: def.type,
|
||||
variantOptions: def.variantOptions || null,
|
||||
defaultValue: def.defaultValue
|
||||
}))
|
||||
}));
|
||||
return { componentSets: result, count: result.length };
|
||||
```
|
||||
|
||||
Note: to search ALL pages, iterate `figma.root.children` and `setCurrentPageAsync` for each.
|
||||
|
||||
### List All Styles
|
||||
|
||||
```javascript
|
||||
const [textStyles, effectStyles, paintStyles] = await Promise.all([
|
||||
figma.getLocalTextStylesAsync(),
|
||||
figma.getLocalEffectStylesAsync(),
|
||||
figma.getLocalPaintStylesAsync()
|
||||
]);
|
||||
|
||||
return {
|
||||
textStyles: textStyles.map(s => ({ id: s.id, name: s.name, fontSize: s.fontSize, fontName: s.fontName })),
|
||||
effectStyles: effectStyles.map(s => ({ id: s.id, name: s.name, effectCount: s.effects.length })),
|
||||
paintStyles: paintStyles.map(s => ({ id: s.id, name: s.name })),
|
||||
counts: { text: textStyles.length, effect: effectStyles.length, paint: paintStyles.length }
|
||||
};
|
||||
```
|
||||
|
||||
### Check Naming Conventions on an Existing Component
|
||||
|
||||
```javascript
|
||||
// Replace with the node ID of an existing component to analyze
|
||||
const node = await figma.getNodeByIdAsync("YOUR_NODE_ID");
|
||||
if (!node) { return { error: "Node not found" }; }
|
||||
|
||||
// Check fills for variable bindings
|
||||
const fillInfo = [];
|
||||
if ('fills' in node && Array.isArray(node.fills)) {
|
||||
for (const fill of node.fills) {
|
||||
if (fill.type === 'SOLID' && fill.boundVariables?.color) {
|
||||
fillInfo.push({ type: 'variable_alias', id: fill.boundVariables.color.id });
|
||||
} else if (fill.type === 'SOLID') {
|
||||
fillInfo.push({ type: 'hardcoded', r: fill.color.r, g: fill.color.g, b: fill.color.b });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: node.name,
|
||||
type: node.type,
|
||||
fills: fillInfo,
|
||||
sharedPluginData: node.getSharedPluginData('dsb', 'key') || null
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Using search_design_system
|
||||
|
||||
### What It Searches
|
||||
|
||||
`search_design_system` runs three parallel searches against **subscribed design libraries** for the given file:
|
||||
|
||||
1. **Components** — published library components, searched by name/description via a recommendation engine (relevance-ranked, not exact match)
|
||||
2. **Variables** — design tokens (colors, spacing, etc.) across subscribed libraries
|
||||
3. **Styles** — paint styles, text styles, and effect styles
|
||||
|
||||
Only libraries the file has subscribed to are searched. If results are empty, the file may not be subscribed to any design system libraries.
|
||||
|
||||
### Input
|
||||
|
||||
```
|
||||
search_design_system({
|
||||
query: "button", // required — text query
|
||||
fileKey: "abc123", // required — your file key
|
||||
includeComponents: true, // default true
|
||||
includeVariables: true, // default true
|
||||
includeStyles: true // default true
|
||||
})
|
||||
```
|
||||
|
||||
### What It Returns
|
||||
|
||||
```json
|
||||
{
|
||||
"components": [
|
||||
{
|
||||
"name": "Button",
|
||||
"libraryName": "Design System",
|
||||
"assetType": "component_set",
|
||||
"componentKey": "abc123def",
|
||||
"description": "Primary action button"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"name": "colors/primary/500",
|
||||
"variableType": "COLOR",
|
||||
"variableSetKey": "set1key",
|
||||
"key": "var1key",
|
||||
"scopes": ["FRAME_FILL", "SHAPE_FILL"],
|
||||
"variableCollectionName": "Colors"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
{
|
||||
"name": "Heading/H1",
|
||||
"styleType": "TEXT",
|
||||
"key": "style1key"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### How to Interpret Results
|
||||
|
||||
**Components:** The `componentKey` can be used in `use_figma` to import the component:
|
||||
```javascript
|
||||
const component = await figma.importComponentByKeyAsync("abc123def");
|
||||
// or for component sets:
|
||||
const componentSet = await figma.importComponentSetByKeyAsync("abc123def");
|
||||
```
|
||||
|
||||
**Variables:** The `variableSetKey` is the collection key. The `key` is the variable key. Use these to understand what naming conventions are in use, and what tokens are available to alias from.
|
||||
|
||||
**Styles:** The `key` is usable with `figma.importStyleByKeyAsync(key)` to import into the current file.
|
||||
|
||||
### When to Search
|
||||
|
||||
- **Phase 0, step 0c**: Search broadly (`query: "button"`, `query: "color"`, `query: "spacing"`) before planning anything. This establishes the reuse baseline.
|
||||
- **Immediately before each component creation**: Search for the specific component name before writing any `use_figma` creation code.
|
||||
|
||||
**Reuse decision:**
|
||||
|
||||
| Condition | Decision |
|
||||
|---|---|
|
||||
| Found component with matching variant API, same token model | Import and reuse |
|
||||
| Found component but wrong variant properties or hardcoded values | Rebuild |
|
||||
| Found component that matches visually but API is incompatible | Wrap: nest as instance inside a new wrapper component |
|
||||
|
||||
---
|
||||
|
||||
## 4. Building the Plan
|
||||
|
||||
After codebase analysis and Figma inspection, produce a mapping table and present it to the user.
|
||||
|
||||
### Token → Variable Mapping Table
|
||||
|
||||
For each token found in code, record:
|
||||
|
||||
| Code Token | CSS Name | Raw Value | Figma Collection | Figma Variable Name | Figma Type | Mode(s) |
|
||||
|---|---|---|---|---|---|---|
|
||||
| `theme.colors.blue[500]` | `--color-blue-500` | `#3B82F6` | Primitives | `blue/500` | COLOR | Value |
|
||||
| `theme.colors.bg.primary` | `--color-bg-primary` | (light: blue/50, dark: gray/900) | Color | `color/bg/primary` | COLOR | Light, Dark |
|
||||
| `theme.spacing.sm` | `--spacing-sm` | `8px` | Spacing | `spacing/sm` | FLOAT | Value |
|
||||
| `theme.radii.md` | `--radius-md` | `8px` | Spacing | `radius/md` | FLOAT | Value |
|
||||
| `theme.shadows.md` | `--shadow-md` | `0 4px 6px rgba(0,0,0,0.1)` | — | — | Effect Style | — |
|
||||
|
||||
### Component → Component Set Mapping Table
|
||||
|
||||
| Code Component | Props → Variant Axes | Variant Count | Figma Page | Reuse? |
|
||||
|---|---|---|---|---|
|
||||
| `Button` | size (sm/md/lg) × variant (primary/secondary) × state (default/hover/disabled) | 18 | Buttons | Search first |
|
||||
| `Avatar` | size (sm/md/lg) × type (image/initials/icon) | 9 | Avatars | Search first |
|
||||
|
||||
### Gap Identification
|
||||
|
||||
Compare what was found in code vs what already exists in Figma:
|
||||
|
||||
- **New:** tokens or components that exist in code but not in Figma → create
|
||||
- **Existing:** tokens or components already in Figma with matching names → verify scope/code-syntax, skip or update
|
||||
- **Conflict:** same name, different value → escalate to user (see section 5)
|
||||
- **Figma-only:** exists in Figma but not in code → flag for user, likely skip
|
||||
|
||||
### User-Facing Checkpoint Message Template
|
||||
|
||||
Present this message before proceeding. Never begin Phase 1 without explicit user approval.
|
||||
|
||||
```
|
||||
Here's what I found and what I plan to build:
|
||||
|
||||
CODEBASE ANALYSIS
|
||||
Colors: {N} primitives ({families}), {M} semantic tokens ({light/dark if applicable})
|
||||
Spacing: {N} tokens ({range})
|
||||
Typography: {N} text styles, {M} individual scale tokens
|
||||
Shadows: {N} levels → will become Effect Styles
|
||||
Components: {list of component names}
|
||||
|
||||
EXISTING FIGMA FILE
|
||||
Collections: {N} existing collections
|
||||
Variables: {M} existing variables
|
||||
Styles: {K} text, {L} effect, {J} paint styles
|
||||
Components: {list}
|
||||
|
||||
PLAN
|
||||
New collections: {list with mode counts}
|
||||
New variables: ~{N} ({breakdown by collection})
|
||||
New styles: {N} text, {M} effect
|
||||
New components: {list}
|
||||
Libraries to search before each component: {list}
|
||||
|
||||
GAPS / CONFLICTS NEEDING DECISIONS
|
||||
⚠ {conflict description} — Code says X, Figma already has Y. Which wins?
|
||||
|
||||
WHAT I WON'T BUILD (and why)
|
||||
- {item}: already exists in Figma with matching conventions
|
||||
- {item}: not supported as a Figma variable (e.g. z-index, animation timing)
|
||||
|
||||
Shall I proceed?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Conflict Resolution — When Code and Figma Disagree
|
||||
|
||||
When the same token/component exists in both code and Figma but with different values, names, or structures, **always ask the user**. Never silently pick one.
|
||||
|
||||
### Decision Framework
|
||||
|
||||
| Scenario | Ask the user |
|
||||
|---|---|
|
||||
| Same CSS name, different hex value (e.g., `--color-accent` is `#3366FF` in code but `#5B7FFF` in Figma) | "Code says `#3366FF`, Figma currently has `#5B7FFF` for `color/accent/default`. Which is correct?" |
|
||||
| Same component name, different variant axes (code has `size: sm/md/lg`, Figma has `Size: Small/Large`) | "Code uses 3 sizes (sm/md/lg) but Figma has 2 (Small/Large). Should I add Medium, or rename to match code?" |
|
||||
| Code has a semantic token with no primitive layer; Figma already has a fully-layered system | "The codebase uses a flat single-layer token model. The Figma file uses a primitive/semantic split. Should I match the Figma architecture or the code architecture?" |
|
||||
| Figma variable exists but has `ALL_SCOPES` (incorrect per best practice) | "I found `color/bg/primary` already exists but it uses ALL_SCOPES. I recommend changing it to `FRAME_FILL, SHAPE_FILL`. May I update the scope?" |
|
||||
| Code uses camelCase (`backgroundColor`), Figma uses slash-separated (`color/bg/default`) | "The codebase uses camelCase naming. The Figma file uses slash-separated hierarchy. For new variables, should I use slash-separated (Figma standard) and map via code syntax?" |
|
||||
|
||||
### Code Wins
|
||||
|
||||
Default to code as the source of truth for:
|
||||
- Hex values (code is the live production value)
|
||||
- Token naming (the CSS variable names become code syntax)
|
||||
- Mode values (light/dark split comes from code)
|
||||
|
||||
### Figma Wins
|
||||
|
||||
Default to Figma as the source of truth for:
|
||||
- Collection architecture (if a well-structured system already exists, extend it rather than replace it)
|
||||
- Variable naming hierarchy (if designers are already using the system with specific names)
|
||||
- Page structure (match the existing page organization pattern)
|
||||
|
||||
### Neither: Negotiate
|
||||
|
||||
When neither is clearly correct, propose a resolution and ask:
|
||||
> "I'd suggest [option]. This way both the code token name and the Figma naming convention are preserved. Does that work?"
|
||||
+834
@@ -0,0 +1,834 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Documentation Creation Reference
|
||||
|
||||
This reference covers Phase 2 of the design system build: the cover page, foundations documentation page (color swatches, type specimens, spacing bars, shadow cards, radius demo), page layout dimensions, and inline component documentation. Every code block is complete `use_figma`-ready JavaScript (helper-function form — meant to be embedded in a larger script that uses `return` to send results back).
|
||||
|
||||
---
|
||||
|
||||
## 1. Cover Page
|
||||
|
||||
The cover page is always the first page in the file. It is a branded title card that sets context for anyone opening the file.
|
||||
|
||||
### What to include
|
||||
|
||||
- File/system name as a large heading (48–72px)
|
||||
- Version string or date
|
||||
- Brief tagline (1 sentence)
|
||||
- Optional: color block background using the primary brand color variable
|
||||
|
||||
### Cover page dimensions
|
||||
|
||||
The cover frame should be **1440 × 900px** — this matches the default Figma canvas and looks correct in the page thumbnail.
|
||||
|
||||
### use_figma for cover page
|
||||
|
||||
```javascript
|
||||
async function createCoverPage(systemName, tagline, version, primaryColorVar) {
|
||||
// primaryColorVar: a Figma Variable object for the brand primary fill
|
||||
const page = figma.createPage();
|
||||
page.name = 'Cover';
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
|
||||
const frame = figma.createFrame();
|
||||
frame.name = 'Cover';
|
||||
frame.resize(1440, 900);
|
||||
frame.x = 0;
|
||||
frame.y = 0;
|
||||
frame.layoutMode = 'VERTICAL';
|
||||
frame.primaryAxisAlignItems = 'CENTER';
|
||||
frame.counterAxisAlignItems = 'CENTER';
|
||||
frame.itemSpacing = 16;
|
||||
frame.paddingTop = 0;
|
||||
frame.paddingBottom = 0;
|
||||
frame.paddingLeft = 0;
|
||||
frame.paddingRight = 0;
|
||||
|
||||
// Background: bind to primary variable if provided, else solid dark
|
||||
if (primaryColorVar) {
|
||||
const bgPaint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0.05, g: 0.05, b: 0.05 } },
|
||||
'color',
|
||||
primaryColorVar
|
||||
);
|
||||
frame.fills = [bgPaint];
|
||||
} else {
|
||||
frame.fills = [{ type: 'SOLID', color: { r: 0.06, g: 0.06, b: 0.07 } }];
|
||||
}
|
||||
page.appendChild(frame);
|
||||
|
||||
// System name heading
|
||||
const title = figma.createText();
|
||||
title.fontName = { family: 'Inter', style: 'Bold' };
|
||||
title.characters = systemName;
|
||||
title.fontSize = 64;
|
||||
title.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
||||
title.textAlignHorizontal = 'CENTER';
|
||||
frame.appendChild(title);
|
||||
|
||||
// Tagline
|
||||
const tag = figma.createText();
|
||||
tag.fontName = { family: 'Inter', style: 'Regular' };
|
||||
tag.characters = tagline;
|
||||
tag.fontSize = 20;
|
||||
tag.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.7 } }];
|
||||
tag.textAlignHorizontal = 'CENTER';
|
||||
frame.appendChild(tag);
|
||||
|
||||
// Version
|
||||
const ver = figma.createText();
|
||||
ver.fontName = { family: 'Inter', style: 'Medium' };
|
||||
ver.characters = version;
|
||||
ver.fontSize = 13;
|
||||
ver.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.45 } }];
|
||||
ver.textAlignHorizontal = 'CENTER';
|
||||
frame.appendChild(ver);
|
||||
|
||||
return { page, frameId: frame.id };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Foundations Page
|
||||
|
||||
The Foundations page is always placed **before any component pages**. It visually documents the design tokens — colors, typography, spacing, shadows, and border radii — so designers and engineers can see available primitives at a glance.
|
||||
|
||||
### Page layout dimensions
|
||||
|
||||
The outer documentation frame should be **1440px wide**. Sections stack vertically with **64–100px gaps** between them. Each section frame fills the full 1440px width and hugs its content vertically.
|
||||
|
||||
### Full Foundations page skeleton
|
||||
|
||||
```javascript
|
||||
async function createFoundationsPage() {
|
||||
const page = figma.createPage();
|
||||
page.name = 'Foundations';
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
// Root scroll frame
|
||||
const root = figma.createFrame();
|
||||
root.name = 'Foundations';
|
||||
root.layoutMode = 'VERTICAL';
|
||||
root.primaryAxisAlignItems = 'MIN';
|
||||
root.counterAxisAlignItems = 'MIN';
|
||||
root.itemSpacing = 80;
|
||||
root.paddingTop = 80;
|
||||
root.paddingBottom = 120;
|
||||
root.paddingLeft = 80;
|
||||
root.paddingRight = 80;
|
||||
root.layoutSizingHorizontal = 'FIXED';
|
||||
root.layoutSizingVertical = 'HUG';
|
||||
root.resize(1440, 1);
|
||||
root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
||||
page.appendChild(root);
|
||||
|
||||
return { page, root };
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Color Swatches (bound to variables)
|
||||
|
||||
Color swatches must be **bound to actual Figma variables** — never hardcode hex values in swatch fills. This keeps documentation in sync automatically when variable values change.
|
||||
|
||||
### Single color swatch
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a single color swatch card (rectangle + variable name label).
|
||||
* The swatch rectangle fill is bound to the provided variable.
|
||||
*
|
||||
* @param {FrameNode} parent - The auto-layout row to append to.
|
||||
* @param {string} varName - Display name (e.g. "color/bg/primary").
|
||||
* @param {Variable} variable - The Figma Variable object to bind to.
|
||||
* @returns {FrameNode} The swatch frame.
|
||||
*/
|
||||
async function createColorSwatch(parent, varName, variable) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
const swatchFrame = figma.createFrame();
|
||||
swatchFrame.name = `Swatch/${varName}`;
|
||||
swatchFrame.layoutMode = 'VERTICAL';
|
||||
swatchFrame.primaryAxisAlignItems = 'MIN';
|
||||
swatchFrame.counterAxisAlignItems = 'MIN';
|
||||
swatchFrame.itemSpacing = 6;
|
||||
swatchFrame.layoutSizingHorizontal = 'FIXED';
|
||||
swatchFrame.layoutSizingVertical = 'HUG';
|
||||
swatchFrame.resize(88, 1);
|
||||
swatchFrame.fills = [];
|
||||
|
||||
// Color rectangle — bound to variable
|
||||
const rect = figma.createRectangle();
|
||||
rect.resize(88, 88);
|
||||
rect.cornerRadius = 8;
|
||||
const paint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
|
||||
'color',
|
||||
variable
|
||||
);
|
||||
rect.fills = [paint];
|
||||
swatchFrame.appendChild(rect);
|
||||
|
||||
// Name label
|
||||
const label = figma.createText();
|
||||
label.fontName = { family: 'Inter', style: 'Regular' };
|
||||
label.characters = varName.split('/').pop(); // show leaf name only
|
||||
label.fontSize = 10;
|
||||
label.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
|
||||
label.layoutSizingHorizontal = 'FILL';
|
||||
swatchFrame.appendChild(label);
|
||||
|
||||
// Full path tooltip label (smaller, lighter)
|
||||
const pathLabel = figma.createText();
|
||||
pathLabel.fontName = { family: 'Inter', style: 'Regular' };
|
||||
pathLabel.characters = varName;
|
||||
pathLabel.fontSize = 9;
|
||||
pathLabel.fills = [{ type: 'SOLID', color: { r: 0.6, g: 0.6, b: 0.6 } }];
|
||||
pathLabel.layoutSizingHorizontal = 'FILL';
|
||||
swatchFrame.appendChild(pathLabel);
|
||||
|
||||
parent.appendChild(swatchFrame);
|
||||
return swatchFrame;
|
||||
}
|
||||
```
|
||||
|
||||
### Color section builder (primitives row + semantic grid)
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a complete color documentation section with a section heading,
|
||||
* a row of primitive swatches, and a grid of semantic swatches.
|
||||
*
|
||||
* @param {FrameNode} root - The root vertical stack frame.
|
||||
* @param {Variable[]} primitiveVars - Variables from the Primitives collection.
|
||||
* @param {Variable[]} semanticVars - Variables from the semantic Color collection.
|
||||
*/
|
||||
async function createColorSection(root, primitiveVars, semanticVars) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
// Section container
|
||||
const section = figma.createFrame();
|
||||
section.name = 'Section/Colors';
|
||||
section.layoutMode = 'VERTICAL';
|
||||
section.itemSpacing = 24;
|
||||
section.layoutSizingHorizontal = 'FILL';
|
||||
section.layoutSizingVertical = 'HUG';
|
||||
section.fills = [];
|
||||
root.appendChild(section);
|
||||
|
||||
// Section heading
|
||||
const heading = figma.createText();
|
||||
heading.fontName = { family: 'Inter', style: 'Bold' };
|
||||
heading.characters = 'Colors';
|
||||
heading.fontSize = 32;
|
||||
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
section.appendChild(heading);
|
||||
|
||||
// Description
|
||||
const desc = figma.createText();
|
||||
desc.fontName = { family: 'Inter', style: 'Regular' };
|
||||
desc.characters = 'Primitive color palette and semantic color tokens. Semantic tokens reference primitives — always use semantic tokens in components.';
|
||||
desc.fontSize = 14;
|
||||
desc.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }];
|
||||
desc.layoutSizingHorizontal = 'FILL';
|
||||
section.appendChild(desc);
|
||||
|
||||
// Primitive swatches row
|
||||
const primLabel = figma.createText();
|
||||
primLabel.fontName = { family: 'Inter', style: 'Bold' };
|
||||
primLabel.characters = 'Primitives';
|
||||
primLabel.fontSize = 13;
|
||||
primLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
|
||||
section.appendChild(primLabel);
|
||||
|
||||
const primRow = figma.createFrame();
|
||||
primRow.name = 'Primitives/Row';
|
||||
primRow.layoutMode = 'HORIZONTAL';
|
||||
primRow.itemSpacing = 12;
|
||||
primRow.layoutSizingHorizontal = 'FILL';
|
||||
primRow.layoutSizingVertical = 'HUG';
|
||||
primRow.fills = [];
|
||||
primRow.layoutWrap = 'WRAP';
|
||||
section.appendChild(primRow);
|
||||
|
||||
for (const v of primitiveVars) {
|
||||
await createColorSwatch(primRow, v.name, v);
|
||||
}
|
||||
|
||||
// Semantic swatches grid
|
||||
if (semanticVars.length > 0) {
|
||||
const semLabel = figma.createText();
|
||||
semLabel.fontName = { family: 'Inter', style: 'Bold' };
|
||||
semLabel.characters = 'Semantic';
|
||||
semLabel.fontSize = 13;
|
||||
semLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
|
||||
section.appendChild(semLabel);
|
||||
|
||||
const semRow = figma.createFrame();
|
||||
semRow.name = 'Semantic/Row';
|
||||
semRow.layoutMode = 'HORIZONTAL';
|
||||
semRow.itemSpacing = 12;
|
||||
semRow.layoutSizingHorizontal = 'FILL';
|
||||
semRow.layoutSizingVertical = 'HUG';
|
||||
semRow.fills = [];
|
||||
semRow.layoutWrap = 'WRAP';
|
||||
section.appendChild(semRow);
|
||||
|
||||
for (const v of semanticVars) {
|
||||
await createColorSwatch(semRow, v.name, v);
|
||||
}
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Type Specimens
|
||||
|
||||
Typography specimens show each text style rendered at its actual size with a sample string, the style name, and its specifications.
|
||||
|
||||
### Single type specimen row
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a single type specimen row: style name (small label) + sample text +
|
||||
* specification line (family · style · size · line-height).
|
||||
*
|
||||
* @param {FrameNode} parent - The parent vertical stack.
|
||||
* @param {string} styleName - The text style name (e.g. "Display Large").
|
||||
* @param {string} fontFamily - Font family (e.g. "Inter").
|
||||
* @param {string} fontStyle - Font style (e.g. "Bold").
|
||||
* @param {number} fontSize - Font size in pixels.
|
||||
* @param {number} lineHeight - Line height in pixels.
|
||||
* @returns {FrameNode} The specimen row frame.
|
||||
*/
|
||||
async function createTypeSpecimen(parent, styleName, fontFamily, fontStyle, fontSize, lineHeight) {
|
||||
await figma.loadFontAsync({ family: fontFamily, style: fontStyle });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
const row = figma.createFrame();
|
||||
row.name = `Type/${styleName}`;
|
||||
row.layoutMode = 'VERTICAL';
|
||||
row.itemSpacing = 6;
|
||||
row.paddingTop = 16;
|
||||
row.paddingBottom = 16;
|
||||
row.layoutSizingHorizontal = 'FILL';
|
||||
row.layoutSizingVertical = 'HUG';
|
||||
row.fills = [];
|
||||
parent.appendChild(row);
|
||||
|
||||
// Style name label (small, muted)
|
||||
const nameText = figma.createText();
|
||||
nameText.fontName = { family: 'Inter', style: 'Medium' };
|
||||
nameText.characters = styleName;
|
||||
nameText.fontSize = 11;
|
||||
nameText.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
|
||||
nameText.layoutSizingHorizontal = 'FILL';
|
||||
row.appendChild(nameText);
|
||||
|
||||
// Sample text rendered in the actual style
|
||||
const specimen = figma.createText();
|
||||
specimen.fontName = { family: fontFamily, style: fontStyle };
|
||||
specimen.characters = 'The quick brown fox jumps over the lazy dog';
|
||||
specimen.fontSize = fontSize;
|
||||
specimen.lineHeight = { value: lineHeight, unit: 'PIXELS' };
|
||||
specimen.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
specimen.layoutSizingHorizontal = 'FILL';
|
||||
row.appendChild(specimen);
|
||||
|
||||
// Specification line
|
||||
const specs = figma.createText();
|
||||
specs.fontName = { family: 'Inter', style: 'Regular' };
|
||||
specs.characters = `${fontFamily} ${fontStyle} · ${fontSize}px · ${lineHeight}px line height`;
|
||||
specs.fontSize = 11;
|
||||
specs.fills = [{ type: 'SOLID', color: { r: 0.65, g: 0.65, b: 0.65 } }];
|
||||
specs.layoutSizingHorizontal = 'FILL';
|
||||
row.appendChild(specs);
|
||||
|
||||
// Divider line
|
||||
const divider = figma.createRectangle();
|
||||
divider.resize(1280, 1);
|
||||
divider.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }];
|
||||
divider.layoutSizingHorizontal = 'FILL';
|
||||
row.appendChild(divider);
|
||||
|
||||
return row;
|
||||
}
|
||||
```
|
||||
|
||||
### Typography section builder
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a complete typography documentation section.
|
||||
* Pass an array of style definitions; the function renders one specimen per entry.
|
||||
*
|
||||
* @param {FrameNode} root - Root vertical stack.
|
||||
* @param {Array<{name, family, style, size, lineHeight}>} typeStyles - Style definitions.
|
||||
*/
|
||||
async function createTypographySection(root, typeStyles) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
|
||||
const section = figma.createFrame();
|
||||
section.name = 'Section/Typography';
|
||||
section.layoutMode = 'VERTICAL';
|
||||
section.itemSpacing = 0;
|
||||
section.layoutSizingHorizontal = 'FILL';
|
||||
section.layoutSizingVertical = 'HUG';
|
||||
section.fills = [];
|
||||
root.appendChild(section);
|
||||
|
||||
const heading = figma.createText();
|
||||
heading.fontName = { family: 'Inter', style: 'Bold' };
|
||||
heading.characters = 'Typography';
|
||||
heading.fontSize = 32;
|
||||
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
section.appendChild(heading);
|
||||
|
||||
for (const ts of typeStyles) {
|
||||
await createTypeSpecimen(section, ts.name, ts.family, ts.style, ts.size, ts.lineHeight);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Spacing Bars
|
||||
|
||||
Spacing bars show each spacing token as a filled rectangle whose width equals the spacing value. Shorter bars for small values, longer bars for large values — the visual encoding is immediate.
|
||||
|
||||
### Spacing bar row
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a single spacing bar: a colored rectangle sized to the spacing value,
|
||||
* with a label showing name + pixel value + code syntax.
|
||||
*
|
||||
* @param {FrameNode} parent - Parent vertical stack.
|
||||
* @param {string} name - Token name (e.g. "spacing/sm").
|
||||
* @param {number} value - Spacing value in pixels.
|
||||
* @param {Variable} variable - Figma Variable to bind the width to.
|
||||
* @param {string} codeSyntax - CSS variable string (e.g. "var(--spacing-sm)").
|
||||
*/
|
||||
async function createSpacingBar(parent, name, value, variable, codeSyntax) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
const row = figma.createFrame();
|
||||
row.name = `Spacing/${name}`;
|
||||
row.layoutMode = 'HORIZONTAL';
|
||||
row.counterAxisAlignItems = 'CENTER';
|
||||
row.itemSpacing = 16;
|
||||
row.layoutSizingHorizontal = 'FILL';
|
||||
row.layoutSizingVertical = 'HUG';
|
||||
row.fills = [];
|
||||
parent.appendChild(row);
|
||||
|
||||
// The bar rectangle — width bound to spacing variable
|
||||
const bar = figma.createRectangle();
|
||||
bar.resize(value, 16);
|
||||
bar.cornerRadius = 3;
|
||||
bar.fills = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98 } }];
|
||||
// Bind width to the spacing variable so it reflects the actual token value
|
||||
if (variable) {
|
||||
bar.setBoundVariable('width', variable);
|
||||
}
|
||||
row.appendChild(bar);
|
||||
|
||||
// Label: "spacing/sm 8px var(--spacing-sm)"
|
||||
const label = figma.createText();
|
||||
label.fontName = { family: 'Inter', style: 'Regular' };
|
||||
label.characters = `${name} ${value}px ${codeSyntax}`;
|
||||
label.fontSize = 12;
|
||||
label.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
|
||||
row.appendChild(label);
|
||||
|
||||
return row;
|
||||
}
|
||||
```
|
||||
|
||||
### Spacing section builder
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates the full spacing documentation section.
|
||||
*
|
||||
* @param {FrameNode} root - Root vertical stack.
|
||||
* @param {Array<{name, value, variable, codeSyntax}>} spacingTokens - Token definitions.
|
||||
*/
|
||||
async function createSpacingSection(root, spacingTokens) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
|
||||
const section = figma.createFrame();
|
||||
section.name = 'Section/Spacing';
|
||||
section.layoutMode = 'VERTICAL';
|
||||
section.itemSpacing = 12;
|
||||
section.layoutSizingHorizontal = 'FILL';
|
||||
section.layoutSizingVertical = 'HUG';
|
||||
section.fills = [];
|
||||
root.appendChild(section);
|
||||
|
||||
const heading = figma.createText();
|
||||
heading.fontName = { family: 'Inter', style: 'Bold' };
|
||||
heading.characters = 'Spacing';
|
||||
heading.fontSize = 32;
|
||||
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
section.appendChild(heading);
|
||||
|
||||
for (const tok of spacingTokens) {
|
||||
await createSpacingBar(section, tok.name, tok.value, tok.variable, tok.codeSyntax);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Shadow Cards (Elevation)
|
||||
|
||||
Elevation documentation shows cards with progressively stronger drop shadows, labeled with name and effect parameters.
|
||||
|
||||
### Single shadow card
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a shadow card: a white rectangle with a drop shadow effect,
|
||||
* labeled with the elevation name and shadow parameters.
|
||||
*
|
||||
* @param {FrameNode} parent - The horizontal row to append to.
|
||||
* @param {string} name - Elevation name (e.g. "Shadow/Medium").
|
||||
* @param {DropShadowEffect[]} effects - Array of Figma effect objects.
|
||||
*/
|
||||
async function createShadowCard(parent, name, effects) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
|
||||
const card = figma.createFrame();
|
||||
card.name = `ShadowCard/${name}`;
|
||||
card.layoutMode = 'VERTICAL';
|
||||
card.primaryAxisAlignItems = 'CENTER';
|
||||
card.counterAxisAlignItems = 'CENTER';
|
||||
card.itemSpacing = 8;
|
||||
card.paddingTop = 16;
|
||||
card.paddingBottom = 16;
|
||||
card.resize(120, 120);
|
||||
card.cornerRadius = 8;
|
||||
card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }];
|
||||
card.effects = effects;
|
||||
parent.appendChild(card);
|
||||
|
||||
// Elevation name
|
||||
const nameLabel = figma.createText();
|
||||
nameLabel.fontName = { family: 'Inter', style: 'Medium' };
|
||||
nameLabel.characters = name.split('/').pop();
|
||||
nameLabel.fontSize = 12;
|
||||
nameLabel.textAlignHorizontal = 'CENTER';
|
||||
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.2, b: 0.2 } }];
|
||||
card.appendChild(nameLabel);
|
||||
|
||||
// Effect parameters as small text
|
||||
if (effects.length > 0) {
|
||||
const e = effects[0];
|
||||
if (e.type === 'DROP_SHADOW') {
|
||||
const params = figma.createText();
|
||||
params.fontName = { family: 'Inter', style: 'Regular' };
|
||||
params.characters = `x:${e.offset.x} y:${e.offset.y}\nblur:${e.radius}`;
|
||||
params.fontSize = 10;
|
||||
params.textAlignHorizontal = 'CENTER';
|
||||
params.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
|
||||
card.appendChild(params);
|
||||
}
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
```
|
||||
|
||||
### Shadow section builder
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates the full elevation/shadow documentation section.
|
||||
*
|
||||
* @param {FrameNode} root - Root vertical stack.
|
||||
* @param {Array<{name, effects}>} shadowTokens - Shadow definitions.
|
||||
*/
|
||||
async function createShadowSection(root, shadowTokens) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
|
||||
const section = figma.createFrame();
|
||||
section.name = 'Section/Elevation';
|
||||
section.layoutMode = 'VERTICAL';
|
||||
section.itemSpacing = 24;
|
||||
section.layoutSizingHorizontal = 'FILL';
|
||||
section.layoutSizingVertical = 'HUG';
|
||||
section.fills = [];
|
||||
root.appendChild(section);
|
||||
|
||||
const heading = figma.createText();
|
||||
heading.fontName = { family: 'Inter', style: 'Bold' };
|
||||
heading.characters = 'Elevation';
|
||||
heading.fontSize = 32;
|
||||
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
section.appendChild(heading);
|
||||
|
||||
// Cards row — extra top padding so shadows are visible
|
||||
const row = figma.createFrame();
|
||||
row.name = 'Elevation/Row';
|
||||
row.layoutMode = 'HORIZONTAL';
|
||||
row.itemSpacing = 32;
|
||||
row.paddingTop = 24;
|
||||
row.paddingBottom = 40;
|
||||
row.layoutSizingHorizontal = 'FILL';
|
||||
row.layoutSizingVertical = 'HUG';
|
||||
row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }];
|
||||
row.cornerRadius = 8;
|
||||
row.paddingLeft = 24;
|
||||
row.paddingRight = 24;
|
||||
section.appendChild(row);
|
||||
|
||||
for (const tok of shadowTokens) {
|
||||
await createShadowCard(row, tok.name, tok.effects);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Border Radius Demo
|
||||
|
||||
Border radius documentation shows rectangles at each corner radius value, labeled with the token name and pixel value.
|
||||
|
||||
### Single radius card
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a single border radius card: a square with corner radius applied,
|
||||
* labeled with the token name and pixel value.
|
||||
*
|
||||
* @param {FrameNode} parent - The horizontal row to append to.
|
||||
* @param {string} name - Token name (e.g. "radius/md").
|
||||
* @param {number} value - Corner radius in pixels (0 for none, 9999 for full).
|
||||
* @param {Variable} [variable] - Optional Figma Variable to bind corner radius.
|
||||
*/
|
||||
async function createRadiusCard(parent, name, value, variable) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' });
|
||||
|
||||
const wrapper = figma.createFrame();
|
||||
wrapper.name = `Radius/${name}`;
|
||||
wrapper.layoutMode = 'VERTICAL';
|
||||
wrapper.primaryAxisAlignItems = 'CENTER';
|
||||
wrapper.counterAxisAlignItems = 'CENTER';
|
||||
wrapper.itemSpacing = 8;
|
||||
wrapper.fills = [];
|
||||
wrapper.layoutSizingHorizontal = 'FIXED';
|
||||
wrapper.layoutSizingVertical = 'HUG';
|
||||
wrapper.resize(96, 1);
|
||||
parent.appendChild(wrapper);
|
||||
|
||||
const rect = figma.createRectangle();
|
||||
rect.resize(72, 72);
|
||||
rect.fills = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98, a: 0.15 } }];
|
||||
rect.strokes = [{ type: 'SOLID', color: { r: 0.22, g: 0.47, b: 0.98 } }];
|
||||
rect.strokeWeight = 1.5;
|
||||
|
||||
// Cap display value — 9999 is how Figma represents "full/pill"
|
||||
const displayRadius = Math.min(value, 36);
|
||||
rect.cornerRadius = displayRadius;
|
||||
|
||||
// Bind to variable if provided
|
||||
if (variable) {
|
||||
rect.setBoundVariable('cornerRadius', variable);
|
||||
}
|
||||
wrapper.appendChild(rect);
|
||||
|
||||
const nameLabel = figma.createText();
|
||||
nameLabel.fontName = { family: 'Inter', style: 'Medium' };
|
||||
nameLabel.characters = name.split('/').pop();
|
||||
nameLabel.fontSize = 11;
|
||||
nameLabel.textAlignHorizontal = 'CENTER';
|
||||
nameLabel.fills = [{ type: 'SOLID', color: { r: 0.2, g: 0.2, b: 0.2 } }];
|
||||
wrapper.appendChild(nameLabel);
|
||||
|
||||
const valueLabel = figma.createText();
|
||||
valueLabel.fontName = { family: 'Inter', style: 'Regular' };
|
||||
valueLabel.characters = value >= 9999 ? 'full' : `${value}px`;
|
||||
valueLabel.fontSize = 10;
|
||||
valueLabel.textAlignHorizontal = 'CENTER';
|
||||
valueLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }];
|
||||
wrapper.appendChild(valueLabel);
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
```
|
||||
|
||||
### Radius section builder
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates the full border radius documentation section.
|
||||
*
|
||||
* @param {FrameNode} root - Root vertical stack.
|
||||
* @param {Array<{name, value, variable}>} radiusTokens - Radius token definitions.
|
||||
*/
|
||||
async function createRadiusSection(root, radiusTokens) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
|
||||
const section = figma.createFrame();
|
||||
section.name = 'Section/Radius';
|
||||
section.layoutMode = 'VERTICAL';
|
||||
section.itemSpacing = 24;
|
||||
section.layoutSizingHorizontal = 'FILL';
|
||||
section.layoutSizingVertical = 'HUG';
|
||||
section.fills = [];
|
||||
root.appendChild(section);
|
||||
|
||||
const heading = figma.createText();
|
||||
heading.fontName = { family: 'Inter', style: 'Bold' };
|
||||
heading.characters = 'Border Radius';
|
||||
heading.fontSize = 32;
|
||||
heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
section.appendChild(heading);
|
||||
|
||||
const row = figma.createFrame();
|
||||
row.name = 'Radius/Row';
|
||||
row.layoutMode = 'HORIZONTAL';
|
||||
row.itemSpacing = 24;
|
||||
row.paddingTop = 24;
|
||||
row.paddingBottom = 24;
|
||||
row.paddingLeft = 24;
|
||||
row.paddingRight = 24;
|
||||
row.layoutSizingHorizontal = 'FILL';
|
||||
row.layoutSizingVertical = 'HUG';
|
||||
row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }];
|
||||
row.cornerRadius = 8;
|
||||
section.appendChild(row);
|
||||
|
||||
for (const tok of radiusTokens) {
|
||||
await createRadiusCard(row, tok.name, tok.value, tok.variable);
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Documentation Alongside Components
|
||||
|
||||
Each component page should include a documentation frame directly on the canvas, placed to the left of the component set. This keeps docs and the component in sync without requiring a separate file.
|
||||
|
||||
### Component page documentation frame
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates the documentation frame for a component page: title, description,
|
||||
* and usage notes, positioned at x=0 with the component set to its right.
|
||||
*
|
||||
* @param {PageNode} page - The component page (must already be current).
|
||||
* @param {string} componentName - The component name.
|
||||
* @param {string} description - What the component does and when to use it.
|
||||
* @param {string[]} usageNotes - Bullet points for usage guidance.
|
||||
* @returns {FrameNode} The documentation frame.
|
||||
*/
|
||||
async function createComponentDocFrame(page, componentName, description, usageNotes) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' });
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' });
|
||||
|
||||
const doc = figma.createFrame();
|
||||
doc.name = '_Doc';
|
||||
doc.layoutMode = 'VERTICAL';
|
||||
doc.itemSpacing = 16;
|
||||
doc.paddingTop = 40;
|
||||
doc.paddingBottom = 40;
|
||||
doc.paddingLeft = 40;
|
||||
doc.paddingRight = 40;
|
||||
doc.layoutSizingHorizontal = 'FIXED';
|
||||
doc.layoutSizingVertical = 'HUG';
|
||||
doc.resize(360, 1);
|
||||
doc.fills = [];
|
||||
doc.x = 0;
|
||||
doc.y = 0;
|
||||
page.appendChild(doc);
|
||||
|
||||
// Component name — large heading
|
||||
const title = figma.createText();
|
||||
title.fontName = { family: 'Inter', style: 'Bold' };
|
||||
title.characters = componentName;
|
||||
title.fontSize = 28;
|
||||
title.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
title.layoutSizingHorizontal = 'FILL';
|
||||
doc.appendChild(title);
|
||||
|
||||
// Description
|
||||
const descText = figma.createText();
|
||||
descText.fontName = { family: 'Inter', style: 'Regular' };
|
||||
descText.characters = description;
|
||||
descText.fontSize = 13;
|
||||
descText.lineHeight = { value: 20, unit: 'PIXELS' };
|
||||
descText.fills = [{ type: 'SOLID', color: { r: 0.35, g: 0.35, b: 0.35 } }];
|
||||
descText.layoutSizingHorizontal = 'FILL';
|
||||
doc.appendChild(descText);
|
||||
|
||||
// Divider
|
||||
const divider = figma.createRectangle();
|
||||
divider.resize(280, 1);
|
||||
divider.fills = [{ type: 'SOLID', color: { r: 0.88, g: 0.88, b: 0.88 } }];
|
||||
divider.layoutSizingHorizontal = 'FILL';
|
||||
doc.appendChild(divider);
|
||||
|
||||
// Usage notes
|
||||
if (usageNotes.length > 0) {
|
||||
const usageHeading = figma.createText();
|
||||
usageHeading.fontName = { family: 'Inter', style: 'Bold' };
|
||||
usageHeading.characters = 'Usage';
|
||||
usageHeading.fontSize = 13;
|
||||
usageHeading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }];
|
||||
doc.appendChild(usageHeading);
|
||||
|
||||
for (const note of usageNotes) {
|
||||
const noteText = figma.createText();
|
||||
noteText.fontName = { family: 'Inter', style: 'Regular' };
|
||||
noteText.characters = `• ${note}`;
|
||||
noteText.fontSize = 12;
|
||||
noteText.lineHeight = { value: 18, unit: 'PIXELS' };
|
||||
noteText.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }];
|
||||
noteText.layoutSizingHorizontal = 'FILL';
|
||||
doc.appendChild(noteText);
|
||||
}
|
||||
}
|
||||
|
||||
return doc;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Critical Rules
|
||||
|
||||
1. **Bind swatches to variables** — use `setBoundVariableForPaint` for color fills, `setBoundVariable('width', ...)` for spacing bars, and `setBoundVariable('cornerRadius', ...)` for radius cards. Never hardcode values that have corresponding variables.
|
||||
2. **Foundations page comes before component pages** — always insert it between the file structure separators and the first component page.
|
||||
3. **Show both primitive and semantic layers** — if the system has a Primitives collection and a semantic Color collection, document both on the Foundations page with clear section labels.
|
||||
4. **Page frame width = 1440px** — this is the convention across Simple DS, Polaris, and Material 3. Use it unless you detect a different existing convention via `get_metadata`.
|
||||
5. **Section spacing = 64–80px** — the gap between color / typography / spacing / shadow / radius sections should be at minimum 64px so the page is scannable.
|
||||
6. **Match existing page style** — if the target file uses emoji page name prefixes or a decorative separator style, carry that through to the Foundations page name.
|
||||
7. **Include code syntax in labels** — where variables have code syntax set, display the CSS variable name in the swatch/bar label so developers can copy it directly.
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Error Recovery Reference
|
||||
|
||||
Protocol for handling failures and incomplete runs across a 20–100+ call design system build.
|
||||
|
||||
---
|
||||
|
||||
## 1. Core Protocol: STOP → Inspect → Fix → Retry
|
||||
|
||||
**`use_figma` is atomic — a failed script does not execute.** If a script errors, no changes are made to the file. There are no partial nodes or half-built state from the failed call itself. Retrying after a fix is safe.
|
||||
|
||||
However, in multi-step workflows (20–100+ calls), **previously successful calls** will have created state that persists. If a workflow is abandoned mid-way, nodes from earlier successful calls remain in the file. The cleanup and idempotency patterns in this document handle that scenario.
|
||||
|
||||
The recovery sequence for a failed script:
|
||||
|
||||
```
|
||||
1. STOP — Do not run any more use_figma writes.
|
||||
2. INSPECT — Read the error message carefully. Optionally call get_metadata or get_screenshot to understand the current file state.
|
||||
3. FIX — Correct the script that failed.
|
||||
4. RETRY — Re-run the corrected script.
|
||||
5. PERSIST — Update the state ledger with the outcome.
|
||||
```
|
||||
|
||||
For **abandoned multi-step workflows** (where you need to roll back nodes from previous *successful* calls), use the cleanup protocol in Section 2.
|
||||
|
||||
---
|
||||
|
||||
## 2. `sharedPluginData`-Based Cleanup: Why Name Matching is Dangerous
|
||||
|
||||
### Why name-prefix matching fails
|
||||
|
||||
A cleanup script that deletes "all nodes whose name starts with `Button`" will also delete nodes the user may have created manually with that name, or nodes from a previous approved phase. Name-based cleanup has no way to distinguish "orphan from a failed attempt" from "intentional user node."
|
||||
|
||||
Furthermore, variant names (`Size=Medium, Style=Primary, State=Default`) do not have consistent prefixes that are safe to target without also hitting legitimate nodes.
|
||||
|
||||
### How `setSharedPluginData` / `getSharedPluginData` works
|
||||
|
||||
`sharedPluginData` is a key-value store attached to individual nodes. It persists across sessions and is invisible to the user in the Figma UI. Data is scoped by namespace — we use `'dsb'`. Use three keys:
|
||||
|
||||
```javascript
|
||||
node.setSharedPluginData('dsb', 'run_id', 'ds-build-2024-001'); // identifies the build run
|
||||
node.setSharedPluginData('dsb', 'phase', 'phase3'); // which phase created this node
|
||||
node.setSharedPluginData('dsb', 'key', 'componentset/button');// unique logical key
|
||||
|
||||
// Reading:
|
||||
const runId = node.getSharedPluginData('dsb', 'run_id'); // returns '' if never set
|
||||
const key = node.getSharedPluginData('dsb', 'key');
|
||||
```
|
||||
|
||||
`getSharedPluginData` returns `''` (empty string, not null) for unset keys. Always check for `!== ''`.
|
||||
|
||||
**Tag every created node immediately after creation** — this enables safe cleanup if the multi-step workflow is abandoned later. Tag in the same statement sequence as creation:
|
||||
|
||||
```javascript
|
||||
const comp = figma.createComponent();
|
||||
comp.setSharedPluginData('dsb', 'run_id', RUN_ID); // tag immediately
|
||||
comp.setSharedPluginData('dsb', 'key', key); // tag immediately
|
||||
// ... then do the rest of the setup
|
||||
```
|
||||
|
||||
### Complete `cleanupOrphans` script using `run_id`
|
||||
|
||||
This script finds all nodes tagged with a given `run_id` and optionally a `phase` filter, then removes them. Run it on the specific page where the failure occurred.
|
||||
|
||||
```javascript
|
||||
const TARGET_RUN_ID = 'ds-build-2024-001'; // run ID to clean
|
||||
const TARGET_PHASE = 'phase3'; // optionally filter by phase ('' = all phases)
|
||||
const PAGE_NAME = 'Button'; // page to clean (or null for all pages)
|
||||
|
||||
const pagesToSearch = PAGE_NAME
|
||||
? [figma.root.children.find(p => p.name === PAGE_NAME)].filter(Boolean)
|
||||
: figma.root.children;
|
||||
|
||||
const removed = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const page of pagesToSearch) {
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
const orphans = page.findAll(node => {
|
||||
const runId = node.getSharedPluginData('dsb', 'run_id');
|
||||
if (runId !== TARGET_RUN_ID) return false;
|
||||
if (TARGET_PHASE && node.getSharedPluginData('dsb', 'phase') !== TARGET_PHASE) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// Remove leaf-first to avoid removing parents before children
|
||||
// Sort by depth (deepest first) to avoid double-remove errors
|
||||
const sorted = orphans.slice().sort((a, b) => {
|
||||
let depthA = 0, depthB = 0;
|
||||
let n = a; while (n.parent) { depthA++; n = n.parent; }
|
||||
n = b; while (n.parent) { depthB++; n = n.parent; }
|
||||
return depthB - depthA;
|
||||
});
|
||||
|
||||
for (const node of sorted) {
|
||||
try {
|
||||
if (node.removed) continue; // already removed (was a child of removed parent)
|
||||
node.remove();
|
||||
removed.push({ id: node.id, name: node.name, key: node.getSharedPluginData('dsb', 'key') });
|
||||
} catch (e) {
|
||||
skipped.push({ id: node.id, name: node.name, error: e.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { removed: removed.length, skipped: skipped.length, details: removed };
|
||||
```
|
||||
|
||||
After running cleanup, call `get_metadata` on the target page to confirm the orphaned nodes are gone before retrying.
|
||||
|
||||
---
|
||||
|
||||
## 3. Idempotency Patterns: Check-Before-Create
|
||||
|
||||
Run an idempotency check at the start of every create operation. If the entity already exists (tagged with the expected `key`), skip creation and return the existing ID.
|
||||
|
||||
### Check-before-create for a variable collection
|
||||
|
||||
```javascript
|
||||
const KEY = 'collection/color';
|
||||
const RUN_ID = 'ds-build-2024-001';
|
||||
const COLLECTION_NAME = 'Color';
|
||||
|
||||
// Check: does a collection tagged with this key already exist?
|
||||
const allCollections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
// Variables/collections support sharedPluginData too — check by name as fallback
|
||||
// Note: VariableCollection sharedPluginData is set via collection.setSharedPluginData(...)
|
||||
const existing = allCollections.find(c =>
|
||||
c.getSharedPluginData('dsb', 'key') === KEY
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return {
|
||||
collectionId: existing.id,
|
||||
modeIds: existing.modes.map(m => ({ name: m.name, id: m.modeId })),
|
||||
alreadyExisted: true,
|
||||
};
|
||||
}
|
||||
|
||||
// Create fresh
|
||||
const collection = figma.variables.createVariableCollection(COLLECTION_NAME);
|
||||
collection.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
collection.setSharedPluginData('dsb', 'key', KEY);
|
||||
|
||||
// Rename default mode, add second mode
|
||||
collection.renameMode(collection.modes[0].modeId, 'Light');
|
||||
const darkModeId = collection.addMode('Dark');
|
||||
|
||||
return {
|
||||
collectionId: collection.id,
|
||||
modeIds: [
|
||||
{ name: 'Light', id: collection.modes[0].modeId },
|
||||
{ name: 'Dark', id: darkModeId },
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
### Check-before-create for a page
|
||||
|
||||
```javascript
|
||||
const KEY = 'page/button';
|
||||
const PAGE_NAME = 'Button';
|
||||
const RUN_ID = 'ds-build-2024-001';
|
||||
|
||||
// Check by sharedPluginData key first, then by name as fallback
|
||||
let page = figma.root.children.find(p => p.getSharedPluginData('dsb', 'key') === KEY);
|
||||
if (!page) {
|
||||
page = figma.root.children.find(p => p.name === PAGE_NAME);
|
||||
}
|
||||
|
||||
if (page) {
|
||||
// Ensure it's tagged if it was found by name only
|
||||
if (!page.getSharedPluginData('dsb', 'key')) {
|
||||
page.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
page.setSharedPluginData('dsb', 'key', KEY);
|
||||
}
|
||||
return { pageId: page.id, alreadyExisted: true };
|
||||
}
|
||||
|
||||
page = figma.createPage();
|
||||
page.name = PAGE_NAME;
|
||||
page.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
page.setSharedPluginData('dsb', 'key', KEY);
|
||||
|
||||
return { pageId: page.id, alreadyExisted: false };
|
||||
```
|
||||
|
||||
### Check-before-create for a component set
|
||||
|
||||
```javascript
|
||||
const KEY = 'componentset/button';
|
||||
const PAGE_ID = 'PAGE_ID_FROM_STATE';
|
||||
const RUN_ID = 'ds-build-2024-001';
|
||||
|
||||
const page = await figma.getNodeByIdAsync(PAGE_ID);
|
||||
await figma.setCurrentPageAsync(page);
|
||||
|
||||
const existing = page.findAll(n =>
|
||||
n.type === 'COMPONENT_SET' && n.getSharedPluginData('dsb', 'key') === KEY
|
||||
);
|
||||
|
||||
if (existing.length > 0) {
|
||||
return {
|
||||
componentSetId: existing[0].id,
|
||||
alreadyExisted: true,
|
||||
};
|
||||
}
|
||||
|
||||
// ... proceed with creation
|
||||
return { componentSetId: null, alreadyExisted: false };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. State Ledger
|
||||
|
||||
### JSON Schema
|
||||
|
||||
Maintain a state ledger in your context (not in the Figma file) across calls. This is your source of truth for node IDs, completed steps, and pending validations.
|
||||
|
||||
```json
|
||||
{
|
||||
"runId": "ds-build-2024-001",
|
||||
"phase": "phase3",
|
||||
"step": "component-button/combine-variants",
|
||||
"completedSteps": [
|
||||
"phase0",
|
||||
"phase1/collections",
|
||||
"phase1/primitives",
|
||||
"phase1/semantics",
|
||||
"phase2/pages",
|
||||
"phase2/foundations-docs",
|
||||
"phase3/component-avatar",
|
||||
"phase3/component-icon"
|
||||
],
|
||||
"entities": {
|
||||
"collections": {
|
||||
"primitives": "VariableCollectionId:1234:5678",
|
||||
"color": "VariableCollectionId:1234:5679",
|
||||
"spacing": "VariableCollectionId:1234:5680"
|
||||
},
|
||||
"variables": {
|
||||
"color/bg/primary": "VariableId:2345:1",
|
||||
"color/bg/secondary": "VariableId:2345:2",
|
||||
"color/bg/disabled": "VariableId:2345:3",
|
||||
"color/text/on-primary": "VariableId:2345:4",
|
||||
"color/text/on-secondary": "VariableId:2345:5",
|
||||
"color/text/disabled": "VariableId:2345:6",
|
||||
"spacing/sm": "VariableId:2345:7",
|
||||
"spacing/md": "VariableId:2345:8",
|
||||
"spacing/lg": "VariableId:2345:9",
|
||||
"radius/md": "VariableId:2345:10"
|
||||
},
|
||||
"modes": {
|
||||
"color/light": "2345:1",
|
||||
"color/dark": "2345:2"
|
||||
},
|
||||
"pages": {
|
||||
"Cover": "0:1",
|
||||
"Foundations": "0:2",
|
||||
"Button": "0:3"
|
||||
},
|
||||
"components": {
|
||||
"Icon": "3456:1",
|
||||
"Avatar": "3456:2",
|
||||
"Button": "3456:3"
|
||||
},
|
||||
"componentSets": {
|
||||
"Button": "4567:1"
|
||||
}
|
||||
},
|
||||
"pendingValidations": [
|
||||
"Button:metadata",
|
||||
"Button:screenshot"
|
||||
],
|
||||
"userCheckpoints": {
|
||||
"phase0": "approved-2024-01-15",
|
||||
"phase1": "approved-2024-01-15",
|
||||
"phase2": "approved-2024-01-15",
|
||||
"component-avatar": "approved-2024-01-15"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Persisting between calls
|
||||
|
||||
After every successful `use_figma` call:
|
||||
1. Extract all IDs from the return value
|
||||
2. Add them to the appropriate `entities` section of the ledger
|
||||
3. Add the completed step to `completedSteps`
|
||||
4. Remove from `pendingValidations` if this call validated something
|
||||
5. Update `phase` and `step` to the current position
|
||||
|
||||
### Rehydrating at session start
|
||||
|
||||
If a conversation is interrupted and resumed, read the state ledger and verify key entities still exist:
|
||||
|
||||
```javascript
|
||||
// Verify that critical nodes from the ledger still exist
|
||||
const toVerify = {
|
||||
'color-collection': 'VariableCollectionId:1234:5679',
|
||||
'button-page': '0:3',
|
||||
'button-componentset': '4567:1',
|
||||
};
|
||||
|
||||
const results = {};
|
||||
for (const [label, id] of Object.entries(toVerify)) {
|
||||
const node = await figma.getNodeByIdAsync(id)
|
||||
.catch(() => null);
|
||||
results[label] = node ? { found: true, name: node.name } : { found: false };
|
||||
}
|
||||
|
||||
return results;
|
||||
```
|
||||
|
||||
If any entity is missing, treat the phase that created it as incomplete and re-run from that checkpoint.
|
||||
|
||||
---
|
||||
|
||||
## 5. Resume Protocol
|
||||
|
||||
### Step 1: Inspect the file for `run_id` tags
|
||||
|
||||
```javascript
|
||||
const TARGET_RUN_ID = 'ds-build-2024-001';
|
||||
const inventory = { pages: [], variables: [], componentSets: [], frames: [] };
|
||||
|
||||
// Scan pages
|
||||
for (const page of figma.root.children) {
|
||||
if (page.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
|
||||
inventory.pages.push({ id: page.id, name: page.name, key: page.getSharedPluginData('dsb', 'key') });
|
||||
}
|
||||
}
|
||||
|
||||
// Scan variables
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
for (const v of allVars) {
|
||||
if (v.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
|
||||
inventory.variables.push({ id: v.id, name: v.name, key: v.getSharedPluginData('dsb', 'key') });
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all component sets and frames on each page
|
||||
for (const page of figma.root.children) {
|
||||
await figma.setCurrentPageAsync(page);
|
||||
const nodes = page.findAll(n => n.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID);
|
||||
for (const n of nodes) {
|
||||
if (n.type === 'COMPONENT_SET') {
|
||||
inventory.componentSets.push({ id: n.id, name: n.name, key: n.getSharedPluginData('dsb', 'key') });
|
||||
} else if (n.type === 'FRAME') {
|
||||
inventory.frames.push({ id: n.id, name: n.name, key: n.getSharedPluginData('dsb', 'key') });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return inventory;
|
||||
```
|
||||
|
||||
### Step 2: Reconstruct state from inventory
|
||||
|
||||
Map the inventory keys back to the state ledger schema. For each entity found with a `key`, add its ID to the appropriate section. Mark the corresponding step as `completedSteps`.
|
||||
|
||||
Example mapping:
|
||||
```
|
||||
key: 'collection/color' → entities.collections.color
|
||||
key: 'variable/color/bg/primary' → entities.variables['color/bg/primary']
|
||||
key: 'page/button' → entities.pages.Button
|
||||
key: 'componentset/button' → entities.componentSets.Button
|
||||
```
|
||||
|
||||
### Step 3: Identify the resume point
|
||||
|
||||
The resume point is the first step in the workflow that is NOT in `completedSteps`. If the inventory shows the Button component set exists but the pending validations list shows `'Button:screenshot'`, the resume point is the screenshot validation call, not re-creation.
|
||||
|
||||
Use the checkpoint table from the workflow to determine which phase to continue from:
|
||||
|
||||
```
|
||||
Phase 0 complete: all planned pages listed in entities.pages
|
||||
Phase 1 complete: all planned variables listed in entities.variables with correct scopes
|
||||
Phase 2 complete: all structural pages + foundations doc frames present
|
||||
Phase 3 complete (per component): componentSet exists + no pending validations + user checkpoint recorded
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Failure Taxonomy
|
||||
|
||||
### Recoverable Errors
|
||||
|
||||
These can be fixed and retried without affecting already-created entities:
|
||||
|
||||
| Category | Examples | Recovery |
|
||||
|---|---|---|
|
||||
| Layout errors | Variants stacked at (0,0), wrong padding values | Re-run the positioning step only |
|
||||
| Naming issues | Typo in variant name, wrong casing | Find nodes by `dsb_key`, update `name` property |
|
||||
| Missing property wiring | `componentPropertyReferences` not set | Find component set by ID, re-run the property wiring step |
|
||||
| Variable binding omission | A fill was hardcoded instead of bound | Find nodes by `dsb_key`, re-bind the fill |
|
||||
| Wrong variable bound | Bound to wrong variable ID | Re-bind with correct variable ID |
|
||||
| Text not visible | Font not loaded before text write | Re-run text creation with `loadFontAsync` first |
|
||||
| Script timeout | Script exceeded time limit before completing | Script is atomic — nothing was created. Reduce scope (fewer nodes per call) and retry |
|
||||
|
||||
### Structural Corruption (Requires Rollback or Restart)
|
||||
|
||||
These errors leave the file in a state where continuing forward is unreliable:
|
||||
|
||||
| Category | Examples | Recovery |
|
||||
|---|---|---|
|
||||
| Component cycle | A component instance was accidentally nested inside itself | Full cleanup of the affected component, restart that component from Call 1 |
|
||||
| combineAsVariants with non-components | Mixed node types passed to combineAsVariants, causing unexpected merges | Remove the malformed component set, re-run from variant creation |
|
||||
| Variable collection ID drift | Collection was deleted and re-created, old IDs in state ledger are stale | Re-run Phase 1 completely; update all IDs in state ledger |
|
||||
| Page deletion | A page was deleted after component sets were created on it | Treat as Phase 2 incomplete; re-create the page + re-run affected component creations |
|
||||
| Mode limit exceeded | `addMode` threw because the plan is Starter or Professional | Redesign variable collection architecture to fit mode limits, restart Phase 1 |
|
||||
|
||||
**Recovery from structural corruption**: run `cleanupOrphans` for the entire run ID, then restart from the affected phase. Do NOT attempt to patch corrupted structure in-place.
|
||||
|
||||
---
|
||||
|
||||
## 7. Common Error Table
|
||||
|
||||
| Error message | Likely cause | Fix |
|
||||
|---|---|---|
|
||||
| `"Cannot create component from node"` | Tried to call `createComponentFromNode` on a node inside a component | Create a fresh component instead: `figma.createComponent()` |
|
||||
| `"in addMode: Limited to N modes only"` | Plan mode limit hit (Starter=1, Professional=4) | Redesign to use fewer modes or upgrade plan |
|
||||
| `"setCurrentPageAsync: page does not exist"` | Page was deleted or wrong ID | Re-create the page using the idempotency pattern |
|
||||
| `"Cannot read properties of null"` | `getNodeByIdAsync` returned null — node was deleted | Run the resume protocol to find what exists, update state ledger |
|
||||
| `"Expected nodes to be component nodes"` | Passed a non-ComponentNode to `combineAsVariants` | Filter the array: `nodes.filter(n => n.type === 'COMPONENT')` |
|
||||
| `"in createVariable: Cannot create variable"` | Collection was deleted or ID is wrong | Verify collection exists with `getVariableCollectionByIdAsync` |
|
||||
| `"font not loaded"` | Called a text property setter without `loadFontAsync` first | Add `await figma.loadFontAsync({ family, style })` before the text operation |
|
||||
| `"Cannot set properties of a read-only array"` | Tried to mutate fills/strokes in-place | Clone first: `const fills = JSON.parse(JSON.stringify(node.fills))` |
|
||||
| `"Expected RGBA color"` | Color value out of 0–1 range | Divide RGB 0–255 values by 255: `{ r: 65/255, g: 85/255, b: 143/255 }` |
|
||||
| `"Cannot add children to a non-parent node"` | Tried to append a child to a leaf node (text, rect) | Ensure the parent is a FrameNode, ComponentNode, or GroupNode |
|
||||
| `"in combineAsVariants: nodes must be in the same parent"` | Components are on different pages | Move all components to the same page before combining |
|
||||
| `"Script exceeded time limit"` | Loop creating too many nodes in one call | Split the work: create N/2 variants per call |
|
||||
| Component set deletes itself | Tried to create a component set with no children | `combineAsVariants` requires at least 1 node — always pass 1+ |
|
||||
| `addComponentProperty` returns unexpected name | This is normal — `BOOLEAN`/`TEXT`/`INSTANCE_SWAP` get `#id:id` suffix | Save the returned key immediately and use that, not the input name |
|
||||
|
||||
---
|
||||
|
||||
## 8. Per-Phase Recovery Guidance
|
||||
|
||||
### Phase 1 fails (variable creation)
|
||||
|
||||
Since `use_figma` is atomic, a failed call creates nothing. The most common scenario is that some calls in Phase 1 succeeded (creating some variables) while a later call failed.
|
||||
|
||||
Recovery steps:
|
||||
1. Run inspection script to find all variables tagged with your `run_id`
|
||||
2. Compare against the plan to identify which variables were successfully created and which are still missing
|
||||
3. If a successfully created variable has wrong values, call `variable.remove()` and recreate it
|
||||
4. Fix the failed script and retry — it's safe since the failed call created nothing
|
||||
5. Do NOT proceed to Phase 2 until ALL planned variables exist with correct scopes and code syntax
|
||||
|
||||
**The most common Phase 1 failure:** script timeout when creating many variables. Fix: batch variable creation — create at most 20–30 variables per call.
|
||||
|
||||
### Phase 2 fails mid-execution (page/file structure)
|
||||
|
||||
Symptoms: some pages exist, others are missing; foundations doc frames are incomplete.
|
||||
|
||||
Recovery steps:
|
||||
1. Identify which pages were successfully created (check for `key` tags)
|
||||
2. Mark remaining pages as pending and create them in subsequent calls
|
||||
3. If a foundations doc frame is malformed, run `cleanupOrphans` for `dsb_phase: 'phase2'` on that page, then recreate
|
||||
|
||||
Phase 2 failures rarely require Phase 1 rollback unless the page structure itself is corrupted (which is unusual).
|
||||
|
||||
### Phase 3 fails (component creation)
|
||||
|
||||
This is the most common failure mode in long builds. Since `use_figma` is atomic, a failed call creates nothing — but previous successful calls in the component creation sequence will have created state. Handle by which call in the sequence failed:
|
||||
|
||||
```
|
||||
If failure in Call 1 (page creation):
|
||||
→ Nothing was created. Fix the script and retry.
|
||||
|
||||
If failure in Call 2 (doc frame):
|
||||
→ Call 1's page exists. Fix Call 2 and retry — idempotency check handles it.
|
||||
|
||||
If failure in Call 3 (base component):
|
||||
→ Calls 1-2 succeeded. Fix Call 3 and retry.
|
||||
|
||||
If failure in Call 4 (variant creation):
|
||||
→ Call 3's base component exists. Fix Call 4 and retry.
|
||||
→ If you need to restart from Call 3, clean up Call 3's nodes first
|
||||
using cleanupOrphans scoped to the component page.
|
||||
|
||||
If failure in Call 5 (combineAsVariants + layout):
|
||||
→ Variant ComponentNodes from Call 4 exist but aren't combined yet.
|
||||
→ Fix Call 5 and retry.
|
||||
→ If the component set was already created by a prior attempt of Call 5
|
||||
that succeeded, remove it first, then re-run.
|
||||
|
||||
If failure in Call 6 (component properties):
|
||||
→ The component set already exists and is structurally sound.
|
||||
→ Fix Call 6 and retry — addComponentProperty is safe to retry if
|
||||
you first check componentPropertyDefinitions for existing properties.
|
||||
→ Idempotency check: if 'Label' property already exists, skip addComponentProperty.
|
||||
```
|
||||
|
||||
**Idempotency for component properties (Call 6 retry):**
|
||||
|
||||
```javascript
|
||||
const existingDefs = cs.componentPropertyDefinitions;
|
||||
const labelKey = existingDefs['Label']
|
||||
? Object.keys(existingDefs).find(k => k.startsWith('Label'))
|
||||
: cs.addComponentProperty('Label', 'TEXT', 'Button');
|
||||
```
|
||||
|
||||
### Phase 4 fails mid-execution (QA / Code Connect)
|
||||
|
||||
Phase 4 is non-destructive. Failures here do not corrupt Phase 3 work. Common failures:
|
||||
|
||||
- **Accessibility audit finds contrast failures:** do not attempt auto-fix. Report the specific variable IDs and token names that fail, then ask the user which value to update.
|
||||
- **Naming audit finds duplicates:** list all duplicates with their `key` values, ask user which to keep, then remove the duplicates.
|
||||
- **Code Connect mapping fails:** treat as incomplete, not broken. Continue and leave as pending.
|
||||
+527
@@ -0,0 +1,527 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Naming Conventions Reference
|
||||
|
||||
This reference documents every naming convention used in the figma-generate-library workflow. Cover all naming decisions in order: variables, components, pages, variants, styles, separators, status indicators. The last section explains when to match an existing file's conventions vs. using the defaults here.
|
||||
|
||||
---
|
||||
|
||||
## 1. Variable Naming
|
||||
|
||||
### Slash hierarchy (the universal pattern)
|
||||
|
||||
All Figma variables use slash-separated paths. The slash creates visual grouping in the Variables panel and maps directly to the token hierarchy in code.
|
||||
|
||||
```
|
||||
{category}/{subcategory}/{role}
|
||||
```
|
||||
|
||||
Real examples from Simple DS and Material 3:
|
||||
|
||||
```
|
||||
color/bg/primary
|
||||
color/bg/secondary
|
||||
color/text/primary
|
||||
color/text/muted
|
||||
color/border/default
|
||||
color/border/focus
|
||||
color/feedback/error
|
||||
color/feedback/success
|
||||
spacing/xs
|
||||
spacing/sm
|
||||
spacing/md
|
||||
spacing/lg
|
||||
spacing/xl
|
||||
spacing/2xl
|
||||
radius/none
|
||||
radius/sm
|
||||
radius/md
|
||||
radius/lg
|
||||
radius/full
|
||||
typography/body/font-size
|
||||
typography/body/line-height
|
||||
typography/heading/font-size
|
||||
typography/heading/font-weight
|
||||
```
|
||||
|
||||
### Primitives collection
|
||||
|
||||
Primitive variables hold raw values and are **not** exposed to consumers (scope = `[]`). They use a flat `{family}/{step}` format matching the color scale convention from Simple DS:
|
||||
|
||||
```
|
||||
blue/50
|
||||
blue/100
|
||||
blue/200
|
||||
...
|
||||
blue/900
|
||||
gray/50
|
||||
gray/100
|
||||
...
|
||||
gray/900
|
||||
red/500
|
||||
green/500
|
||||
```
|
||||
|
||||
Step numbers follow the convention of the target codebase. If the codebase uses `100–900`, use that. If it uses `50–950`, use that. If there is no codebase convention, use `100–900` in increments of 100.
|
||||
|
||||
### Semantic collection
|
||||
|
||||
Semantic variables alias primitives. They use the role-based `{category}/{role}` or `{category}/{subcategory}/{role}` pattern:
|
||||
|
||||
```
|
||||
color/bg/primary → alias: primitives/white (light), primitives/gray/900 (dark)
|
||||
color/bg/secondary → alias: primitives/gray/100 (light), primitives/gray/800 (dark)
|
||||
color/text/primary → alias: primitives/gray/900 (light), primitives/white (dark)
|
||||
color/text/secondary → alias: primitives/gray/600 (light), primitives/gray/400 (dark)
|
||||
color/border/default → alias: primitives/gray/200 (light), primitives/gray/700 (dark)
|
||||
```
|
||||
|
||||
**Rule:** Semantic variables must never hold raw hex values — they always alias a primitive. If you need a new color value, create the primitive first, then create the semantic alias.
|
||||
|
||||
### Casing
|
||||
|
||||
**Default:** Use **lowercase** with forward slashes: `color/bg/primary`, `spacing/2xl`.
|
||||
|
||||
**When to deviate:**
|
||||
- If the existing file uses PascalCase (e.g., Material 3 uses `Schemes/Primary`) — match it.
|
||||
- If the design team prefers PascalCase for readability in the Variables panel — acceptable as long as the code syntax is separately defined and uses the platform-correct case.
|
||||
- Mode names can use spaces and mixed case (e.g., `SDS Light`, `Mode 1 → Light`) — these are labels, not identifiers.
|
||||
|
||||
**Never:** camelCase inside variable names (`colorBgPrimary` as a Figma name is wrong — that belongs in Android code syntax only). Never use spaces inside a path segment: `color/bg primary` is wrong; `color/bg/primary` is correct.
|
||||
|
||||
**Key distinction:** The casing rule applies to *Figma variable names*. Code syntax names follow *platform conventions* regardless of the Figma name case — see §9 for the full picture.
|
||||
|
||||
---
|
||||
|
||||
## 2. Component Naming
|
||||
|
||||
### Main components: PascalCase, no prefix
|
||||
|
||||
Published components intended for library consumers use plain PascalCase names:
|
||||
|
||||
```
|
||||
Button
|
||||
Input
|
||||
Checkbox
|
||||
Toggle
|
||||
Avatar
|
||||
Badge
|
||||
Card
|
||||
Dialog
|
||||
Tooltip
|
||||
Banner
|
||||
```
|
||||
|
||||
Do not use a namespace prefix for public components (e.g., do not name them `DS/Button` or `sds-Button`). Slashes in component names create nested grouping in the Assets panel, which is correct for sub-components but not for top-level public components.
|
||||
|
||||
### Sub-components: underscore prefix + slash namespace
|
||||
|
||||
Internal sub-components that are NOT meant for library consumers use the `_` prefix. This hides them from the Assets panel by default and signals to other designers that they should not be used directly.
|
||||
|
||||
```
|
||||
_Button/Slot (internal icon slot for Button)
|
||||
_Input/Indicator (internal state indicator for Input)
|
||||
_Badge/Dot (internal dot sub-component of Badge)
|
||||
_Parts/Avatar.Status (UI3 pattern: _Parts/{ParentName}.{SubPart})
|
||||
_Slider/Handle (UI3 pattern: _{ParentName}/{SubPart})
|
||||
```
|
||||
|
||||
Pattern rules:
|
||||
- Use `_` prefix for ALL internal sub-components — no exceptions.
|
||||
- Use slash namespacing to group sub-components under their parent: `_Button/IconSlot`.
|
||||
- For sub-components shared by multiple parents, use `_Parts/{ComponentName}.{SubPart}`.
|
||||
|
||||
### Private documentation components
|
||||
|
||||
Components used only for internal documentation (not for production use) use the `.` prefix:
|
||||
|
||||
```
|
||||
.ExampleCard
|
||||
.GuidelineHeader
|
||||
.DemoFrame
|
||||
```
|
||||
|
||||
This hides them from consumers while keeping them accessible on the canvas.
|
||||
|
||||
---
|
||||
|
||||
## 3. Page Naming
|
||||
|
||||
Five reference design systems use three distinct naming patterns. Choose one pattern and apply it consistently across all pages in the file.
|
||||
|
||||
### Pattern 1: Plain names (Simple DS, Material 3, Polaris)
|
||||
|
||||
The most common pattern. Clean, readable, no decoration.
|
||||
|
||||
```
|
||||
Cover
|
||||
---
|
||||
Foundations
|
||||
Icons
|
||||
---
|
||||
Accordion
|
||||
Avatars
|
||||
Buttons
|
||||
Cards
|
||||
Dialog
|
||||
Inputs
|
||||
Menu
|
||||
---
|
||||
Utilities
|
||||
Component Playground
|
||||
```
|
||||
|
||||
Use this pattern when starting from scratch or when the target file already uses this style.
|
||||
|
||||
### Pattern 2: Emoji prefix + status (UI3 Library)
|
||||
|
||||
The most expressive pattern. The page name encodes asset type, design status, and code readiness.
|
||||
|
||||
Anatomy: `[Asset Type Emoji] [Optional FPL Label] [Status Circle] Component Name [Code Status Bracket]`
|
||||
|
||||
| Segment | Values |
|
||||
|---------|--------|
|
||||
| Asset type | Component pages use the C-flag emoji; pattern pages use the P-flag emoji |
|
||||
| Design status | Green circle = Ready, Yellow circle = WIP, Red circle = Do not use |
|
||||
| Code status | (none) = Ready in code, `[beta]` = Beta, `[future]` = Not yet built |
|
||||
|
||||
Examples:
|
||||
```
|
||||
Overview
|
||||
Status Key
|
||||
---
|
||||
FPL COMPONENTS (go/fpl)
|
||||
[C-flag] FPL [Green] Buttons
|
||||
[C-flag] FPL [Green] Inputs
|
||||
[C-flag] FPL [Yellow] Popovers [future]
|
||||
---
|
||||
UI3 COMPONENTS
|
||||
[C-flag] [Green] Comments
|
||||
---
|
||||
PATTERNS
|
||||
[P-flag] [Green] Editor / Layers
|
||||
---
|
||||
[Book] Cover
|
||||
[Headstone] Deprecated
|
||||
```
|
||||
|
||||
Use this pattern only when building a large, multi-team design system where lifecycle tracking is needed, or when the target file already uses it.
|
||||
|
||||
### Pattern 3: Emoji prefix (Shop Minis)
|
||||
|
||||
A lighter version of the UI3 pattern without status circles.
|
||||
|
||||
```
|
||||
📔 Cover
|
||||
ℹ️ About
|
||||
🚀 Getting started
|
||||
——— THEME ———
|
||||
Color
|
||||
Typography
|
||||
Spacing
|
||||
——— COMPONENTS ———
|
||||
Button
|
||||
Input
|
||||
Card
|
||||
```
|
||||
|
||||
Use this pattern when the target file already uses emoji prefixes but does not need lifecycle tracking.
|
||||
|
||||
### Universal rules (all patterns)
|
||||
|
||||
- **Cover** is always first.
|
||||
- **Separator pages** come before and after each logical section.
|
||||
- **Foundation/token pages** always come before component pages.
|
||||
- **Utility and internal pages** always come last.
|
||||
- Pick one convention and do not mix patterns within a file.
|
||||
|
||||
---
|
||||
|
||||
## 4. Variant Naming
|
||||
|
||||
### Property=Value format
|
||||
|
||||
All component variant properties and their values use `Property=Value` format in the Figma component set:
|
||||
|
||||
```
|
||||
Size=Small, Style=Primary, State=Default
|
||||
Size=Medium, Style=Secondary, State=Hover
|
||||
Size=Large, Style=Ghost, State=Disabled
|
||||
```
|
||||
|
||||
Actual property names match code prop names where possible:
|
||||
|
||||
| Figma Property | Code Prop Equivalent |
|
||||
|---------------|---------------------|
|
||||
| `Size` | `size` |
|
||||
| `Style` / `Variant` | `variant` |
|
||||
| `State` | Typically controlled by `:hover`, `:focus`, `:disabled` in CSS, but `state` in some systems |
|
||||
| `Type` | `type` |
|
||||
| `Disabled` | `disabled` (boolean) |
|
||||
| `Icon` | `icon` (boolean or instance swap) |
|
||||
|
||||
### Property value casing
|
||||
|
||||
Property values use **Title Case** in Figma (to be readable in the Variants panel), mapping to lowercase in code:
|
||||
|
||||
| Figma value | Code value |
|
||||
|-------------|-----------|
|
||||
| `Small` | `"small"` / `"sm"` |
|
||||
| `Medium` | `"medium"` / `"md"` |
|
||||
| `Large` | `"large"` / `"lg"` |
|
||||
| `Primary` | `"primary"` |
|
||||
| `Disabled` | `disabled` (boolean prop) |
|
||||
| `Default` | *(typically the absent/unset case)* |
|
||||
|
||||
### Boolean properties
|
||||
|
||||
Boolean component properties in Figma use `true` / `false` as values (Figma's native boolean), not `Yes` / `No` or `On` / `Off`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Style Naming (Text and Effect Styles)
|
||||
|
||||
### Text styles: category/name
|
||||
|
||||
```
|
||||
Display/Large
|
||||
Display/Medium
|
||||
Display/Small
|
||||
Heading/1
|
||||
Heading/2
|
||||
Heading/3
|
||||
Body/Large
|
||||
Body/Medium
|
||||
Body/Small
|
||||
Label/Large
|
||||
Label/Small
|
||||
Code/Inline
|
||||
```
|
||||
|
||||
The category segment maps to the typographic role. Use the same category names as the codebase's typography scale where possible.
|
||||
|
||||
### Effect styles (shadows): category/name
|
||||
|
||||
```
|
||||
Shadow/None
|
||||
Shadow/Subtle
|
||||
Shadow/Medium
|
||||
Shadow/Strong
|
||||
Shadow/Overlay
|
||||
Elevation/0
|
||||
Elevation/1
|
||||
Elevation/2
|
||||
Elevation/3
|
||||
Elevation/4
|
||||
Elevation/5
|
||||
```
|
||||
|
||||
Use `Shadow/` for named semantic shadows. Use `Elevation/N` for Material Design-style numbered elevation levels.
|
||||
|
||||
---
|
||||
|
||||
## 6. Separator Pages
|
||||
|
||||
Separator pages are empty pages whose sole purpose is to create visual breaks in the Figma page panel. Two conventions:
|
||||
|
||||
| Convention | Example | Used by |
|
||||
|------------|---------|---------|
|
||||
| Three dashes | `---` | Simple DS, UI3, Polaris, Material 3 |
|
||||
| Decorated text | `——— COMPONENTS ———` | Shop Minis |
|
||||
|
||||
The three-dash convention (`---`) is the most common and the default for new files. Use it unless the target file uses the decorated-text style.
|
||||
|
||||
**Where to place separators:**
|
||||
|
||||
```
|
||||
Cover
|
||||
--- ← after cover
|
||||
Foundations
|
||||
Icons
|
||||
--- ← before components
|
||||
[component pages]
|
||||
--- ← before utilities
|
||||
Utilities
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Status Indicators (UI3 Emoji System)
|
||||
|
||||
The UI3 Library uses colored circle emojis in page names to communicate design readiness at a glance. This system is optional but powerful for large teams.
|
||||
|
||||
| Emoji | Meaning | When to use |
|
||||
|-------|---------|-------------|
|
||||
| Green circle | Ready / Approved | Design is stable, reviewed, and safe to use |
|
||||
| Yellow circle | WIP / In Progress | Design is being actively worked on, may change |
|
||||
| Red circle | Do not use | Not ready, do not reference; may be deprecated |
|
||||
|
||||
Code readiness is communicated via brackets appended to the component name:
|
||||
|
||||
| Bracket | Meaning |
|
||||
|---------|---------|
|
||||
| (none) | Component is implemented in code and stable |
|
||||
| `[beta]` | Component is in code but not yet stable (~3 weeks from ready) |
|
||||
| `[future]` | Not yet implemented in code |
|
||||
|
||||
**Documentation status (within component pages):**
|
||||
|
||||
If building a UI3-style system, each documentation frame gets a status banner with one of these labels:
|
||||
|
||||
- `APPROVED` — fully vetted
|
||||
- `READY FOR REVIEW` — awaiting sign-off
|
||||
- `WORK IN PROGRESS` — actively being designed
|
||||
- `NEEDS UPDATE` — outdated, requires revision
|
||||
- `DO NOT REFERENCE` — should not be used
|
||||
|
||||
This system is only recommended for large, multi-team systems where lifecycle tracking provides real value. For smaller systems, skip the emoji status indicators and use plain page names.
|
||||
|
||||
---
|
||||
|
||||
## 8. When to Match Existing vs. Use Defaults
|
||||
|
||||
**Always inspect before naming anything.** Run `get_metadata` or `inspectFileStructure` to discover existing conventions before creating any pages or variables.
|
||||
|
||||
### Match the existing file when:
|
||||
|
||||
- The file already has pages with a consistent naming pattern (emoji prefixes, separator style, casing).
|
||||
- The file already has variable collections with an established naming scheme.
|
||||
- The file was started by a design team and carries intentional decisions.
|
||||
- Any existing component names use a specific pattern (PascalCase, kebab-case, namespace prefixes).
|
||||
|
||||
### Use the defaults from this document when:
|
||||
|
||||
- Starting a brand-new Figma file with no existing content.
|
||||
- The existing conventions are inconsistent (mix of styles = no convention to match).
|
||||
- The user explicitly asks for a fresh design system following best practices.
|
||||
|
||||
### When code and Figma disagree:
|
||||
|
||||
If the codebase uses `button-primary` but Figma has a component named `Button`, do not rename the Figma component. Instead:
|
||||
- Keep the Figma name as `Button` (PascalCase, human-readable).
|
||||
- Set variable code syntax to match the exact CSS token name from the codebase.
|
||||
- Set Code Connect source path to the actual code file and use the exact code component name.
|
||||
|
||||
**The rule:** Figma names are for designers; code syntax and Code Connect source paths carry the exact code identifiers. These two identity systems operate in parallel.
|
||||
|
||||
---
|
||||
|
||||
## 9. Figma Variable Names vs Code Names — The Full Picture
|
||||
|
||||
This is one of the most misunderstood areas. Figma names and code names follow **different conventions on purpose** — they serve different audiences and live in different environments.
|
||||
|
||||
### Why they differ
|
||||
|
||||
| | Figma variable name | Code syntax (WEB) |
|
||||
|---|---|---|
|
||||
| **Audience** | Designers in the Variables panel | Developers in CSS/Swift/Kotlin |
|
||||
| **Separator** | `/` (slash) — creates visual grouping in Figma UI | `-` (hyphen) — required by CSS custom property syntax |
|
||||
| **Case** | lowercase (or PascalCase for display — see below) | kebab-case for CSS; camelCase for JS/Android |
|
||||
| **Depth** | 2–4 levels | Flat for CSS; dot-notation for JS |
|
||||
| **Namespace** | Implicit (by collection) | Explicit prefix (`--p-`, `--md-`, `--cds-`) |
|
||||
|
||||
### The transformation
|
||||
|
||||
```
|
||||
Figma variable name Code syntax (WEB)
|
||||
────────────────── ─────────────────
|
||||
color/bg/primary → var(--color-bg-primary)
|
||||
spacing/xs → var(--spacing-xs)
|
||||
radius/md → var(--radius-md)
|
||||
typography/body/font-size → var(--typography-body-font-size)
|
||||
|
||||
Pattern: replace "/" with "-", wrap in var(--)
|
||||
|
||||
**CRITICAL: The `var()` wrapper is REQUIRED for WEB code syntax.** Figma expects the full CSS function syntax — not just the property name. If you set `--color-bg-primary` (without `var()`), Dev Mode will show raw hex values instead of the variable reference. Always set `var(--color-bg-primary)`.
|
||||
```
|
||||
|
||||
```
|
||||
Figma variable name Code syntax (ANDROID)
|
||||
────────────────── ─────────────────────
|
||||
color/bg/primary → colorBgPrimary
|
||||
spacing/xs → spacingXs
|
||||
radius/md → radiusMd
|
||||
|
||||
Pattern: replace "/" with "", capitalize each word after first
|
||||
```
|
||||
|
||||
```
|
||||
Figma variable name Code syntax (iOS)
|
||||
────────────────── ─────────────────
|
||||
color/bg/primary → Color.bgPrimary
|
||||
spacing/xs → Spacing.xs
|
||||
radius/md → Radius.md
|
||||
|
||||
Pattern: first segment becomes class name, remainder becomes property (camelCase)
|
||||
```
|
||||
|
||||
### Real-world examples from the 5 reference files
|
||||
|
||||
| File | Figma variable name | WEB code syntax | ANDROID code syntax |
|
||||
|------|--------------------|-----------------|--------------------|
|
||||
| Simple DS | `color/bg/primary` | `var(--color-bg-primary)` | `colorBgPrimary` |
|
||||
| Simple DS | `spacing/sm` | `var(--spacing-sm)` | `spacingSm` |
|
||||
| Material 3 | `Schemes/Primary` | `var(--md-sys-color-primary)` | `colorPrimary` |
|
||||
| Material 3 | `Corner/Extra-small` | `var(--md-sys-shape-corner-extra-small)` | `shapeCornerExtraSmall` |
|
||||
| Polaris | `color/bg/surface` | `var(--p-color-bg-surface)` | — |
|
||||
|
||||
**Key observation from Material 3:** The Figma name `Schemes/Primary` uses PascalCase with a space, but the WEB code syntax is `var(--md-sys-color-primary)` — entirely kebab-case with a vendor prefix `md-sys-`. The Figma name and the code syntax bear almost no resemblance. This is intentional and common in mature design systems.
|
||||
|
||||
### Casing in Figma: lowercase is default, PascalCase is valid for display
|
||||
|
||||
The guideline to use lowercase is a default, not a universal rule. Evidence from real files:
|
||||
|
||||
| File | Figma case | Code output case | Why |
|
||||
|------|-----------|------------------|-----|
|
||||
| Simple DS | `color/bg/primary` (lowercase) | `var(--color-bg-primary)` | Direct mapping — simple |
|
||||
| Material 3 | `Schemes/Primary` (PascalCase) | `var(--md-sys-color-primary)` | PascalCase reads better in Variables panel; code name is independently defined |
|
||||
| Polaris | `color/bg/surface` (lowercase) | `var(--p-color-bg-surface)` | Direct mapping with vendor prefix |
|
||||
|
||||
**Rule:** Use lowercase when the Figma name will map directly to the CSS name. Use PascalCase (or match existing file) when the design system has human-readable variable names that are distinct from the technical code names.
|
||||
|
||||
### When the codebase doesn't use CSS custom properties
|
||||
|
||||
Some JavaScript-first systems (Chakra, Ant Design, MUI) don't use CSS `var(--...)` at all. Their tokens live in JS theme objects:
|
||||
|
||||
```
|
||||
Chakra: colors.gray[500] → JS: theme.colors.gray[500]
|
||||
Ant: colorPrimary → JS: token.colorPrimary
|
||||
MUI: palette.primary.main → JS: theme.palette.primary.main
|
||||
```
|
||||
|
||||
In these cases, set WEB code syntax to the JS property path rather than a CSS variable:
|
||||
```javascript
|
||||
// For a JS-object-based system like Chakra:
|
||||
v.setVariableCodeSyntax('WEB', 'colors.gray.500');
|
||||
|
||||
// For Ant Design:
|
||||
v.setVariableCodeSyntax('WEB', 'colorPrimary');
|
||||
```
|
||||
|
||||
### Hierarchy depth: match the codebase
|
||||
|
||||
The number of slash levels should mirror the codebase's nesting depth:
|
||||
|
||||
| Codebase pattern | Figma depth | Example |
|
||||
|-----------------|------------|---------|
|
||||
| `--primary` (flat) | 1–2 levels | `color/primary` |
|
||||
| `--color-bg-surface` (3-part) | 3 levels | `color/bg/surface` |
|
||||
| `--md-sys-color-primary` (vendor + 3-part) | 3 levels (vendor prefix goes in code syntax only) | `color/primary` |
|
||||
| `theme.palette.primary.main` (4-part) | 3–4 levels | `color/palette/primary/main` |
|
||||
|
||||
**Important:** Vendor prefixes (`--p-`, `--md-sys-`, `--cds-`) belong in the **code syntax**, not the Figma variable name. The Figma name `color/bg/surface` + code syntax `var(--p-color-bg-surface)` is the correct pattern.
|
||||
|
||||
### Action at discovery time
|
||||
|
||||
During Phase 0 discovery, capture both sides of the mapping explicitly:
|
||||
|
||||
```
|
||||
For each token found in the codebase:
|
||||
CSS variable: --sds-color-background-brand-default
|
||||
Figma name: color/bg/brand/default (slash hierarchy, no vendor prefix)
|
||||
WEB syntax: var(--sds-color-background-brand-default) (exact CSS name)
|
||||
ANDROID syntax: sdsColorBackgroundBrandDefault (camelCase)
|
||||
iOS syntax: Color.backgroundBrandDefault (dot-notation)
|
||||
```
|
||||
|
||||
Store this mapping in the state ledger. Use it when calling `setVariableCodeSyntax` in Phase 1. Never derive the code syntax from the Figma name if you have the original CSS variable name — always use the original.
|
||||
+888
@@ -0,0 +1,888 @@
|
||||
> Part of the [figma-generate-library skill](../SKILL.md).
|
||||
|
||||
# Token Creation Reference
|
||||
|
||||
This document covers Phase 1: creating variable collections, modes, primitives, semantic aliases, scopes, code syntax, styles, and validation. All code is copy-paste ready for `use_figma`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Collection Architecture
|
||||
|
||||
Choose the pattern that matches your token count and complexity:
|
||||
|
||||
### Simple Pattern (< 50 tokens)
|
||||
|
||||
One collection, 2 modes. Appropriate for small projects or brand kits.
|
||||
|
||||
```
|
||||
Collection: "Tokens" modes: ["Light", "Dark"]
|
||||
color/bg/primary → Light: #FFFFFF, Dark: #1A1A1A
|
||||
spacing/sm = 8
|
||||
```
|
||||
|
||||
### Standard Pattern (50–200 tokens) — Recommended Starting Point
|
||||
|
||||
Separate primitives from semantics. The real-world reference is Figma's Simple Design System (SDS): 7 collections, 368 variables, light/dark modes on semantic colors, single-mode primitives.
|
||||
|
||||
```
|
||||
Collection: "Primitives" modes: ["Value"] ← raw hex values, no modes
|
||||
blue/500 = #3B82F6
|
||||
gray/900 = #111827
|
||||
white/1000 = #FFFFFF
|
||||
|
||||
Collection: "Color" modes: ["Light", "Dark"] ← aliases to Primitives
|
||||
color/bg/primary → Light: alias Primitives/white/1000, Dark: alias Primitives/gray/900
|
||||
color/text/primary → Light: alias Primitives/gray/900, Dark: alias Primitives/white/1000
|
||||
|
||||
Collection: "Spacing" modes: ["Value"]
|
||||
spacing/xs = 4, spacing/sm = 8, spacing/md = 16, spacing/lg = 24, spacing/xl = 32
|
||||
|
||||
Collection: "Typography Primitives" modes: ["Value"]
|
||||
family/sans = "Inter", scale/01 = 12, scale/02 = 14, scale/03 = 16, weight/regular = 400
|
||||
|
||||
Collection: "Typography" modes: ["Value"] ← aliases to Typography Primitives
|
||||
body/font-family → alias family/sans
|
||||
body/size-md → alias scale/03
|
||||
```
|
||||
|
||||
### Advanced Pattern (200+ tokens) — M3 Model
|
||||
|
||||
Multiple semantic collections, 4–8 modes. Use when you need light/dark × contrast × brand or responsive breakpoints.
|
||||
|
||||
```
|
||||
Collection: "M3" modes: ["Light", "Dark", "Light High Contrast", "Dark High Contrast", ...]
|
||||
Collection: "Typeface" modes: ["Baseline", "Wireframe"]
|
||||
Collection: "Typescale" modes: ["Value"] ← aliases into Typeface
|
||||
Collection: "Shape" modes: ["Value"]
|
||||
```
|
||||
|
||||
Key insight from M3: ALL 196 semantic color variables live in a SINGLE collection with 8 modes. Switching a frame's mode once updates every color simultaneously.
|
||||
|
||||
---
|
||||
|
||||
## 2. Creating Collections + Modes
|
||||
|
||||
### Creating a Primitives Collection
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001"; // use the same RUN_ID throughout the build
|
||||
|
||||
// Create the collection
|
||||
const primColl = figma.variables.createVariableCollection("Primitives");
|
||||
|
||||
// Rename the default "Mode 1" to "Value"
|
||||
primColl.renameMode(primColl.modes[0].modeId, "Value");
|
||||
const valueMode = primColl.modes[0].modeId;
|
||||
|
||||
// Tag for idempotency
|
||||
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
primColl.setSharedPluginData('dsb', 'key', 'collection/primitives');
|
||||
|
||||
return {
|
||||
collectionId: primColl.id,
|
||||
modeId: valueMode,
|
||||
name: primColl.name
|
||||
};
|
||||
```
|
||||
|
||||
### Creating a Semantic Color Collection with Light/Dark Modes
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
const colorColl = figma.variables.createVariableCollection("Color");
|
||||
|
||||
// Rename default "Mode 1" to "Light"
|
||||
colorColl.renameMode(colorColl.modes[0].modeId, "Light");
|
||||
const lightModeId = colorColl.modes[0].modeId;
|
||||
|
||||
// Add "Dark" mode — requires Professional plan or higher
|
||||
// Throws "in addMode: Limited to N modes only" on Starter plan
|
||||
const darkModeId = colorColl.addMode("Dark");
|
||||
|
||||
colorColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
colorColl.setSharedPluginData('dsb', 'key', 'collection/color');
|
||||
|
||||
return {
|
||||
collectionId: colorColl.id,
|
||||
lightModeId,
|
||||
darkModeId
|
||||
};
|
||||
```
|
||||
|
||||
**Mode plan limits:** Starter = 1 mode, Professional = 4 modes, Organization/Enterprise = 40 modes. If `addMode` throws, the file is on a Starter plan — tell the user and ask how to proceed.
|
||||
|
||||
### Creating a Spacing Collection (single mode)
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
const spacingColl = figma.variables.createVariableCollection("Spacing");
|
||||
spacingColl.renameMode(spacingColl.modes[0].modeId, "Value");
|
||||
const valueMode = spacingColl.modes[0].modeId;
|
||||
|
||||
spacingColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
spacingColl.setSharedPluginData('dsb', 'key', 'collection/spacing');
|
||||
|
||||
return {
|
||||
collectionId: spacingColl.id,
|
||||
modeId: valueMode
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Creating All Variable Types
|
||||
|
||||
### hex → {r, g, b} Conversion Helper
|
||||
|
||||
Colors in the Figma Plugin API are 0–1 range, not 0–255. Embed this helper in any script that creates color variables:
|
||||
|
||||
```javascript
|
||||
function hexToRgb(hex) {
|
||||
const clean = hex.replace('#', '');
|
||||
return {
|
||||
r: parseInt(clean.substring(0, 2), 16) / 255,
|
||||
g: parseInt(clean.substring(2, 4), 16) / 255,
|
||||
b: parseInt(clean.substring(4, 6), 16) / 255
|
||||
};
|
||||
}
|
||||
|
||||
// With alpha channel (for semi-transparent primitives like Black/200 at 10%):
|
||||
function hexToRgba(hex) {
|
||||
const clean = hex.replace('#', '');
|
||||
const hasAlpha = clean.length === 8;
|
||||
return {
|
||||
r: parseInt(clean.substring(0, 2), 16) / 255,
|
||||
g: parseInt(clean.substring(2, 4), 16) / 255,
|
||||
b: parseInt(clean.substring(4, 6), 16) / 255,
|
||||
a: hasAlpha ? parseInt(clean.substring(6, 8), 16) / 255 : 1
|
||||
};
|
||||
}
|
||||
|
||||
// Usage:
|
||||
// hexToRgb('#3B82F6') → {r: 0.231, g: 0.510, b: 0.965}
|
||||
// hexToRgb('#14AE5C') → {r: 0.078, g: 0.682, b: 0.361}
|
||||
// hexToRgba('#0c0c0d1a') → {r: 0.047, g: 0.047, b: 0.051, a: 0.102}
|
||||
```
|
||||
|
||||
### Creating Primitive Color Variables (Real SDS Data)
|
||||
|
||||
This creates a subset of the Simple Design System's `Color Primitives` collection (Blue family, from the Standard pattern used by real design systems):
|
||||
|
||||
```javascript
|
||||
function hexToRgb(hex) {
|
||||
const c = hex.replace('#', '');
|
||||
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
|
||||
}
|
||||
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Get the Primitives collection created in the previous step
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
|
||||
if (!primColl) throw new Error("Primitives collection not found — run collection creation first");
|
||||
const valueMode = primColl.modes[0].modeId;
|
||||
|
||||
// Define primitives — use real values from your codebase
|
||||
const primitiveColors = [
|
||||
// Blue scale
|
||||
{ name: 'blue/100', hex: '#EFF6FF' },
|
||||
{ name: 'blue/200', hex: '#DBEAFE' },
|
||||
{ name: 'blue/300', hex: '#93C5FD' },
|
||||
{ name: 'blue/400', hex: '#60A5FA' },
|
||||
{ name: 'blue/500', hex: '#3B82F6' },
|
||||
{ name: 'blue/600', hex: '#2563EB' },
|
||||
{ name: 'blue/700', hex: '#1D4ED8' },
|
||||
{ name: 'blue/800', hex: '#1E40AF' },
|
||||
{ name: 'blue/900', hex: '#1E3A8A' },
|
||||
// Gray scale
|
||||
{ name: 'gray/100', hex: '#F9FAFB' },
|
||||
{ name: 'gray/200', hex: '#F3F4F6' },
|
||||
{ name: 'gray/300', hex: '#D1D5DB' },
|
||||
{ name: 'gray/400', hex: '#9CA3AF' },
|
||||
{ name: 'gray/500', hex: '#6B7280' },
|
||||
{ name: 'gray/600', hex: '#4B5563' },
|
||||
{ name: 'gray/700', hex: '#374151' },
|
||||
{ name: 'gray/800', hex: '#1F2937' },
|
||||
{ name: 'gray/900', hex: '#111827' },
|
||||
// White / Black
|
||||
{ name: 'white/1000', hex: '#FFFFFF' },
|
||||
{ name: 'black/1000', hex: '#000000' },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, hex } of primitiveColors) {
|
||||
const v = figma.variables.createVariable(name, primColl, 'COLOR');
|
||||
v.setValueForMode(valueMode, hexToRgb(hex));
|
||||
// Primitives: EMPTY scopes (hidden from all pickers — designers use semantics)
|
||||
v.scopes = [];
|
||||
// Code syntax from the actual CSS variable name
|
||||
v.setVariableCodeSyntax('WEB', `var(--color-${name.replace('/', '-')})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', `primitive/${name}`);
|
||||
created.push({ name, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
**Critical scope rule for primitives:** Set `v.scopes = []`. This hides primitives from every picker. Designers should only see semantic tokens. The exception is semi-transparent overlay primitives (Black/White with alpha) — those get `["EFFECT_COLOR"]` so they appear in shadow pickers.
|
||||
|
||||
### Creating FLOAT Variables (Spacing, Radius, Font Size)
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const spacingColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/spacing');
|
||||
if (!spacingColl) throw new Error("Spacing collection not found");
|
||||
const valueMode = spacingColl.modes[0].modeId;
|
||||
|
||||
const spacingTokens = [
|
||||
{ name: 'spacing/xs', value: 4, scope: 'GAP', cssVar: '--spacing-xs' },
|
||||
{ name: 'spacing/sm', value: 8, scope: 'GAP', cssVar: '--spacing-sm' },
|
||||
{ name: 'spacing/md', value: 16, scope: 'GAP', cssVar: '--spacing-md' },
|
||||
{ name: 'spacing/lg', value: 24, scope: 'GAP', cssVar: '--spacing-lg' },
|
||||
{ name: 'spacing/xl', value: 32, scope: 'GAP', cssVar: '--spacing-xl' },
|
||||
{ name: 'spacing/2xl', value: 48, scope: 'GAP', cssVar: '--spacing-2xl' },
|
||||
];
|
||||
|
||||
const radiusTokens = [
|
||||
{ name: 'radius/none', value: 0, scope: 'CORNER_RADIUS', cssVar: '--radius-none' },
|
||||
{ name: 'radius/sm', value: 4, scope: 'CORNER_RADIUS', cssVar: '--radius-sm' },
|
||||
{ name: 'radius/md', value: 8, scope: 'CORNER_RADIUS', cssVar: '--radius-md' },
|
||||
{ name: 'radius/lg', value: 16, scope: 'CORNER_RADIUS', cssVar: '--radius-lg' },
|
||||
{ name: 'radius/full', value: 9999, scope: 'CORNER_RADIUS', cssVar: '--radius-full' },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, value, scope, cssVar } of [...spacingTokens, ...radiusTokens]) {
|
||||
const v = figma.variables.createVariable(name, spacingColl, 'FLOAT');
|
||||
v.setValueForMode(valueMode, value);
|
||||
v.scopes = [scope];
|
||||
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', name);
|
||||
created.push({ name, value, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
### Creating STRING Variables (Font Family, Font Style)
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const typoPrimColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/typography-primitives');
|
||||
if (!typoPrimColl) throw new Error("Typography Primitives collection not found");
|
||||
const valueMode = typoPrimColl.modes[0].modeId;
|
||||
|
||||
const fontTokens = [
|
||||
{ name: 'family/sans', value: 'Inter', scope: 'FONT_FAMILY', cssVar: '--font-family-sans' },
|
||||
{ name: 'family/mono', value: 'Roboto Mono', scope: 'FONT_FAMILY', cssVar: '--font-family-mono' },
|
||||
// Font style strings — these are the Figma fontName.style values:
|
||||
{ name: 'weight/regular', value: 'Regular', scope: 'FONT_STYLE', cssVar: '--font-weight-regular' },
|
||||
{ name: 'weight/medium', value: 'Medium', scope: 'FONT_STYLE', cssVar: '--font-weight-medium' },
|
||||
{ name: 'weight/semibold', value: 'Semi Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-semibold' },
|
||||
{ name: 'weight/bold', value: 'Bold', scope: 'FONT_STYLE', cssVar: '--font-weight-bold' },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, value, scope, cssVar } of fontTokens) {
|
||||
const v = figma.variables.createVariable(name, typoPrimColl, 'STRING');
|
||||
v.setValueForMode(valueMode, value);
|
||||
v.scopes = [scope];
|
||||
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', `typo-prim/${name}`);
|
||||
created.push({ name, value, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
### Creating BOOLEAN Variables
|
||||
|
||||
BOOLEAN variables have no scopes (scopes are not supported for BOOLEAN type).
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const coll = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/tokens');
|
||||
if (!coll) throw new Error("Collection not found");
|
||||
const valueMode = coll.modes[0].modeId;
|
||||
|
||||
const v = figma.variables.createVariable('feature-flags/show-beta-badge', coll, 'BOOLEAN');
|
||||
v.setValueForMode(valueMode, false);
|
||||
// No scopes — BOOLEAN does not support scopes
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', 'feature-flags/show-beta-badge');
|
||||
|
||||
return { id: v.id, name: v.name };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Variable Aliasing (VARIABLE_ALIAS) — Primitive → Semantic Chain
|
||||
|
||||
Semantic tokens reference primitives via `VARIABLE_ALIAS`. This is the core pattern that makes light/dark theming work.
|
||||
|
||||
**Architecture:**
|
||||
```
|
||||
Color Primitives collection (1 mode: Value)
|
||||
blue/500 = #3B82F6 ← raw value
|
||||
|
||||
Color collection (2 modes: Light, Dark)
|
||||
color/bg/accent/default:
|
||||
Light → VARIABLE_ALIAS → Primitives/blue/500
|
||||
Dark → VARIABLE_ALIAS → Primitives/blue/300
|
||||
```
|
||||
|
||||
### Complete Semantic Alias Creation Script (SDS-style)
|
||||
|
||||
```javascript
|
||||
function hexToRgb(hex) {
|
||||
const c = hex.replace('#', '');
|
||||
return { r: parseInt(c.slice(0,2),16)/255, g: parseInt(c.slice(2,4),16)/255, b: parseInt(c.slice(4,6),16)/255 };
|
||||
}
|
||||
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
|
||||
const primColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/primitives');
|
||||
const colorColl = collections.find(c => c.getSharedPluginData('dsb', 'key') === 'collection/color');
|
||||
if (!primColl || !colorColl) throw new Error("Collections not found — run primitive/color collection creation first");
|
||||
|
||||
const primValueMode = primColl.modes[0].modeId;
|
||||
const lightModeId = colorColl.modes.find(m => m.name === 'Light').modeId;
|
||||
const darkModeId = colorColl.modes.find(m => m.name === 'Dark').modeId;
|
||||
|
||||
// Load all primitive variables for lookup
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const primsByKey = {};
|
||||
for (const v of allVars) {
|
||||
if (v.variableCollectionId === primColl.id) {
|
||||
primsByKey[v.getSharedPluginData('dsb', 'key')] = v;
|
||||
}
|
||||
}
|
||||
|
||||
function getPrim(name) {
|
||||
const v = primsByKey[`primitive/${name}`];
|
||||
if (!v) throw new Error(`Primitive not found: primitive/${name}`);
|
||||
return v;
|
||||
}
|
||||
|
||||
// Define semantic → [lightPrimitiveName, darkPrimitiveName]
|
||||
// Following the SDS pattern: Background/{Intent}/{Emphasis}
|
||||
const semanticColors = [
|
||||
// Background
|
||||
{ name: 'color/bg/default/default', lightPrim: 'white/1000', darkPrim: 'gray/900',
|
||||
cssVar: '--color-bg-default-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
{ name: 'color/bg/default/secondary', lightPrim: 'gray/100', darkPrim: 'gray/800',
|
||||
cssVar: '--color-bg-default-secondary', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
{ name: 'color/bg/brand/default', lightPrim: 'blue/600', darkPrim: 'blue/300',
|
||||
cssVar: '--color-bg-brand-default', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
// Text
|
||||
{ name: 'color/text/default/default', lightPrim: 'gray/900', darkPrim: 'white/1000',
|
||||
cssVar: '--color-text-default-default', scopes: ['TEXT_FILL'] },
|
||||
{ name: 'color/text/default/secondary', lightPrim: 'gray/500', darkPrim: 'gray/400',
|
||||
cssVar: '--color-text-default-secondary', scopes: ['TEXT_FILL'] },
|
||||
{ name: 'color/text/brand/default', lightPrim: 'blue/700', darkPrim: 'blue/200',
|
||||
cssVar: '--color-text-brand-default', scopes: ['TEXT_FILL'] },
|
||||
// Border
|
||||
{ name: 'color/border/default/default', lightPrim: 'gray/300', darkPrim: 'gray/600',
|
||||
cssVar: '--color-border-default-default', scopes: ['STROKE_COLOR'] },
|
||||
{ name: 'color/border/brand/default', lightPrim: 'blue/500', darkPrim: 'blue/400',
|
||||
cssVar: '--color-border-brand-default', scopes: ['STROKE_COLOR'] },
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, lightPrim, darkPrim, cssVar, scopes } of semanticColors) {
|
||||
const v = figma.variables.createVariable(name, colorColl, 'COLOR');
|
||||
// Alias to primitive in Light mode
|
||||
v.setValueForMode(lightModeId, figma.variables.createVariableAlias(getPrim(lightPrim)));
|
||||
// Alias to primitive in Dark mode
|
||||
v.setValueForMode(darkModeId, figma.variables.createVariableAlias(getPrim(darkPrim)));
|
||||
// Set scopes (semantic layer — these ARE shown in pickers)
|
||||
v.scopes = scopes;
|
||||
// Code syntax
|
||||
v.setVariableCodeSyntax('WEB', `var(${cssVar})`);
|
||||
v.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
v.setSharedPluginData('dsb', 'key', name);
|
||||
created.push({ name, id: v.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
**Key API points:**
|
||||
- `figma.variables.createVariableAlias(variable)` — takes a Variable object, returns `{type:'VARIABLE_ALIAS', id: variable.id}`
|
||||
- The aliased variable MUST have the same `resolvedType` as the semantic variable
|
||||
- Never duplicate raw values in the semantic layer — always alias
|
||||
|
||||
---
|
||||
|
||||
## 5. Variable Scopes — Complete Reference Table
|
||||
|
||||
| Semantic Role | Recommended Scopes | Variable Type |
|
||||
|---|---|---|
|
||||
| Primitive colors (raw) | `[]` — empty, hidden from all pickers | COLOR |
|
||||
| Semi-transparent overlay primitives | `["EFFECT_COLOR"]` | COLOR |
|
||||
| Background fills (frame, shape) | `["FRAME_FILL", "SHAPE_FILL"]` | COLOR |
|
||||
| Text color | `["TEXT_FILL"]` | COLOR |
|
||||
| Icon / shape fill | `["SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
|
||||
| Border / stroke color | `["STROKE_COLOR"]` | COLOR |
|
||||
| Background + border combined | `["FRAME_FILL", "SHAPE_FILL", "STROKE_COLOR"]` | COLOR |
|
||||
| Shadow color | `["EFFECT_COLOR"]` | COLOR |
|
||||
| Spacing / gap between items | `["GAP"]` | FLOAT |
|
||||
| Padding (if separate from gap) | `["GAP"]` | FLOAT |
|
||||
| Corner radius | `["CORNER_RADIUS"]` | FLOAT |
|
||||
| Width / height dimensions | `["WIDTH_HEIGHT"]` | FLOAT |
|
||||
| Font size | `["FONT_SIZE"]` | FLOAT |
|
||||
| Line height | `["LINE_HEIGHT"]` | FLOAT |
|
||||
| Letter spacing | `["LETTER_SPACING"]` | FLOAT |
|
||||
| Font weight (numeric) | `["FONT_WEIGHT"]` | FLOAT |
|
||||
| Stroke width | `["STROKE_FLOAT"]` | FLOAT |
|
||||
| Effect blur radius | `["EFFECT_FLOAT"]` | FLOAT |
|
||||
| Opacity | `["OPACITY"]` | FLOAT |
|
||||
| Font family | `["FONT_FAMILY"]` | STRING |
|
||||
| Font style (e.g. "Semi Bold") | `["FONT_STYLE"]` | STRING |
|
||||
| Boolean flags | *(scopes not supported)* | BOOLEAN |
|
||||
|
||||
**Never use `ALL_SCOPES`** on any variable. It pollutes every picker with irrelevant tokens. The Simple Design System (SDS), the gold standard, uses targeted scopes on every variable.
|
||||
|
||||
**`ALL_FILLS` note:** `ALL_FILLS` is exclusive among fill scopes — it covers `FRAME_FILL`, `SHAPE_FILL`, and `TEXT_FILL` together. If set, you cannot also add individual fill scopes. Prefer specifying individual scopes for precision.
|
||||
|
||||
### Batch Scope-Setting (After Variables are Created)
|
||||
|
||||
If you created variables without scopes and need to set them in batch:
|
||||
|
||||
```javascript
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
|
||||
// Scope mapping: partial name match → scopes
|
||||
const scopeRules = [
|
||||
{ match: 'color/bg/', scopes: ['FRAME_FILL', 'SHAPE_FILL'] },
|
||||
{ match: 'color/text/', scopes: ['TEXT_FILL'] },
|
||||
{ match: 'color/icon/', scopes: ['SHAPE_FILL', 'STROKE_COLOR'] },
|
||||
{ match: 'color/border/', scopes: ['STROKE_COLOR'] },
|
||||
{ match: 'spacing/', scopes: ['GAP'] },
|
||||
{ match: 'radius/', scopes: ['CORNER_RADIUS'] },
|
||||
{ match: 'blue/', scopes: [] }, // primitives — hide
|
||||
{ match: 'gray/', scopes: [] },
|
||||
{ match: 'white/', scopes: [] },
|
||||
{ match: 'black/', scopes: [] },
|
||||
];
|
||||
|
||||
const updated = [];
|
||||
for (const v of allVars) {
|
||||
if (v.remote) continue; // skip library variables
|
||||
for (const rule of scopeRules) {
|
||||
if (v.name.startsWith(rule.match)) {
|
||||
v.scopes = rule.scopes;
|
||||
updated.push({ name: v.name, scopes: rule.scopes });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { updated, count: updated.length };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Code Syntax — WEB/ANDROID/iOS
|
||||
|
||||
Every variable must have code syntax set. This is what powers the developer handoff experience:
|
||||
|
||||
**What code syntax does:** When a developer inspects any element in Figma Dev Mode that has a variable-bound property (fill, padding, radius, etc.), the code snippet shown uses the variable's code syntax name — not the Figma variable name. For example, a button's background fill bound to `color/bg/primary` will show `background: var(--color-bg-primary)` in the CSS snippet, not `color/bg/primary`. Without code syntax set, Dev Mode shows raw hex values or nothing useful.
|
||||
|
||||
You can set up to **3 syntaxes per variable** — one per platform (Web, iOS, Android). Set all three if the codebase targets multiple platforms; set only WEB if it's a web-only project.
|
||||
|
||||
```javascript
|
||||
// WEB: MUST include the var() wrapper — this is the full CSS function syntax
|
||||
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-primary)');
|
||||
// ^^^^ ^
|
||||
// var() wrapper is REQUIRED
|
||||
|
||||
// ANDROID: Kotlin property name — camelCase, no wrapper
|
||||
variable.setVariableCodeSyntax('ANDROID', 'colorBgPrimary');
|
||||
|
||||
// iOS: Swift property — dot-notation, no wrapper
|
||||
variable.setVariableCodeSyntax('iOS', 'Color.bgPrimary');
|
||||
```
|
||||
|
||||
> **CRITICAL — WEB code syntax MUST use the `var()` wrapper.** Setting just `--color-bg-primary` (without `var()`) will cause Dev Mode to show raw hex values instead of the CSS variable reference. Always use the full `var(--name)` form. ANDROID and iOS do NOT use a wrapper.
|
||||
|
||||
**Platform derivation rules from the CSS variable name:**
|
||||
|
||||
| Platform | Pattern | Example |
|
||||
|---|---|---|
|
||||
| WEB | **`var(--{css-var-name})`** — `var()` wrapper required | `var(--sds-color-bg-primary)` |
|
||||
| ANDROID | camelCase, no wrapper, strip `--` prefix | `sdsColorBgPrimary` |
|
||||
| iOS | PascalCase after `.`, no wrapper, strip `--` prefix | `Color.SdsColorBgPrimary` or `Color.bgPrimary` |
|
||||
|
||||
**Always use the actual CSS variable name from the codebase** — do not derive it from the Figma variable name. If the code uses `--sds-color-background-brand-default`, that exact string is the WEB code syntax (minus the `var()` wrapper that you add).
|
||||
|
||||
### Batch Code Syntax Setting
|
||||
|
||||
```javascript
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const updated = [];
|
||||
|
||||
for (const v of allVars) {
|
||||
if (v.remote) continue;
|
||||
// If code syntax already set, skip
|
||||
if (v.codeSyntax['WEB']) continue;
|
||||
|
||||
// FALLBACK: derive from Figma name: color/bg/primary → var(--color-bg-primary)
|
||||
// PREFERRED: pass in a cssVarMap built from actual codebase CSS variable names
|
||||
// e.g. cssVarMap = { 'color/bg/primary': '--color-bg-primary', ... }
|
||||
const cssName = cssVarMap?.[v.name]
|
||||
?? v.name.replace(/\//g, '-').replace(/\s/g, '-').toLowerCase();
|
||||
v.setVariableCodeSyntax('WEB', `var(--${cssName})`);
|
||||
updated.push({ name: v.name, web: `var(--${cssName})` });
|
||||
}
|
||||
|
||||
return { updated, count: updated.length };
|
||||
```
|
||||
|
||||
Note: derived names are a fallback only. Always prefer overriding with actual CSS variable names from the codebase when they are known.
|
||||
|
||||
---
|
||||
|
||||
## 7. Effect Styles (Shadows) and Text Styles
|
||||
|
||||
Shadows and composite typography cannot be variables — they are Styles.
|
||||
|
||||
### Creating Effect Styles (Shadows)
|
||||
|
||||
Reference from SDS (15 effect styles) and the SDS shadow pattern `Shadow/{Level}`:
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Shadow definitions — CSS equivalent in comments
|
||||
// CSS: 0 1px 2px rgba(0,0,0,0.05)
|
||||
const shadows = [
|
||||
{
|
||||
name: 'Shadow/Subtle',
|
||||
effects: [{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.05 },
|
||||
offset: { x: 0, y: 1 },
|
||||
radius: 2,
|
||||
spread: 0,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
}]
|
||||
},
|
||||
{
|
||||
// CSS: 0 4px 6px -1px rgba(0,0,0,0.10), 0 2px 4px -1px rgba(0,0,0,0.06)
|
||||
name: 'Shadow/Medium',
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.10 },
|
||||
offset: { x: 0, y: 4 },
|
||||
radius: 6,
|
||||
spread: -1,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
},
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.06 },
|
||||
offset: { x: 0, y: 2 },
|
||||
radius: 4,
|
||||
spread: -1,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
// CSS: 0 10px 15px -3px rgba(0,0,0,0.10), 0 4px 6px -2px rgba(0,0,0,0.05)
|
||||
name: 'Shadow/Strong',
|
||||
effects: [
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.10 },
|
||||
offset: { x: 0, y: 10 },
|
||||
radius: 15,
|
||||
spread: -3,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
},
|
||||
{
|
||||
type: 'DROP_SHADOW',
|
||||
color: { r: 0, g: 0, b: 0, a: 0.05 },
|
||||
offset: { x: 0, y: 4 },
|
||||
radius: 6,
|
||||
spread: -2,
|
||||
visible: true,
|
||||
blendMode: 'NORMAL'
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// M3-style dual shadow (umbra + penumbra pattern):
|
||||
const m3Shadows = [
|
||||
{
|
||||
name: 'Elevation/1',
|
||||
effects: [
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:1}, radius:3, spread:1, visible:true, blendMode:'NORMAL' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Elevation/2',
|
||||
effects: [
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:2, spread:0, visible:true, blendMode:'NORMAL' },
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:2}, radius:6, spread:2, visible:true, blendMode:'NORMAL' }
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'Elevation/3',
|
||||
effects: [
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.30}, offset:{x:0,y:1}, radius:3, spread:0, visible:true, blendMode:'NORMAL' },
|
||||
{ type: 'DROP_SHADOW', color: {r:0,g:0,b:0,a:0.15}, offset:{x:0,y:4}, radius:8, spread:3, visible:true, blendMode:'NORMAL' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
const created = [];
|
||||
for (const { name, effects } of shadows) {
|
||||
const style = figma.createEffectStyle();
|
||||
style.name = name;
|
||||
style.effects = effects;
|
||||
style.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
style.setSharedPluginData('dsb', 'key', `effect-style/${name}`);
|
||||
created.push({ name, id: style.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
### Creating Text Styles
|
||||
|
||||
Fonts must be loaded before creating text styles.
|
||||
|
||||
```javascript
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Define text styles — based on SDS typography hierarchy
|
||||
const textStyles = [
|
||||
// Display / Hero
|
||||
{ name: 'Display/Hero', family: 'Inter', style: 'Bold', size: 72, lineHeight: 80, letterSpacing: -1.5 },
|
||||
// Headings
|
||||
{ name: 'Heading/H1', family: 'Inter', style: 'Bold', size: 48, lineHeight: 56, letterSpacing: -1.0 },
|
||||
{ name: 'Heading/H2', family: 'Inter', style: 'Bold', size: 40, lineHeight: 48, letterSpacing: -0.5 },
|
||||
{ name: 'Heading/H3', family: 'Inter', style: 'Semi Bold', size: 32, lineHeight: 40, letterSpacing: 0 },
|
||||
{ name: 'Heading/H4', family: 'Inter', style: 'Semi Bold', size: 24, lineHeight: 32, letterSpacing: 0 },
|
||||
// Body
|
||||
{ name: 'Body/Large', family: 'Inter', style: 'Regular', size: 18, lineHeight: 28, letterSpacing: 0 },
|
||||
{ name: 'Body/Medium', family: 'Inter', style: 'Regular', size: 16, lineHeight: 24, letterSpacing: 0 },
|
||||
{ name: 'Body/Small', family: 'Inter', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
|
||||
// Label
|
||||
{ name: 'Label/Large', family: 'Inter', style: 'Medium', size: 14, lineHeight: 20, letterSpacing: 0.1 },
|
||||
{ name: 'Label/Medium', family: 'Inter', style: 'Medium', size: 12, lineHeight: 16, letterSpacing: 0.5 },
|
||||
{ name: 'Label/Small', family: 'Inter', style: 'Medium', size: 11, lineHeight: 16, letterSpacing: 0.5 },
|
||||
// Code
|
||||
{ name: 'Code/Base', family: 'Roboto Mono', style: 'Regular', size: 14, lineHeight: 20, letterSpacing: 0 },
|
||||
];
|
||||
|
||||
// Load all required fonts first
|
||||
const fontSet = new Set(textStyles.map(s => JSON.stringify({ family: s.family, style: s.style })));
|
||||
await Promise.all([...fontSet].map(f => figma.loadFontAsync(JSON.parse(f))));
|
||||
|
||||
const created = [];
|
||||
for (const { name, family, style, size, lineHeight, letterSpacing } of textStyles) {
|
||||
const ts = figma.createTextStyle();
|
||||
ts.name = name;
|
||||
ts.fontName = { family, style };
|
||||
ts.fontSize = size;
|
||||
ts.lineHeight = { value: lineHeight, unit: 'PIXELS' };
|
||||
ts.letterSpacing = { value: letterSpacing, unit: 'PIXELS' };
|
||||
ts.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
ts.setSharedPluginData('dsb', 'key', `text-style/${name}`);
|
||||
created.push({ name, id: ts.id });
|
||||
}
|
||||
|
||||
return { created, count: created.length };
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Idempotency — Check-Before-Create Pattern
|
||||
|
||||
Every creation script should check whether the entity already exists before creating it. This prevents duplicates when a script is re-run after partial failure.
|
||||
|
||||
### Check-Before-Create for Collections
|
||||
|
||||
```javascript
|
||||
const DSB_KEY = 'collection/primitives';
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Check if already exists
|
||||
const existing = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
let primColl = existing.find(c => c.getSharedPluginData('dsb', 'key') === DSB_KEY);
|
||||
|
||||
if (primColl) {
|
||||
return { status: 'already_exists', collectionId: primColl.id, name: primColl.name };
|
||||
}
|
||||
|
||||
// Create only if not found
|
||||
primColl = figma.variables.createVariableCollection("Primitives");
|
||||
primColl.renameMode(primColl.modes[0].modeId, "Value");
|
||||
primColl.setSharedPluginData('dsb', 'run_id', RUN_ID);
|
||||
primColl.setSharedPluginData('dsb', 'key', DSB_KEY);
|
||||
|
||||
return { status: 'created', collectionId: primColl.id };
|
||||
```
|
||||
|
||||
### Check-Before-Create for Variables
|
||||
|
||||
```javascript
|
||||
const VARIABLE_KEY = 'primitive/blue/500';
|
||||
const RUN_ID = "ds-build-2024-001";
|
||||
|
||||
// Check if already exists by sharedPluginData key
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const existing = allVars.find(v => v.getSharedPluginData('dsb', 'key') === VARIABLE_KEY);
|
||||
|
||||
if (existing) {
|
||||
return { status: 'already_exists', id: existing.id, name: existing.name };
|
||||
}
|
||||
|
||||
// ... create the variable ...
|
||||
return { status: 'created' };
|
||||
```
|
||||
|
||||
### sharedPluginData Tagging Strategy
|
||||
|
||||
Tag every created node immediately after creation. The `key` is the stable logical identifier used for idempotency checks. The `run_id` identifies which build run created it (useful for cleanup).
|
||||
|
||||
```javascript
|
||||
node.setSharedPluginData('dsb', 'run_id', RUN_ID); // build run ID
|
||||
node.setSharedPluginData('dsb', 'phase', 'phase1'); // which phase
|
||||
node.setSharedPluginData('dsb', 'key', 'color/bg/primary'); // stable logical key
|
||||
```
|
||||
|
||||
**Cleanup by run ID (safe — targets only tagged nodes, never user-owned nodes):**
|
||||
|
||||
```javascript
|
||||
const TARGET_RUN_ID = "ds-build-2024-001"; // run to remove
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const removed = [];
|
||||
for (const v of allVars) {
|
||||
if (v.getSharedPluginData('dsb', 'run_id') === TARGET_RUN_ID) {
|
||||
removed.push(v.name);
|
||||
v.remove();
|
||||
}
|
||||
}
|
||||
return { removed, count: removed.length };
|
||||
```
|
||||
|
||||
**Never clean up by name prefix** (e.g., deleting everything starting with `color/`). This will destroy user-created variables that happen to share the prefix.
|
||||
|
||||
---
|
||||
|
||||
## 9. Validation — Verify Counts, Aliases, and Scopes
|
||||
|
||||
Run these scripts after Phase 1 to verify everything was created correctly before proceeding to Phase 2.
|
||||
|
||||
### Verify Collection and Variable Counts
|
||||
|
||||
```javascript
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
|
||||
const summary = collections.map(c => {
|
||||
const vars = allVars.filter(v => v.variableCollectionId === c.id);
|
||||
return {
|
||||
name: c.name,
|
||||
id: c.id,
|
||||
modes: c.modes.map(m => m.name),
|
||||
variableCount: vars.length,
|
||||
missingScopes: vars.filter(v => v.scopes.length === 0 && v.resolvedType !== 'BOOLEAN').length,
|
||||
missingCodeSyntax: vars.filter(v => !v.codeSyntax['WEB'] && !v.remote).length,
|
||||
sampleVariables: vars.slice(0, 3).map(v => v.name)
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
collectionCount: collections.length,
|
||||
totalVariables: allVars.length,
|
||||
collections: summary
|
||||
};
|
||||
```
|
||||
|
||||
Interpret: `missingScopes > 0` (for non-primitives and non-BOOLEANs) → scope-setting failed, re-run scope script. `missingCodeSyntax > 0` → code syntax not set, run batch code syntax script.
|
||||
|
||||
Note: primitives correctly have `scopes = []` (empty, hidden). `missingScopes` above counts non-BOOLEAN variables with empty scopes — review the list to confirm they are all primitives.
|
||||
|
||||
### Verify Aliases Resolve
|
||||
|
||||
```javascript
|
||||
const allVars = await figma.variables.getLocalVariablesAsync();
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
|
||||
const brokenAliases = [];
|
||||
const aliasedVars = [];
|
||||
|
||||
for (const v of allVars) {
|
||||
if (v.remote) continue;
|
||||
const coll = collections.find(c => c.id === v.variableCollectionId);
|
||||
if (!coll) continue;
|
||||
|
||||
for (const [modeId, val] of Object.entries(v.valuesByMode)) {
|
||||
if (val && typeof val === 'object' && val.type === 'VARIABLE_ALIAS') {
|
||||
aliasedVars.push({ name: v.name, aliasTargetId: val.id });
|
||||
// Verify the target exists
|
||||
const target = allVars.find(t => t.id === val.id);
|
||||
if (!target) {
|
||||
brokenAliases.push({ variable: v.name, modeId, missingTargetId: val.id });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalAliased: aliasedVars.length,
|
||||
brokenAliases,
|
||||
brokenCount: brokenAliases.length,
|
||||
status: brokenAliases.length === 0 ? 'all_aliases_resolve' : 'BROKEN_ALIASES_FOUND'
|
||||
};
|
||||
```
|
||||
|
||||
Interpret: `brokenCount > 0` means a semantic variable references a primitive that was deleted or not yet created. Create the missing primitives, then re-run alias creation for the affected semantic variables.
|
||||
|
||||
### Verify Style Counts
|
||||
|
||||
```javascript
|
||||
const [textStyles, effectStyles] = await Promise.all([
|
||||
figma.getLocalTextStylesAsync(),
|
||||
figma.getLocalEffectStylesAsync()
|
||||
]);
|
||||
|
||||
return {
|
||||
textStyles: textStyles.map(s => ({ name: s.name, fontSize: s.fontSize, fontFamily: s.fontName.family })),
|
||||
effectStyles: effectStyles.map(s => ({ name: s.name, effectCount: s.effects.length })),
|
||||
counts: { text: textStyles.length, effect: effectStyles.length }
|
||||
};
|
||||
```
|
||||
|
||||
### Phase 1 Exit Criteria Checklist
|
||||
|
||||
Before proceeding to Phase 2, verify all of the following:
|
||||
|
||||
- Every planned collection exists with the correct number of modes
|
||||
- Primitive variables: `scopes = []`, code syntax set
|
||||
- Semantic variables: targeted scopes set, code syntax set, aliases pointing to primitives (not raw values)
|
||||
- All broken alias count = 0
|
||||
- All planned text styles exist with correct font family/size/weight
|
||||
- All planned effect styles exist with correct shadow values
|
||||
- No variable has `ALL_SCOPES` unless explicitly approved by the user
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* bindVariablesToComponent
|
||||
*
|
||||
* Binds design token variables to the visual properties of a component node.
|
||||
* Supports fills, strokes, all padding directions, item spacing, and corner radius.
|
||||
* Only binds properties for which a variable ID is provided in `bindings`.
|
||||
*
|
||||
* This function should be called on each variant individually within a component
|
||||
* set, OR on the component set itself for properties shared by all variants.
|
||||
*
|
||||
* @param {ComponentNode | FrameNode | RectangleNode} component
|
||||
* The Figma node to mutate. Usually a ComponentNode or one of its children.
|
||||
* @param {{
|
||||
* fills?: string,
|
||||
* strokes?: string,
|
||||
* paddingTop?: string,
|
||||
* paddingBottom?: string,
|
||||
* paddingLeft?: string,
|
||||
* paddingRight?: string,
|
||||
* itemSpacing?: string,
|
||||
* cornerRadius?: string
|
||||
* }} bindings
|
||||
* Each key is a visual property name; each value is a Figma Variable ID
|
||||
* (e.g. "VariableID:123:456"). Omit a key to skip binding that property.
|
||||
* @returns {Promise<{ mutatedNodeIds: string[] }>}
|
||||
* List of node IDs that were mutated (for audit/validation purposes).
|
||||
*/
|
||||
async function bindVariablesToComponent(component, bindings) {
|
||||
const mutatedNodeIds = []
|
||||
|
||||
if (!component) {
|
||||
return { mutatedNodeIds }
|
||||
}
|
||||
|
||||
// --- Fills ---
|
||||
if (bindings.fills) {
|
||||
const fillVar = await figma.variables.getVariableByIdAsync(bindings.fills)
|
||||
if (fillVar) {
|
||||
const existingFills = component.fills
|
||||
if (Array.isArray(existingFills) && existingFills.length > 0) {
|
||||
// Bind the color of the first fill to the variable
|
||||
const boundFill = figma.variables.setBoundVariableForPaint(
|
||||
existingFills[0],
|
||||
'color',
|
||||
fillVar,
|
||||
)
|
||||
component.fills = [boundFill, ...existingFills.slice(1)]
|
||||
} else {
|
||||
// No existing fill — create a solid fill bound to the variable
|
||||
const boundFill = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
|
||||
'color',
|
||||
fillVar,
|
||||
)
|
||||
component.fills = [boundFill]
|
||||
}
|
||||
mutatedNodeIds.push(component.id)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Strokes ---
|
||||
if (bindings.strokes) {
|
||||
const strokeVar = await figma.variables.getVariableByIdAsync(bindings.strokes)
|
||||
if (strokeVar) {
|
||||
const existingStrokes = component.strokes
|
||||
if (Array.isArray(existingStrokes) && existingStrokes.length > 0) {
|
||||
const boundStroke = figma.variables.setBoundVariableForPaint(
|
||||
existingStrokes[0],
|
||||
'color',
|
||||
strokeVar,
|
||||
)
|
||||
component.strokes = [boundStroke, ...existingStrokes.slice(1)]
|
||||
} else {
|
||||
const boundStroke = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0.5, g: 0.5, b: 0.5 } },
|
||||
'color',
|
||||
strokeVar,
|
||||
)
|
||||
component.strokes = [boundStroke]
|
||||
}
|
||||
if (!mutatedNodeIds.includes(component.id)) {
|
||||
mutatedNodeIds.push(component.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Spacing properties (FLOAT variables bound via setBoundVariable) ---
|
||||
const floatBindings = [
|
||||
['paddingTop', 'paddingTop'],
|
||||
['paddingBottom', 'paddingBottom'],
|
||||
['paddingLeft', 'paddingLeft'],
|
||||
['paddingRight', 'paddingRight'],
|
||||
['itemSpacing', 'itemSpacing'],
|
||||
['cornerRadius', 'cornerRadius'],
|
||||
]
|
||||
|
||||
for (const [bindingKey, figmaProp] of floatBindings) {
|
||||
if (bindings[bindingKey]) {
|
||||
const variable = await figma.variables.getVariableByIdAsync(bindings[bindingKey])
|
||||
if (variable) {
|
||||
component.setBoundVariable(figmaProp, variable)
|
||||
if (!mutatedNodeIds.includes(component.id)) {
|
||||
mutatedNodeIds.push(component.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mutatedNodeIds }
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* cleanupOrphans
|
||||
*
|
||||
* Finds and removes all Figma nodes (pages, frames, components, variables,
|
||||
* and variable collections) that were tagged with the given `dsb_run_id`
|
||||
* by a previous build run. This is safe cleanup: it uses plugin data tags,
|
||||
* never name-prefix matching, so it cannot accidentally delete user-owned nodes.
|
||||
*
|
||||
* Use this when a build run fails mid-way and you need to reset to a clean
|
||||
* slate before retrying. The function traverses the entire document looking
|
||||
* for `dsb_run_id` plugin data matching `runId`.
|
||||
*
|
||||
* Variables and variable collections are handled separately (they are not
|
||||
* scene nodes and cannot be discovered via node traversal).
|
||||
*
|
||||
* @param {string} runId - The dsb_run_id value to match (e.g. "ds-build-2024-001").
|
||||
* @returns {Promise<{
|
||||
* removedCount: number,
|
||||
* removedIds: string[]
|
||||
* }>}
|
||||
*/
|
||||
async function cleanupOrphans(runId) {
|
||||
if (!runId) {
|
||||
throw new Error('cleanupOrphans: runId is required.')
|
||||
}
|
||||
|
||||
const removedIds = []
|
||||
const originalPage = figma.currentPage
|
||||
|
||||
// --- Remove tagged scene nodes (pages, frames, components, etc.) ---
|
||||
// Collect pages to remove (can't remove during iteration)
|
||||
const pagesToRemove = []
|
||||
|
||||
for (const page of figma.root.children) {
|
||||
if (page.getPluginData('dsb_run_id') === runId) {
|
||||
pagesToRemove.push(page)
|
||||
continue
|
||||
}
|
||||
|
||||
// Traverse all nodes on this page
|
||||
await figma.setCurrentPageAsync(page)
|
||||
|
||||
const nodesToRemove = []
|
||||
page.findAll((node) => {
|
||||
if (node.getPluginData('dsb_run_id') === runId) {
|
||||
nodesToRemove.push(node)
|
||||
return false // Don't descend — removing the parent removes its children
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
// Remove deepest nodes first (children before parents) to avoid
|
||||
// "parent no longer exists" errors
|
||||
const sorted = nodesToRemove.sort((a, b) => {
|
||||
// Sort by depth descending: deeper nodes first
|
||||
return getDepth(b) - getDepth(a)
|
||||
})
|
||||
|
||||
for (const node of sorted) {
|
||||
if (node && node.parent) {
|
||||
removedIds.push(node.id)
|
||||
node.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove tagged pages last
|
||||
for (const page of pagesToRemove) {
|
||||
// Cannot remove the last page in the document
|
||||
if (figma.root.children.length <= 1) {
|
||||
break
|
||||
}
|
||||
removedIds.push(page.id)
|
||||
page.remove()
|
||||
}
|
||||
|
||||
// --- Remove tagged variables ---
|
||||
const allVariables = await figma.variables.getLocalVariablesAsync()
|
||||
for (const variable of allVariables) {
|
||||
if (variable.getPluginData('dsb_run_id') === runId) {
|
||||
removedIds.push(variable.id)
|
||||
variable.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// --- Remove tagged variable collections ---
|
||||
// Must be done after variables are removed
|
||||
const allCollections = await figma.variables.getLocalVariableCollectionsAsync()
|
||||
for (const collection of allCollections) {
|
||||
if (collection.getPluginData('dsb_run_id') === runId) {
|
||||
removedIds.push(collection.id)
|
||||
collection.remove()
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original page (if it still exists)
|
||||
try {
|
||||
await figma.setCurrentPageAsync(originalPage)
|
||||
} catch (_) {
|
||||
// Original page was removed — switch to first available page
|
||||
if (figma.root.children.length > 0) {
|
||||
await figma.setCurrentPageAsync(figma.root.children[0])
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
removedCount: removedIds.length,
|
||||
removedIds,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the depth of a node in the document tree.
|
||||
* Root children (pages) have depth 1; their children have depth 2; etc.
|
||||
*
|
||||
* @param {BaseNode} node
|
||||
* @returns {number}
|
||||
*/
|
||||
function getDepth(node) {
|
||||
let depth = 0
|
||||
let current = node
|
||||
while (current.parent) {
|
||||
depth++
|
||||
current = current.parent
|
||||
}
|
||||
return depth
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* createComponentWithVariants
|
||||
*
|
||||
* Creates a component set by generating all combinations of `variantAxes`,
|
||||
* building one Figma component per combination, then calling
|
||||
* `figma.combineAsVariants` to produce the component set. After combining,
|
||||
* the variants are repositioned into a grid so they don't all stack at (0, 0).
|
||||
*
|
||||
* @param {{
|
||||
* name: string,
|
||||
* variantAxes: Record<string, string[]>,
|
||||
* baseProps: {
|
||||
* width: number,
|
||||
* height: number,
|
||||
* fills?: Paint[],
|
||||
* padding?: {top?: number, bottom?: number, left?: number, right?: number},
|
||||
* radius?: number,
|
||||
* layoutMode?: 'HORIZONTAL' | 'VERTICAL' | 'NONE',
|
||||
* itemSpacing?: number
|
||||
* },
|
||||
* page: PageNode
|
||||
* }} config
|
||||
* - `name`: Component set name (e.g. "Button").
|
||||
* - `variantAxes`: Each key is a variant property name; each value is an array of
|
||||
* allowed values. All combinations are generated (Cartesian product).
|
||||
* Example: { Size: ['Small', 'Medium', 'Large'], Style: ['Primary', 'Ghost'] }
|
||||
* produces 6 variants.
|
||||
* - `baseProps`: Visual properties applied to every variant.
|
||||
* - `page`: The PageNode to create components on (must be set as current page by caller).
|
||||
* @param {string} [runId] - Optional dsb_run_id to tag every node.
|
||||
* @returns {Promise<{
|
||||
* componentSet: ComponentSetNode,
|
||||
* variants: ComponentNode[]
|
||||
* }>}
|
||||
*/
|
||||
async function createComponentWithVariants(config, runId) {
|
||||
const { name, variantAxes, baseProps, page } = config
|
||||
|
||||
// Ensure we are on the correct page
|
||||
await figma.setCurrentPageAsync(page)
|
||||
|
||||
// Compute Cartesian product of variant axes
|
||||
const axisNames = Object.keys(variantAxes)
|
||||
const axisValues = axisNames.map((k) => variantAxes[k])
|
||||
const combinations = cartesianProduct(axisValues)
|
||||
|
||||
// Build one component per combination
|
||||
const components = []
|
||||
for (const combo of combinations) {
|
||||
const comp = figma.createComponent()
|
||||
|
||||
// Name: "Property=Value, Property=Value, ..."
|
||||
comp.name = axisNames.map((ax, i) => `${ax}=${combo[i]}`).join(', ')
|
||||
|
||||
// Base geometry
|
||||
comp.resize(baseProps.width, baseProps.height)
|
||||
|
||||
// Fills
|
||||
if (baseProps.fills !== undefined) {
|
||||
comp.fills = baseProps.fills
|
||||
} else {
|
||||
comp.fills = [{ type: 'SOLID', color: { r: 0.9, g: 0.9, b: 0.9 } }]
|
||||
}
|
||||
|
||||
// Corner radius
|
||||
if (baseProps.radius !== undefined) {
|
||||
comp.cornerRadius = baseProps.radius
|
||||
}
|
||||
|
||||
// Auto-layout
|
||||
if (baseProps.layoutMode && baseProps.layoutMode !== 'NONE') {
|
||||
comp.layoutMode = baseProps.layoutMode
|
||||
comp.primaryAxisAlignItems = 'CENTER'
|
||||
comp.counterAxisAlignItems = 'CENTER'
|
||||
if (baseProps.itemSpacing !== undefined) {
|
||||
comp.itemSpacing = baseProps.itemSpacing
|
||||
}
|
||||
}
|
||||
|
||||
// Padding
|
||||
if (baseProps.padding) {
|
||||
comp.paddingTop = baseProps.padding.top ?? 0
|
||||
comp.paddingBottom = baseProps.padding.bottom ?? 0
|
||||
comp.paddingLeft = baseProps.padding.left ?? 0
|
||||
comp.paddingRight = baseProps.padding.right ?? 0
|
||||
}
|
||||
|
||||
// Plugin data
|
||||
const variantKey = axisNames.map((ax, i) => `${ax}:${combo[i]}`).join('|')
|
||||
comp.setPluginData('dsb_key', `component/${name}/${variantKey}`)
|
||||
if (runId) {
|
||||
comp.setPluginData('dsb_run_id', runId)
|
||||
}
|
||||
|
||||
page.appendChild(comp)
|
||||
components.push(comp)
|
||||
}
|
||||
|
||||
// Combine into a component set
|
||||
const componentSet = figma.combineAsVariants(components, page)
|
||||
componentSet.name = name
|
||||
componentSet.setPluginData('dsb_key', `componentSet/${name}`)
|
||||
if (runId) {
|
||||
componentSet.setPluginData('dsb_run_id', runId)
|
||||
}
|
||||
|
||||
// Grid layout — variants stack at (0, 0) after combineAsVariants; reposition them.
|
||||
const GRID_GAP = 16
|
||||
const cols = Math.max(1, axisValues[axisValues.length - 1]?.length ?? 1)
|
||||
const variantWidth = baseProps.width
|
||||
const variantHeight = baseProps.height
|
||||
|
||||
componentSet.children.forEach((variant, idx) => {
|
||||
const col = idx % cols
|
||||
const row = Math.floor(idx / cols)
|
||||
variant.x = col * (variantWidth + GRID_GAP)
|
||||
variant.y = row * (variantHeight + GRID_GAP)
|
||||
})
|
||||
|
||||
// Resize component set to wrap its children with padding
|
||||
const totalCols = Math.min(cols, combinations.length)
|
||||
const totalRows = Math.ceil(combinations.length / cols)
|
||||
const PADDING = 40
|
||||
componentSet.resize(
|
||||
totalCols * variantWidth + (totalCols - 1) * GRID_GAP + PADDING * 2,
|
||||
totalRows * variantHeight + (totalRows - 1) * GRID_GAP + PADDING * 2,
|
||||
)
|
||||
|
||||
// Position component set at a safe canvas location
|
||||
componentSet.x = 480
|
||||
componentSet.y = 80
|
||||
|
||||
return { componentSet, variants: componentSet.children }
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the Cartesian product of multiple arrays.
|
||||
* cartesianProduct([[A, B], [1, 2]]) → [[A,1], [A,2], [B,1], [B,2]]
|
||||
*
|
||||
* @param {Array<string[]>} arrays
|
||||
* @returns {string[][]}
|
||||
*/
|
||||
function cartesianProduct(arrays) {
|
||||
return arrays.reduce(
|
||||
(acc, curr) => acc.flatMap((combo) => curr.map((val) => [...combo, val])),
|
||||
[[]],
|
||||
)
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* createDocumentationPage
|
||||
*
|
||||
* Creates a new Figma page with a standardized documentation layout: a page
|
||||
* title, optional description, and an ordered list of sections each built by
|
||||
* a caller-supplied `contentFn`. The content function receives the section
|
||||
* frame and may append any nodes to it.
|
||||
*
|
||||
* This function is used for standalone documentation pages (e.g. a Foundations
|
||||
* page, a Getting Started page, or a component page with documentation).
|
||||
* It does not handle component sets — those live on separate pages created by
|
||||
* createComponentWithVariants.
|
||||
*
|
||||
* @param {string} pageName - The Figma page name (e.g. "Foundations", "Getting Started").
|
||||
* @param {{
|
||||
* title: string,
|
||||
* description?: string,
|
||||
* sections: Array<{
|
||||
* name: string,
|
||||
* contentFn: (sectionFrame: FrameNode) => Promise<void>
|
||||
* }>
|
||||
* }} config
|
||||
* - `title`: Large heading displayed at the top of the page.
|
||||
* - `description`: Optional subtitle displayed below the heading.
|
||||
* - `sections`: Ordered list of sections. Each section gets its own frame
|
||||
* with a heading and is passed to `contentFn` for population.
|
||||
* @param {string} [runId] - Optional dsb_run_id to tag every created node.
|
||||
* @returns {Promise<{
|
||||
* page: PageNode,
|
||||
* titleNode: TextNode,
|
||||
* frameIds: string[]
|
||||
* }>}
|
||||
* `frameIds` is an ordered list of IDs for the root frame and each section frame.
|
||||
*/
|
||||
async function createDocumentationPage(pageName, config, runId) {
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Bold' })
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Regular' })
|
||||
await figma.loadFontAsync({ family: 'Inter', style: 'Medium' })
|
||||
|
||||
// Create and activate the page
|
||||
const page = figma.createPage()
|
||||
page.name = pageName
|
||||
await figma.setCurrentPageAsync(page)
|
||||
|
||||
if (runId) {
|
||||
page.setPluginData('dsb_run_id', runId)
|
||||
page.setPluginData('dsb_key', `page/${pageName}`)
|
||||
}
|
||||
|
||||
const frameIds = []
|
||||
|
||||
// Root scroll container — 1440px wide, auto-height
|
||||
const root = figma.createFrame()
|
||||
root.name = pageName
|
||||
root.layoutMode = 'VERTICAL'
|
||||
root.primaryAxisAlignItems = 'MIN'
|
||||
root.counterAxisAlignItems = 'MIN'
|
||||
root.itemSpacing = 80
|
||||
root.paddingTop = 80
|
||||
root.paddingBottom = 120
|
||||
root.paddingLeft = 80
|
||||
root.paddingRight = 80
|
||||
root.layoutSizingHorizontal = 'FIXED'
|
||||
root.layoutSizingVertical = 'HUG'
|
||||
root.resize(1440, 1)
|
||||
root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]
|
||||
root.x = 0
|
||||
root.y = 0
|
||||
page.appendChild(root)
|
||||
|
||||
if (runId) {
|
||||
root.setPluginData('dsb_run_id', runId)
|
||||
root.setPluginData('dsb_key', `frame/root/${pageName}`)
|
||||
}
|
||||
|
||||
frameIds.push(root.id)
|
||||
|
||||
// Page header: title + optional description
|
||||
const header = figma.createFrame()
|
||||
header.name = 'Header'
|
||||
header.layoutMode = 'VERTICAL'
|
||||
header.itemSpacing = 12
|
||||
header.layoutSizingHorizontal = 'FILL'
|
||||
header.layoutSizingVertical = 'HUG'
|
||||
header.fills = []
|
||||
root.appendChild(header)
|
||||
|
||||
const titleNode = figma.createText()
|
||||
titleNode.fontName = { family: 'Inter', style: 'Bold' }
|
||||
titleNode.characters = config.title
|
||||
titleNode.fontSize = 40
|
||||
titleNode.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]
|
||||
titleNode.layoutSizingHorizontal = 'FILL'
|
||||
header.appendChild(titleNode)
|
||||
|
||||
if (config.description) {
|
||||
const descNode = figma.createText()
|
||||
descNode.fontName = { family: 'Inter', style: 'Regular' }
|
||||
descNode.characters = config.description
|
||||
descNode.fontSize = 16
|
||||
descNode.lineHeight = { value: 24, unit: 'PIXELS' }
|
||||
descNode.fills = [{ type: 'SOLID', color: { r: 0.4, g: 0.4, b: 0.4 } }]
|
||||
descNode.layoutSizingHorizontal = 'FILL'
|
||||
header.appendChild(descNode)
|
||||
}
|
||||
|
||||
// Sections
|
||||
for (const section of config.sections) {
|
||||
const sectionFrame = figma.createFrame()
|
||||
sectionFrame.name = `Section/${section.name}`
|
||||
sectionFrame.layoutMode = 'VERTICAL'
|
||||
sectionFrame.itemSpacing = 20
|
||||
sectionFrame.layoutSizingHorizontal = 'FILL'
|
||||
sectionFrame.layoutSizingVertical = 'HUG'
|
||||
sectionFrame.fills = []
|
||||
root.appendChild(sectionFrame)
|
||||
|
||||
if (runId) {
|
||||
sectionFrame.setPluginData('dsb_run_id', runId)
|
||||
sectionFrame.setPluginData('dsb_key', `frame/section/${pageName}/${section.name}`)
|
||||
}
|
||||
|
||||
// Section heading
|
||||
const sectionHeading = figma.createText()
|
||||
sectionHeading.fontName = { family: 'Inter', style: 'Bold' }
|
||||
sectionHeading.characters = section.name
|
||||
sectionHeading.fontSize = 24
|
||||
sectionHeading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]
|
||||
sectionHeading.layoutSizingHorizontal = 'FILL'
|
||||
sectionFrame.appendChild(sectionHeading)
|
||||
|
||||
// Invoke the caller's content function to populate the section
|
||||
await section.contentFn(sectionFrame)
|
||||
|
||||
frameIds.push(sectionFrame.id)
|
||||
}
|
||||
|
||||
return { page, titleNode, frameIds }
|
||||
}
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* createSemanticTokens
|
||||
*
|
||||
* Creates a batch of Figma variables in the given collection, one per entry in
|
||||
* `tokenMap`. Supports raw values, variable alias references, code syntax, and
|
||||
* scopes. Returns a map of token name → Variable for use in subsequent steps.
|
||||
*
|
||||
* @param {VariableCollection} collection - The target variable collection.
|
||||
* @param {Record<string, string>} modeIds - Map of {modeName: modeId} from createVariableCollection.
|
||||
* @param {Array<{
|
||||
* name: string,
|
||||
* type: 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN',
|
||||
* values: Record<string, string | number | boolean | {type: 'VARIABLE_ALIAS', id: string}>,
|
||||
* scopes?: VariableScope[],
|
||||
* codeSyntax?: {WEB?: string, ANDROID?: string, iOS?: string}
|
||||
* }>} tokenMap - Ordered list of token definitions.
|
||||
* - `name`: Variable name using slash hierarchy (e.g. "color/bg/primary").
|
||||
* - `type`: Figma variable type.
|
||||
* - `values`: Map of {modeName: value}. Values can be raw (hex string for COLOR,
|
||||
* number for FLOAT) or alias objects {type: 'VARIABLE_ALIAS', id: variableId}.
|
||||
* For COLOR, raw values are accepted as hex strings ("#rrggbb" or "#rrggbbaa")
|
||||
* and converted to {r, g, b, a} automatically.
|
||||
* - `scopes`: Array of VariableScope strings. Omit to use [] (hidden/primitive).
|
||||
* - `codeSyntax`: Platform code syntax strings. Omit to skip.
|
||||
* @param {string} [runId] - Optional dsb_run_id to tag every variable.
|
||||
* @returns {Promise<{variables: Record<string, Variable>}>}
|
||||
* `variables` maps each token name to its created Variable object.
|
||||
*/
|
||||
async function createSemanticTokens(collection, modeIds, tokenMap, runId) {
|
||||
const variables = {}
|
||||
|
||||
for (const token of tokenMap) {
|
||||
// Create the variable
|
||||
const variable = figma.variables.createVariable(token.name, collection, token.type)
|
||||
|
||||
// Tag for cleanup
|
||||
variable.setPluginData('dsb_key', `variable/${token.name}`)
|
||||
if (runId) {
|
||||
variable.setPluginData('dsb_run_id', runId)
|
||||
}
|
||||
|
||||
// Set values for each mode
|
||||
for (const [modeName, rawValue] of Object.entries(token.values)) {
|
||||
const modeId = modeIds[modeName]
|
||||
if (!modeId) {
|
||||
throw new Error(
|
||||
`createSemanticTokens: mode "${modeName}" not found in modeIds for token "${token.name}". ` +
|
||||
`Available modes: ${Object.keys(modeIds).join(', ')}`,
|
||||
)
|
||||
}
|
||||
|
||||
let value = rawValue
|
||||
|
||||
// Convert hex strings to Figma RGBA for COLOR type
|
||||
if (token.type === 'COLOR' && typeof rawValue === 'string' && rawValue.startsWith('#')) {
|
||||
value = hexToFigmaColor(rawValue)
|
||||
}
|
||||
|
||||
variable.setValueForMode(modeId, value)
|
||||
}
|
||||
|
||||
// Set scopes (default: empty array = hidden from property pickers / primitives)
|
||||
variable.scopes = token.scopes || []
|
||||
|
||||
// Set code syntax per platform
|
||||
if (token.codeSyntax) {
|
||||
if (token.codeSyntax.WEB) {
|
||||
variable.setVariableCodeSyntax('WEB', token.codeSyntax.WEB)
|
||||
}
|
||||
if (token.codeSyntax.ANDROID) {
|
||||
variable.setVariableCodeSyntax('ANDROID', token.codeSyntax.ANDROID)
|
||||
}
|
||||
if (token.codeSyntax.iOS) {
|
||||
variable.setVariableCodeSyntax('iOS', token.codeSyntax.iOS)
|
||||
}
|
||||
}
|
||||
|
||||
variables[token.name] = variable
|
||||
}
|
||||
|
||||
return { variables }
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a hex color string to a Figma RGBA object.
|
||||
* Supports "#rgb", "#rrggbb", and "#rrggbbaa".
|
||||
*
|
||||
* @param {string} hex
|
||||
* @returns {{ r: number, g: number, b: number, a: number }}
|
||||
*/
|
||||
function hexToFigmaColor(hex) {
|
||||
let h = hex.replace('#', '')
|
||||
|
||||
// Expand shorthand #rgb → #rrggbb
|
||||
if (h.length === 3) {
|
||||
h = h
|
||||
.split('')
|
||||
.map((c) => c + c)
|
||||
.join('')
|
||||
}
|
||||
|
||||
const r = parseInt(h.substring(0, 2), 16) / 255
|
||||
const g = parseInt(h.substring(2, 4), 16) / 255
|
||||
const b = parseInt(h.substring(4, 6), 16) / 255
|
||||
const a = h.length === 8 ? parseInt(h.substring(6, 8), 16) / 255 : 1
|
||||
|
||||
return { r, g, b, a }
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* createVariableCollection
|
||||
*
|
||||
* Creates a new Figma variable collection with the specified name and modes.
|
||||
* If `modeNames` has more than one entry, the first mode is renamed from
|
||||
* Figma's default "Mode 1" to the first name, and additional modes are added.
|
||||
*
|
||||
* Every created collection is tagged with `dsb_key` plugin data so it can be
|
||||
* found and cleaned up idempotently by `cleanupOrphans`.
|
||||
*
|
||||
* @param {string} name - The display name of the collection (e.g. "Color", "Spacing").
|
||||
* @param {string[]} modeNames - Ordered list of mode names (e.g. ["Light", "Dark"] or ["Value"]).
|
||||
* @param {string} [runId] - Optional dsb_run_id to tag for cleanup.
|
||||
* @returns {Promise<{
|
||||
* collection: VariableCollection,
|
||||
* modeIds: Record<string, string>
|
||||
* }>}
|
||||
* `modeIds` maps each mode name to its modeId string.
|
||||
*/
|
||||
async function createVariableCollection(name, modeNames, runId) {
|
||||
if (!modeNames || modeNames.length === 0) {
|
||||
throw new Error('createVariableCollection: modeNames must have at least one entry.')
|
||||
}
|
||||
|
||||
// Create the collection — Figma always creates it with one mode named "Mode 1".
|
||||
const collection = figma.variables.createVariableCollection(name)
|
||||
|
||||
// Tag for idempotent cleanup
|
||||
collection.setPluginData('dsb_key', `collection/${name}`)
|
||||
if (runId) {
|
||||
collection.setPluginData('dsb_run_id', runId)
|
||||
}
|
||||
|
||||
// modeIds accumulator
|
||||
const modeIds = {}
|
||||
|
||||
// Rename the default first mode
|
||||
const defaultMode = collection.modes[0]
|
||||
collection.renameMode(defaultMode.modeId, modeNames[0])
|
||||
modeIds[modeNames[0]] = defaultMode.modeId
|
||||
|
||||
// Add additional modes
|
||||
for (let i = 1; i < modeNames.length; i++) {
|
||||
const newModeId = collection.addMode(modeNames[i])
|
||||
modeIds[modeNames[i]] = newModeId
|
||||
}
|
||||
|
||||
return { collection, modeIds }
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
/**
|
||||
* inspectFileStructure
|
||||
*
|
||||
* Reads the current Figma file and returns a structural inventory:
|
||||
* all pages (with child counts), all local variable collections (with mode
|
||||
* names and variable counts), all component sets, all local text styles,
|
||||
* and all local effect styles.
|
||||
*
|
||||
* This is a read-only discovery function — it never creates or mutates nodes.
|
||||
* Run it at the start of Phase 0 to understand what already exists before
|
||||
* planning any creation work.
|
||||
*
|
||||
* @returns {Promise<{
|
||||
* pages: Array<{id: string, name: string, childCount: number}>,
|
||||
* variableCollections: Array<{
|
||||
* id: string,
|
||||
* name: string,
|
||||
* modes: Array<{modeId: string, name: string}>,
|
||||
* variableCount: number,
|
||||
* variableNames: string[]
|
||||
* }>,
|
||||
* componentSets: Array<{id: string, name: string, variantCount: number, pageId: string, pageName: string}>,
|
||||
* textStyles: Array<{id: string, name: string, fontFamily: string, fontStyle: string, fontSize: number}>,
|
||||
* effectStyles: Array<{id: string, name: string, effectCount: number}>
|
||||
* }>}
|
||||
*/
|
||||
async function inspectFileStructure() {
|
||||
const result = {
|
||||
pages: [],
|
||||
variableCollections: [],
|
||||
componentSets: [],
|
||||
textStyles: [],
|
||||
effectStyles: [],
|
||||
}
|
||||
|
||||
// --- Pages ---
|
||||
for (const page of figma.root.children) {
|
||||
result.pages.push({
|
||||
id: page.id,
|
||||
name: page.name,
|
||||
childCount: page.children.length,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Variable collections ---
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync()
|
||||
for (const coll of collections) {
|
||||
const variables = await Promise.all(
|
||||
coll.variableIds.map((id) => figma.variables.getVariableByIdAsync(id)),
|
||||
)
|
||||
const variableNames = variables.filter(Boolean).map((v) => v.name)
|
||||
|
||||
result.variableCollections.push({
|
||||
id: coll.id,
|
||||
name: coll.name,
|
||||
modes: coll.modes.map((m) => ({ modeId: m.modeId, name: m.name })),
|
||||
variableCount: coll.variableIds.length,
|
||||
variableNames,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Component sets (and standalone components) ---
|
||||
// We need to load all pages to inspect components across the whole file.
|
||||
const originalPage = figma.currentPage
|
||||
|
||||
for (const page of figma.root.children) {
|
||||
await figma.setCurrentPageAsync(page)
|
||||
|
||||
const componentSetsOnPage = page.findAllWithCriteria({ types: ['COMPONENT_SET'] })
|
||||
for (const cs of componentSetsOnPage) {
|
||||
result.componentSets.push({
|
||||
id: cs.id,
|
||||
name: cs.name,
|
||||
variantCount: cs.children.length,
|
||||
pageId: page.id,
|
||||
pageName: page.name,
|
||||
})
|
||||
}
|
||||
|
||||
// Also capture standalone components (not inside a component set)
|
||||
const standaloneComponents = page
|
||||
.findAllWithCriteria({ types: ['COMPONENT'] })
|
||||
.filter((c) => c.parent && c.parent.type !== 'COMPONENT_SET')
|
||||
for (const comp of standaloneComponents) {
|
||||
result.componentSets.push({
|
||||
id: comp.id,
|
||||
name: comp.name,
|
||||
variantCount: 1,
|
||||
pageId: page.id,
|
||||
pageName: page.name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original page
|
||||
await figma.setCurrentPageAsync(originalPage)
|
||||
|
||||
// --- Text styles ---
|
||||
const textStyles = figma.getLocalTextStyles()
|
||||
for (const ts of textStyles) {
|
||||
result.textStyles.push({
|
||||
id: ts.id,
|
||||
name: ts.name,
|
||||
fontFamily: ts.fontName.family,
|
||||
fontStyle: ts.fontName.style,
|
||||
fontSize: ts.fontSize,
|
||||
})
|
||||
}
|
||||
|
||||
// --- Effect styles ---
|
||||
const effectStyles = figma.getLocalEffectStyles()
|
||||
for (const es of effectStyles) {
|
||||
result.effectStyles.push({
|
||||
id: es.id,
|
||||
name: es.name,
|
||||
effectCount: es.effects.length,
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
/**
|
||||
* Scans the entire Figma file for nodes tagged with dsb_* pluginData
|
||||
* and returns a complete state map for session recovery.
|
||||
*
|
||||
* Use this at the start of every new session, after context truncation,
|
||||
* or when resuming an interrupted build.
|
||||
*
|
||||
* @param {string} runId - The run ID to filter by (optional — if omitted, returns ALL dsb-tagged nodes)
|
||||
* @returns {{ runId: string, taggedNodes: Object<string, {nodeId: string, type: string, name: string, phase: string}>, variableCollections: Array, variables: Array, styles: Array }}
|
||||
*/
|
||||
async function rehydrateState(runId) {
|
||||
const taggedNodes = {}
|
||||
const variableCollections = []
|
||||
const variables = []
|
||||
const styles = []
|
||||
|
||||
// Scan all pages for dsb-tagged scene nodes
|
||||
for (const page of figma.root.children) {
|
||||
await figma.setCurrentPageAsync(page)
|
||||
|
||||
// Check the page itself
|
||||
const pageRunId = page.getPluginData('dsb_run_id')
|
||||
const pageKey = page.getPluginData('dsb_key')
|
||||
if (pageKey && (!runId || pageRunId === runId)) {
|
||||
taggedNodes[pageKey] = {
|
||||
nodeId: page.id,
|
||||
type: page.type,
|
||||
name: page.name,
|
||||
phase: page.getPluginData('dsb_phase') || 'unknown',
|
||||
}
|
||||
}
|
||||
|
||||
// Scan all descendants
|
||||
page.findAll((node) => {
|
||||
const nodeRunId = node.getPluginData('dsb_run_id')
|
||||
const nodeKey = node.getPluginData('dsb_key')
|
||||
if (nodeKey && (!runId || nodeRunId === runId)) {
|
||||
taggedNodes[nodeKey] = {
|
||||
nodeId: node.id,
|
||||
type: node.type,
|
||||
name: node.name,
|
||||
phase: node.getPluginData('dsb_phase') || 'unknown',
|
||||
}
|
||||
}
|
||||
return false // don't collect, just scan
|
||||
})
|
||||
}
|
||||
|
||||
// Inventory variable collections (variables don't support pluginData — use name-based lookup)
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync()
|
||||
for (const coll of collections) {
|
||||
variableCollections.push({
|
||||
id: coll.id,
|
||||
name: coll.name,
|
||||
modes: coll.modes.map((m) => ({ modeId: m.modeId, name: m.name })),
|
||||
variableCount: coll.variableIds.length,
|
||||
})
|
||||
}
|
||||
|
||||
// Inventory variables (name + collection for idempotency key)
|
||||
const allVars = await figma.variables.getLocalVariablesAsync()
|
||||
for (const v of allVars) {
|
||||
variables.push({
|
||||
id: v.id,
|
||||
name: v.name,
|
||||
collectionId: v.variableCollectionId,
|
||||
resolvedType: v.resolvedType,
|
||||
})
|
||||
}
|
||||
|
||||
// Inventory styles
|
||||
for (const s of figma.getLocalTextStyles()) {
|
||||
styles.push({ id: s.id, name: s.name, type: 'TEXT' })
|
||||
}
|
||||
for (const s of figma.getLocalEffectStyles()) {
|
||||
styles.push({ id: s.id, name: s.name, type: 'EFFECT' })
|
||||
}
|
||||
for (const s of figma.getLocalPaintStyles()) {
|
||||
styles.push({ id: s.id, name: s.name, type: 'PAINT' })
|
||||
}
|
||||
|
||||
return {
|
||||
runId: runId || 'all',
|
||||
taggedNodes,
|
||||
taggedNodeCount: Object.keys(taggedNodes).length,
|
||||
variableCollections,
|
||||
variableCount: variables.length,
|
||||
variables,
|
||||
styleCount: styles.length,
|
||||
styles,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/**
|
||||
* validateCreation
|
||||
*
|
||||
* Verifies that a set of nodes exist and match expected structural properties.
|
||||
* Designed to run immediately after a creation script to catch partial failures
|
||||
* before proceeding to the next build phase.
|
||||
*
|
||||
* Each check specifies a node ID and any combination of expected properties.
|
||||
* A check passes when all specified expectations are met; it fails (with a
|
||||
* reason string) as soon as any expectation is violated.
|
||||
*
|
||||
* @param {Array<{
|
||||
* nodeId: string,
|
||||
* expectedChildCount?: number,
|
||||
* expectedName?: string,
|
||||
* expectedType?: NodeType
|
||||
* }>} checks
|
||||
* - `nodeId`: The Figma node ID to look up via figma.getNodeById.
|
||||
* - `expectedChildCount`: If set, the node must have exactly this many direct children.
|
||||
* Applies to any node with a `children` property (frames, component sets, etc.).
|
||||
* - `expectedName`: If set, the node's `.name` must exactly match this string.
|
||||
* - `expectedType`: If set, the node's `.type` must exactly match this string.
|
||||
* @returns {{
|
||||
* passed: string[],
|
||||
* failed: Array<{nodeId: string, reason: string}>
|
||||
* }}
|
||||
* `passed`: Array of nodeIds that passed all checks.
|
||||
* `failed`: Array of objects with the nodeId and a human-readable reason string.
|
||||
*/
|
||||
function validateCreation(checks) {
|
||||
const passed = []
|
||||
const failed = []
|
||||
|
||||
for (const check of checks) {
|
||||
const node = figma.getNodeById(check.nodeId)
|
||||
|
||||
// Node must exist
|
||||
if (!node) {
|
||||
failed.push({
|
||||
nodeId: check.nodeId,
|
||||
reason: `Node not found. It may not have been created, or was deleted.`,
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
const reasons = []
|
||||
|
||||
// Type check
|
||||
if (check.expectedType !== undefined && node.type !== check.expectedType) {
|
||||
reasons.push(`type is "${node.type}", expected "${check.expectedType}"`)
|
||||
}
|
||||
|
||||
// Name check
|
||||
if (check.expectedName !== undefined && node.name !== check.expectedName) {
|
||||
reasons.push(`name is "${node.name}", expected "${check.expectedName}"`)
|
||||
}
|
||||
|
||||
// Child count check
|
||||
if (check.expectedChildCount !== undefined) {
|
||||
if (!('children' in node)) {
|
||||
reasons.push(
|
||||
`node type "${node.type}" does not have children, but expectedChildCount=${check.expectedChildCount} was specified`,
|
||||
)
|
||||
} else {
|
||||
const actualCount = node.children.length
|
||||
if (actualCount !== check.expectedChildCount) {
|
||||
reasons.push(`has ${actualCount} children, expected ${check.expectedChildCount}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (reasons.length > 0) {
|
||||
failed.push({
|
||||
nodeId: check.nodeId,
|
||||
reason: reasons.join('; '),
|
||||
})
|
||||
} else {
|
||||
passed.push(check.nodeId)
|
||||
}
|
||||
}
|
||||
|
||||
return { passed, failed }
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
---
|
||||
name: figma-implement-design
|
||||
description: Translates Figma designs into production-ready application code with 1:1 visual fidelity. Use when implementing UI code from Figma files, when user mentions "implement design", "generate code", "implement component", provides Figma URLs, or asks to build components matching Figma specs. For Figma canvas writes via `use_figma`, use `figma-use`.
|
||||
disable-model-invocation: false
|
||||
---
|
||||
|
||||
# Implement Design
|
||||
|
||||
## Overview
|
||||
|
||||
This skill provides a structured workflow for translating Figma designs into production-ready code with pixel-perfect accuracy. It ensures consistent integration with the Figma MCP server, proper use of design tokens, and 1:1 visual parity with designs.
|
||||
|
||||
## Skill Boundaries
|
||||
|
||||
- Use this skill when the deliverable is code in the user's repository.
|
||||
- If the user asks to create/edit/delete nodes inside Figma itself, switch to [figma-use](../figma-use/SKILL.md).
|
||||
- If the user asks to build or update a full-page screen in Figma from code or a description, switch to [figma-generate-design](../figma-generate-design/SKILL.md).
|
||||
- If the user asks only for Code Connect mappings, switch to [figma-code-connect-components](../figma-code-connect-components/SKILL.md).
|
||||
- If the user asks to author reusable agent rules (`CLAUDE.md`/`AGENTS.md`), switch to [figma-create-design-system-rules](../figma-create-design-system-rules/SKILL.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Figma MCP server must be connected and accessible
|
||||
- User must provide a Figma URL in the format: `https://figma.com/design/:fileKey/:fileName?node-id=1-2`
|
||||
- `:fileKey` is the file key
|
||||
- `1-2` is the node ID (the specific component or frame to implement)
|
||||
- **OR** when using `figma-desktop` MCP: User can select a node directly in the Figma desktop app (no URL required)
|
||||
- Project should have an established design system or component library (preferred)
|
||||
|
||||
## Required Workflow
|
||||
|
||||
**Follow these steps in order. Do not skip steps.**
|
||||
|
||||
### Step 1: Get Node ID
|
||||
|
||||
#### Option A: Parse from Figma URL
|
||||
|
||||
When the user provides a Figma URL, extract the file key and node ID to pass as arguments to MCP tools.
|
||||
|
||||
**URL format:** `https://figma.com/design/:fileKey/:fileName?node-id=1-2`
|
||||
|
||||
**Extract:**
|
||||
|
||||
- **File key:** `:fileKey` (the segment after `/design/`)
|
||||
- **Node ID:** `1-2` (the value of the `node-id` query parameter)
|
||||
|
||||
**Note:** When using the local desktop MCP (`figma-desktop`), `fileKey` is not passed as a parameter to tool calls. The server automatically uses the currently open file, so only `nodeId` is needed.
|
||||
|
||||
**Example:**
|
||||
|
||||
- URL: `https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15`
|
||||
- File key: `kL9xQn2VwM8pYrTb4ZcHjF`
|
||||
- Node ID: `42-15`
|
||||
|
||||
#### Option B: Use Current Selection from Figma Desktop App (figma-desktop MCP only)
|
||||
|
||||
When using the `figma-desktop` MCP and the user has NOT provided a URL, the tools automatically use the currently selected node from the open Figma file in the desktop app.
|
||||
|
||||
**Note:** Selection-based prompting only works with the `figma-desktop` MCP server. The remote server requires a link to a frame or layer to extract context. The user must have the Figma desktop app open with a node selected.
|
||||
|
||||
### Step 2: Fetch Design Context
|
||||
|
||||
Run `get_design_context` with the extracted file key and node ID.
|
||||
|
||||
```
|
||||
get_design_context(fileKey=":fileKey", nodeId="1-2")
|
||||
```
|
||||
|
||||
This provides the structured data including:
|
||||
|
||||
- Layout properties (Auto Layout, constraints, sizing)
|
||||
- Typography specifications
|
||||
- Color values and design tokens
|
||||
- Component structure and variants
|
||||
- Spacing and padding values
|
||||
|
||||
**If the response is too large or truncated:**
|
||||
|
||||
1. Run `get_metadata(fileKey=":fileKey", nodeId="1-2")` to get the high-level node map
|
||||
2. Identify the specific child nodes needed from the metadata
|
||||
3. Fetch individual child nodes with `get_design_context(fileKey=":fileKey", nodeId=":childNodeId")`
|
||||
|
||||
### Step 3: Capture Visual Reference
|
||||
|
||||
Run `get_screenshot` with the same file key and node ID for a visual reference.
|
||||
|
||||
```
|
||||
get_screenshot(fileKey=":fileKey", nodeId="1-2")
|
||||
```
|
||||
|
||||
This screenshot serves as the source of truth for visual validation. Keep it accessible throughout implementation.
|
||||
|
||||
### Step 4: Download Required Assets
|
||||
|
||||
Download any assets (images, icons, SVGs) returned by the Figma MCP server.
|
||||
|
||||
**IMPORTANT:** Follow these asset rules:
|
||||
|
||||
- If the Figma MCP server returns a `localhost` source for an image or SVG, use that source directly
|
||||
- DO NOT import or add new icon packages - all assets should come from the Figma payload
|
||||
- DO NOT use or create placeholders if a `localhost` source is provided
|
||||
- Assets are served through the Figma MCP server's built-in assets endpoint
|
||||
|
||||
### Step 5: Translate to Project Conventions
|
||||
|
||||
Translate the Figma output into this project's framework, styles, and conventions.
|
||||
|
||||
**Key principles:**
|
||||
|
||||
- Treat the Figma MCP output (typically React + Tailwind) as a representation of design and behavior, not as final code style
|
||||
- Replace Tailwind utility classes with the project's preferred utilities or design system tokens
|
||||
- Reuse existing components (buttons, inputs, typography, icon wrappers) instead of duplicating functionality
|
||||
- Use the project's color system, typography scale, and spacing tokens consistently
|
||||
- Respect existing routing, state management, and data-fetch patterns
|
||||
|
||||
### Step 6: Achieve 1:1 Visual Parity
|
||||
|
||||
Strive for pixel-perfect visual parity with the Figma design.
|
||||
|
||||
**Guidelines:**
|
||||
|
||||
- Prioritize Figma fidelity to match designs exactly
|
||||
- Avoid hardcoded values - use design tokens from Figma where available
|
||||
- When conflicts arise between design system tokens and Figma specs, prefer design system tokens but adjust spacing or sizes minimally to match visuals
|
||||
- Follow WCAG requirements for accessibility
|
||||
- Add component documentation as needed
|
||||
|
||||
### Step 7: Validate Against Figma
|
||||
|
||||
Before marking complete, validate the final UI against the Figma screenshot.
|
||||
|
||||
**Validation checklist:**
|
||||
|
||||
- [ ] Layout matches (spacing, alignment, sizing)
|
||||
- [ ] Typography matches (font, size, weight, line height)
|
||||
- [ ] Colors match exactly
|
||||
- [ ] Interactive states work as designed (hover, active, disabled)
|
||||
- [ ] Responsive behavior follows Figma constraints
|
||||
- [ ] Assets render correctly
|
||||
- [ ] Accessibility standards met
|
||||
|
||||
## Implementation Rules
|
||||
|
||||
### Component Organization
|
||||
|
||||
- Place UI components in the project's designated design system directory
|
||||
- Follow the project's component naming conventions
|
||||
- Avoid inline styles unless truly necessary for dynamic values
|
||||
|
||||
### Design System Integration
|
||||
|
||||
- ALWAYS use components from the project's design system when possible
|
||||
- Map Figma design tokens to project design tokens
|
||||
- When a matching component exists, extend it rather than creating a new one
|
||||
- Document any new components added to the design system
|
||||
|
||||
### Code Quality
|
||||
|
||||
- Avoid hardcoded values - extract to constants or design tokens
|
||||
- Keep components composable and reusable
|
||||
- Add TypeScript types for component props
|
||||
- Include JSDoc comments for exported components
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Implementing a Button Component
|
||||
|
||||
User says: "Implement this Figma button component: https://figma.com/design/kL9xQn2VwM8pYrTb4ZcHjF/DesignSystem?node-id=42-15"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Parse URL to extract fileKey=`kL9xQn2VwM8pYrTb4ZcHjF` and nodeId=`42-15`
|
||||
2. Run `get_design_context(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")`
|
||||
3. Run `get_screenshot(fileKey="kL9xQn2VwM8pYrTb4ZcHjF", nodeId="42-15")` for visual reference
|
||||
4. Download any button icons from the assets endpoint
|
||||
5. Check if project has existing button component
|
||||
6. If yes, extend it with new variant; if no, create new component using project conventions
|
||||
7. Map Figma colors to project design tokens (e.g., `primary-500`, `primary-hover`)
|
||||
8. Validate against screenshot for padding, border radius, typography
|
||||
|
||||
**Result:** Button component matching Figma design, integrated with project design system.
|
||||
|
||||
### Example 2: Building a Dashboard Layout
|
||||
|
||||
User says: "Build this dashboard: https://figma.com/design/pR8mNv5KqXzGwY2JtCfL4D/Dashboard?node-id=10-5"
|
||||
|
||||
**Actions:**
|
||||
|
||||
1. Parse URL to extract fileKey=`pR8mNv5KqXzGwY2JtCfL4D` and nodeId=`10-5`
|
||||
2. Run `get_metadata(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` to understand the page structure
|
||||
3. Identify main sections from metadata (header, sidebar, content area, cards) and their child node IDs
|
||||
4. Run `get_design_context(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId=":childNodeId")` for each major section
|
||||
5. Run `get_screenshot(fileKey="pR8mNv5KqXzGwY2JtCfL4D", nodeId="10-5")` for the full page
|
||||
6. Download all assets (logos, icons, charts)
|
||||
7. Build layout using project's layout primitives
|
||||
8. Implement each section using existing components where possible
|
||||
9. Validate responsive behavior against Figma constraints
|
||||
|
||||
**Result:** Complete dashboard matching Figma design with responsive layout.
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Always Start with Context
|
||||
|
||||
Never implement based on assumptions. Always fetch `get_design_context` and `get_screenshot` first.
|
||||
|
||||
### Incremental Validation
|
||||
|
||||
Validate frequently during implementation, not just at the end. This catches issues early.
|
||||
|
||||
### Document Deviations
|
||||
|
||||
If you must deviate from the Figma design (e.g., for accessibility or technical constraints), document why in code comments.
|
||||
|
||||
### Reuse Over Recreation
|
||||
|
||||
Always check for existing components before creating new ones. Consistency across the codebase is more important than exact Figma replication.
|
||||
|
||||
### Design System First
|
||||
|
||||
When in doubt, prefer the project's design system patterns over literal Figma translation.
|
||||
|
||||
## Common Issues and Solutions
|
||||
|
||||
### Issue: Figma output is truncated
|
||||
|
||||
**Cause:** The design is too complex or has too many nested layers to return in a single response.
|
||||
**Solution:** Use `get_metadata` to get the node structure, then fetch specific nodes individually with `get_design_context`.
|
||||
|
||||
### Issue: Design doesn't match after implementation
|
||||
|
||||
**Cause:** Visual discrepancies between the implemented code and the original Figma design.
|
||||
**Solution:** Compare side-by-side with the screenshot from Step 3. Check spacing, colors, and typography values in the design context data.
|
||||
|
||||
### Issue: Assets not loading
|
||||
|
||||
**Cause:** The Figma MCP server's assets endpoint is not accessible or the URLs are being modified.
|
||||
**Solution:** Verify the Figma MCP server's assets endpoint is accessible. The server serves assets at `localhost` URLs. Use these directly without modification.
|
||||
|
||||
### Issue: Design token values differ from Figma
|
||||
|
||||
**Cause:** The project's design system tokens have different values than those specified in the Figma design.
|
||||
**Solution:** When project tokens differ from Figma values, prefer project tokens for consistency but adjust spacing/sizing to maintain visual fidelity.
|
||||
|
||||
## Understanding Design Implementation
|
||||
|
||||
The Figma implementation workflow establishes a reliable process for translating designs to code:
|
||||
|
||||
**For designers:** Confidence that implementations will match their designs with pixel-perfect accuracy.
|
||||
**For developers:** A structured approach that eliminates guesswork and reduces back-and-forth revisions.
|
||||
**For teams:** Consistent, high-quality implementations that maintain design system integrity.
|
||||
|
||||
By following this workflow, you ensure that every Figma design is implemented with the same level of care and attention to detail.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- [Figma MCP Server Documentation](https://developers.figma.com/docs/figma-mcp-server/)
|
||||
- [Figma MCP Server Tools and Prompts](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/)
|
||||
- [Figma Variables and Design Tokens](https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma)
|
||||
@@ -0,0 +1,234 @@
|
||||
---
|
||||
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 `use_figma` MCP 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 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 **MUST** be loaded before any text operation: `await figma.loadFontAsync({family, style})`
|
||||
9. **Pages load incrementally** — use `await figma.setCurrentPageAsync(page)` to switch pages and load their content (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` **throws an error** in `use_figma` runtimes.
|
||||
|
||||
```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"`) has a different set of available node types — most design nodes are blocked there.
|
||||
|
||||
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, Webpage.
|
||||
|
||||
## 5. 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.**
|
||||
|
||||
### 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. **Do one thing per call.** Create variables in one call, create components in the next, compose layouts in another. Don't try to build an entire screen in one script.
|
||||
3. **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.
|
||||
4. **Validate after each step.** Use `get_metadata` to verify structure (counts, names, hierarchy, positions). Use `get_screenshot` after major milestones to catch visual issues.
|
||||
5. **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 |
|
||||
|
||||
## 6. 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 | Use `await figma.setCurrentPageAsync(page)` |
|
||||
| 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).
|
||||
|
||||
## 7. 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)
|
||||
- [ ] Fills/strokes are reassigned as new arrays (not mutated in place)
|
||||
- [ ] Page switches use `await figma.setCurrentPageAsync(page)` (sync setter throws)
|
||||
- [ ] `layoutSizingVertical/Horizontal = 'FILL'` is set AFTER `parent.appendChild(child)`
|
||||
- [ ] `loadFontAsync()` called BEFORE any text property changes
|
||||
- [ ] `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
|
||||
|
||||
## 8. 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;
|
||||
```
|
||||
|
||||
## 9. 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 probing, 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 |
|
||||
|
||||
## 10. 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.
|
||||
@@ -0,0 +1,304 @@
|
||||
# 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.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 headless mode
|
||||
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 in use_figma; sync setter throws)
|
||||
figma.setCurrentPageAsync(page) // Switch page and load its content (MUST await)
|
||||
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('<svg>...</svg>')
|
||||
```
|
||||
|
||||
## Images
|
||||
|
||||
```js
|
||||
const image = figma.createImage(uint8Array)
|
||||
node.fills = [{ type: 'IMAGE', scaleMode: 'FILL', imageHash: image.hash }]
|
||||
```
|
||||
|
||||
## 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.listAvailableFontsAsync()` | Not implemented |
|
||||
| `figma.loadAllPagesAsync()` | Not implemented |
|
||||
| `figma.variables.extendLibraryCollectionByKeyAsync()` | Not implemented |
|
||||
| `figma.teamLibrary.*` | Not implemented (requires LiveGraph) |
|
||||
@@ -0,0 +1,441 @@
|
||||
# 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.createFrame()
|
||||
frame.name = "Card"
|
||||
frame.layoutMode = 'VERTICAL'
|
||||
frame.primaryAxisAlignItems = 'MIN'
|
||||
frame.counterAxisAlignItems = 'MIN'
|
||||
frame.paddingLeft = 16
|
||||
frame.paddingRight = 16
|
||||
frame.paddingTop = 12
|
||||
frame.paddingBottom = 12
|
||||
frame.itemSpacing = 8
|
||||
frame.layoutSizingHorizontal = 'HUG'
|
||||
frame.layoutSizingVertical = 'HUG'
|
||||
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.resizeWithoutConstraints(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 }
|
||||
```
|
||||
@@ -0,0 +1,472 @@
|
||||
# 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
|
||||
- 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
|
||||
|
||||
## 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('<svg>...</svg>');
|
||||
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<ComponentNode|ComponentSetNode>}
|
||||
*/
|
||||
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<string, {name: string, type: string, key: string, variantOptions?: string[]}>}
|
||||
*/
|
||||
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<string, object>} 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;
|
||||
```
|
||||
@@ -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<Array<{id: string, name: string, key: string, effectCount: number}>>}
|
||||
*/
|
||||
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 };
|
||||
```
|
||||
@@ -0,0 +1,622 @@
|
||||
# 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
|
||||
- Variable scopes and mode pitfalls
|
||||
- Node cleanup and empty-fill pitfalls
|
||||
- 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.createFrame()
|
||||
card.layoutMode = '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 throws
|
||||
|
||||
The sync setter `figma.currentPage = page` **throws an error** in `use_figma` runtimes (MCP, evals, assistant). Use `await figma.setCurrentPageAsync(page)` instead — it switches the page and loads its content.
|
||||
|
||||
```js
|
||||
// WRONG — throws "Setting figma.currentPage is not supported in this runtime"
|
||||
figma.currentPage = targetPage
|
||||
|
||||
// CORRECT — async method switches and loads content
|
||||
await figma.setCurrentPageAsync(targetPage)
|
||||
```
|
||||
|
||||
## `get_metadata` only sees one page — use `use_figma` to discover all pages
|
||||
|
||||
A Figma file can have multiple pages (canvas nodes). `get_metadata` operates on a single node/page — it cannot scan the entire document. To discover all pages and their top-level contents, use `use_figma`:
|
||||
|
||||
```js
|
||||
// WRONG — calling get_metadata with the file root or expecting it to list all pages
|
||||
// get_metadata only returns the subtree of the node you pass it
|
||||
|
||||
// CORRECT — use use_figma to list pages, then inspect each one
|
||||
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 the `use_figma` runtime. 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)
|
||||
```
|
||||
|
||||
## `TextStyle.setBoundVariable` is not available in headless use_figma
|
||||
|
||||
`setBoundVariable` exists on `TextStyle` in the typed API but is **not available** when running scripts through `use_figma` (MCP, headless assistant mode). Calling it will throw `"not a function"`.
|
||||
|
||||
```js
|
||||
// WRONG — throws "not a function" in use_figma / headless
|
||||
const ts = figma.createTextStyle()
|
||||
ts.setBoundVariable("fontSize", fontSizeVar)
|
||||
|
||||
// CORRECT (headless) — set raw values; bind variables interactively in Figma later
|
||||
const ts = figma.createTextStyle()
|
||||
ts.fontSize = 24
|
||||
```
|
||||
|
||||
This only affects `TextStyle`. Variable binding on **nodes** (`node.setBoundVariable(...)`) and on **paint objects** (`figma.variables.setBoundVariableForPaint(...)`) still works in headless mode as expected.
|
||||
|
||||
If live variable binding on text styles is required, create the styles with raw values via `use_figma`, then bind variables interactively through the Figma Styles panel or a full interactive plugin.
|
||||
|
||||
## `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 — probe before assuming
|
||||
|
||||
Font style names vary per provider and per Figma file. `"SemiBold"` and `"Semi Bold"` are different strings. Loading a font with the wrong style string **throws silently or errors** — there is no canonical list.
|
||||
|
||||
```js
|
||||
// WRONG — guessing style names
|
||||
await figma.loadFontAsync({ family: "Inter", style: "SemiBold" }) // may throw
|
||||
|
||||
// CORRECT — probe which style names are available
|
||||
const candidates = ["SemiBold", "Semi Bold", "Semibold"]
|
||||
for (const style of candidates) {
|
||||
try {
|
||||
await figma.loadFontAsync({ family: "Inter", style })
|
||||
// capture the one that works
|
||||
break
|
||||
} catch (_) {}
|
||||
}
|
||||
```
|
||||
|
||||
When building a type ramp script, always verify font styles against the target file before hardcoding them.
|
||||
|
||||
## combineAsVariants does NOT auto-layout in headless mode
|
||||
|
||||
```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)
|
||||
```
|
||||
|
||||
## COLOR variable values use {r, g, b, a} (with alpha)
|
||||
|
||||
```js
|
||||
// Paint colors use {r, g, b} (no alpha — opacity is a separate paint property)
|
||||
node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]
|
||||
|
||||
// But COLOR variable values use {r, g, b, a} — alpha maps to paint opacity
|
||||
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
|
||||
|
||||
// ⚠️ Don't confuse: {r, g, b} for paint colors vs {r, g, b, a} for variable values
|
||||
```
|
||||
|
||||
## `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!
|
||||
```
|
||||
|
||||
## 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.createFrame()
|
||||
parent.layoutMode = 'HORIZONTAL'
|
||||
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.createFrame()
|
||||
parent.layoutMode = 'HORIZONTAL'
|
||||
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.createFrame()
|
||||
content.layoutMode = 'VERTICAL'
|
||||
content.primaryAxisSizingMode = 'AUTO'
|
||||
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
|
||||
```
|
||||
|
||||
## `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.resizeWithoutConstraints(
|
||||
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'
|
||||
|
||||
## `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.
|
||||
@@ -0,0 +1,516 @@
|
||||
# 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.
|
||||
|
||||
```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 = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M5 12h14M12 5l7 7-7 7" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>`;
|
||||
|
||||
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
|
||||
|
||||
```javascript
|
||||
const frame = figma.createFrame();
|
||||
frame.layoutMode = "VERTICAL"; // or "HORIZONTAL"
|
||||
frame.primaryAxisSizingMode = "AUTO"; // Hug main axis
|
||||
frame.counterAxisSizingMode = "FIXED"; // Fixed cross axis
|
||||
frame.resize(360, 1); // Width fixed, height auto
|
||||
frame.itemSpacing = 16; // Gap between children
|
||||
frame.paddingTop = 24;
|
||||
frame.paddingBottom = 24;
|
||||
frame.paddingLeft = 24;
|
||||
frame.paddingRight = 24;
|
||||
```
|
||||
|
||||
### 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.resizeWithoutConstraints(800, 600);
|
||||
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]);
|
||||
```
|
||||
+11293
File diff suppressed because one or more lines are too long
+437
@@ -0,0 +1,437 @@
|
||||
# Plugin API Index
|
||||
|
||||
> Full typings: `plugin-api-standalone.d.ts` (11,292 lines)
|
||||
> Grep by symbol name to jump to definition. All `L#` line numbers refer to that file.
|
||||
|
||||
---
|
||||
|
||||
## figma.\* — PluginAPI (L24)
|
||||
|
||||
### Identity & State
|
||||
|
||||
| Member | Type |
|
||||
| ------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `apiVersion` | `'1.0.0'` |
|
||||
| `editorType` | `'figma' \| 'figjam' \| 'dev' \| 'slides' \| 'buzz'` |
|
||||
| `mode` | `'default' \| 'textreview' \| 'inspect' \| 'codegen' \| 'linkpreview' \| 'auth'` |
|
||||
| `fileKey` | `string \| undefined` |
|
||||
| `root` | `DocumentNode` |
|
||||
| `currentPage` | `PageNode` — assign via `setCurrentPageAsync` |
|
||||
| `currentUser` | `User \| null` |
|
||||
| `mixed` | `unique symbol` — sentinel for mixed values in selection |
|
||||
| `skipInvisibleInstanceChildren` | `boolean` |
|
||||
|
||||
### Navigation & Lookup
|
||||
|
||||
| Method | Returns |
|
||||
| --------------------------- | ------------------------------------------------------- |
|
||||
| `setCurrentPageAsync(page)` | `Promise<void>` — **MUST use this**; sync setter throws |
|
||||
| `getNodeByIdAsync(id)` | `Promise<BaseNode \| null>` |
|
||||
| `getNodeById(id)` | `BaseNode \| null` |
|
||||
| `getStyleByIdAsync(id)` | `Promise<BaseStyle \| null>` |
|
||||
| `getStyleById(id)` | `BaseStyle \| null` |
|
||||
|
||||
### Create Nodes
|
||||
|
||||
| Method | Returns |
|
||||
| ----------------------------------- | --------------------------- |
|
||||
| `createFrame()` | `FrameNode` |
|
||||
| `createComponent()` | `ComponentNode` |
|
||||
| `createComponentFromNode(node)` | `ComponentNode` |
|
||||
| `createRectangle()` | `RectangleNode` |
|
||||
| `createEllipse()` | `EllipseNode` |
|
||||
| `createLine()` | `LineNode` |
|
||||
| `createPolygon()` | `PolygonNode` |
|
||||
| `createStar()` | `StarNode` |
|
||||
| `createVector()` | `VectorNode` |
|
||||
| `createText()` | `TextNode` |
|
||||
| `createSection()` | `SectionNode` |
|
||||
| `createPage()` | `PageNode` |
|
||||
| `createSlice()` | `SliceNode` |
|
||||
| `createBooleanOperation()` | `BooleanOperationNode` |
|
||||
| `createTable(rows?, cols?)` | `TableNode` |
|
||||
| `createImage(data: Uint8Array)` | `Image` |
|
||||
| `createNodeFromSvg(svg)` | `FrameNode` |
|
||||
| `createNodeFromJSXAsync(jsx)` | `Promise<SceneNode>` |
|
||||
| `importComponentByKeyAsync(key)` | `Promise<ComponentNode>` |
|
||||
| `importComponentSetByKeyAsync(key)` | `Promise<ComponentSetNode>` |
|
||||
| `importStyleByKeyAsync(key)` | `Promise<BaseStyle>` |
|
||||
|
||||
### Styles (Local)
|
||||
|
||||
| Method | Returns |
|
||||
| ---------------------------------- | --------------- |
|
||||
| `createPaintStyle()` | `PaintStyle` |
|
||||
| `createTextStyle()` | `TextStyle` |
|
||||
| `createEffectStyle()` | `EffectStyle` |
|
||||
| `createGridStyle()` | `GridStyle` |
|
||||
| `getLocalPaintStyles()` / `Async` | `PaintStyle[]` |
|
||||
| `getLocalTextStyles()` / `Async` | `TextStyle[]` |
|
||||
| `getLocalEffectStyles()` / `Async` | `EffectStyle[]` |
|
||||
| `getLocalGridStyles()` / `Async` | `GridStyle[]` |
|
||||
|
||||
### Fonts
|
||||
|
||||
| Method | Notes |
|
||||
| --------------------------- | ---------------------------------- |
|
||||
| `loadFontAsync(fontName)` | **MUST call before any text edit** |
|
||||
| `listAvailableFontsAsync()` | `Promise<Font[]>` |
|
||||
| `hasMissingFont` | `boolean` |
|
||||
|
||||
### Plugin Lifecycle
|
||||
|
||||
| Method | Notes |
|
||||
| --------------------------------------- | ------------------------------------------------------------ |
|
||||
| `closePlugin(message?)` | Auto-called; use `return` instead to pass results back |
|
||||
| `closePluginWithFailure(message?)` | Auto-called on errors; do not call manually |
|
||||
| `commitUndo()` | Snapshot to undo history |
|
||||
| `triggerUndo()` | Revert to last snapshot |
|
||||
| `saveVersionHistoryAsync(title, desc?)` | `Promise<VersionHistoryResult>` |
|
||||
| `notify(message, options?)` | **throws "not implemented" in use_figma — do not use** |
|
||||
| `openExternal(url)` | Opens URL in browser |
|
||||
|
||||
### Sub-APIs (properties on figma)
|
||||
|
||||
| Property | Interface | L# |
|
||||
| --------------------- | ------------------------ | ----- |
|
||||
| `figma.variables` | `VariablesAPI` | L2016 |
|
||||
| `figma.ui` | `UIAPI` | L2604 |
|
||||
| `figma.util` | `UtilAPI` | L2691 |
|
||||
| `figma.constants` | `ConstantsAPI` | L2809 |
|
||||
| `figma.clientStorage` | `ClientStorageAPI` | L2531 |
|
||||
| `figma.viewport` | `ViewportAPI` | L3086 |
|
||||
| `figma.parameters` | `ParametersAPI` | L3292 |
|
||||
| `figma.teamLibrary` | `TeamLibraryAPI` | L2372 |
|
||||
| `figma.annotations` | `AnnotationsAPI` | L2187 |
|
||||
| `figma.codegen` | `CodegenAPI` | L2871 |
|
||||
| `figma.textreview?` | `TextReviewAPI` | L3166 |
|
||||
| `figma.payments?` | `PaymentsAPI` | L2420 |
|
||||
| `figma.buzz` | `BuzzAPI` | L2211 |
|
||||
| `figma.timer?` | `TimerAPI` (FigJam only) | L3053 |
|
||||
|
||||
---
|
||||
|
||||
## VariablesAPI — figma.variables (L2016)
|
||||
|
||||
```
|
||||
getVariableByIdAsync(id) Promise<Variable | null> ← preferred; sync deprecated
|
||||
getVariableCollectionByIdAsync(id) Promise<VariableCollection | null> ← preferred; sync deprecated
|
||||
getLocalVariablesAsync(type?) Promise<Variable[]> ← preferred; filter by VariableResolvedDataType; sync deprecated
|
||||
getLocalVariableCollectionsAsync() Promise<VariableCollection[]> ← preferred; sync deprecated
|
||||
createVariable(name, collection, type) Variable
|
||||
createVariableCollection(name) VariableCollection
|
||||
createVariableAlias(variable) VariableAlias
|
||||
importVariableByKeyAsync(key) Promise<Variable>
|
||||
setBoundVariableForPaint(paint, field, variable) → returns NEW paint — reassign
|
||||
setBoundVariableForEffect(effect, field, variable) → returns NEW effect — reassign
|
||||
setBoundVariableForLayoutGrid(grid, field, variable)
|
||||
```
|
||||
|
||||
**Variable (L10204):** `name`, `resolvedType`, `codeSyntax`, `scopes`, `hiddenFromPublishing`, `valuesByMode`, `variableCollectionId`
|
||||
|
||||
- `setVariableCodeSyntax(platform, value)` — platform: `'WEB' | 'ANDROID' | 'iOS'`
|
||||
- `setValueForMode(collectionId, modeId, value)`
|
||||
- `remove()`
|
||||
|
||||
**VariableCollection (L10418):** `name`, `modes`, `variableIds`, `defaultModeId`, `hiddenFromPublishing`
|
||||
|
||||
- `addMode(name)` → `modeId`; `removeMode(modeId)`; `renameMode(modeId, name)`
|
||||
|
||||
---
|
||||
|
||||
## Node Types
|
||||
|
||||
### Concrete Scene Nodes
|
||||
|
||||
| Node | L# | Key characteristics |
|
||||
| ---------------------- | ------ | -------------------------------------------------- |
|
||||
| `DocumentNode` | L8960 | Root; `children: PageNode[]` |
|
||||
| `PageNode` | L9119 | `children`, local styles, `backgrounds` |
|
||||
| `FrameNode` | L9311 | `DefaultFrameMixin` — auto-layout, clips, children |
|
||||
| `GroupNode` | L9321 | Children only, no auto-layout |
|
||||
| `ComponentNode` | L9678 | Like Frame + publishable |
|
||||
| `ComponentSetNode` | L9653 | Variant set container |
|
||||
| `InstanceNode` | L9719 | Like Frame; `mainComponent`, `detach()` |
|
||||
| `RectangleNode` | L9378 | `DefaultShapeMixin` + corners |
|
||||
| `EllipseNode` | L9410 | + `arcData` |
|
||||
| `LineNode` | L9396 | |
|
||||
| `PolygonNode` | L9430 | |
|
||||
| `StarNode` | L9450 | |
|
||||
| `VectorNode` | L9476 | Vector paths |
|
||||
| `TextNode` | L9493 | Rich text, fonts, segments |
|
||||
| `TextPathNode` | L9564 | Text along path |
|
||||
| `BooleanOperationNode` | L9792 | `booleanOperation` property |
|
||||
| `SliceNode` | L9368 | Export only |
|
||||
| `SectionNode` | L10754 | Grouping + fills |
|
||||
| `TableNode` | L9862 | `TableCellNode` children |
|
||||
|
||||
**FigJam only:** `StickyNode` L9812, `ConnectorNode` L10121, `ShapeWithTextNode` L9999, `StampNode` L9838, `CodeBlockNode` L10080, `EmbedNode` L10661, `LinkUnfurlNode` L10701, `MediaNode` L10721
|
||||
|
||||
**Slides only:** `SlideNode` L10784, `SlideRowNode` L10809, `SlideGridNode` L10822
|
||||
|
||||
**Union types:**
|
||||
|
||||
```
|
||||
type SceneNode (L10917) = FrameNode | GroupNode | SliceNode | RectangleNode | LineNode
|
||||
| EllipseNode | PolygonNode | StarNode | VectorNode | TextNode | ComponentSetNode
|
||||
| ComponentNode | InstanceNode | BooleanOperationNode | SectionNode | ...
|
||||
type BaseNode (L10913) = DocumentNode | PageNode | SceneNode
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mixin Interfaces
|
||||
|
||||
| Mixin | L# | Provides |
|
||||
| ---------------------------- | ----- | ----------------------------------------------------------------------------------------------- |
|
||||
| `BaseNodeMixin` | L5284 | `id`, `name`, `type`, `parent`, `remove()`, plugin data |
|
||||
| `SceneNodeMixin` | L5561 | `visible`, `locked`, `opacity`, variable bindings |
|
||||
| `ChildrenMixin` | L5773 | `children`, `appendChild()`, `insertChild()`, `findAll()`, `findOne()`, `findAllWithCriteria()` |
|
||||
| `LayoutMixin` | L6135 | `x`, `y`, `width`, `height`, `rotation`, `resize()`, `rescale()` |
|
||||
| `AutoLayoutMixin` | L6436 | `layoutMode`, axis alignment, padding, `itemSpacing`, `layoutSizingHorizontal/Vertical` |
|
||||
| `AutoLayoutChildrenMixin` | L7064 | `layoutAlign`, `layoutGrow`, sizing — **set AFTER `appendChild()`** |
|
||||
| `GridLayoutMixin` | L6939 | CSS Grid tracks, gap, template |
|
||||
| `GridChildrenMixin` | L7127 | grid child positioning |
|
||||
| `GeometryMixin` | L7485 | `fills`, `strokes`, `strokeWeight`, `strokeAlign` |
|
||||
| `MinimalFillsMixin` | L7328 | `fills` only |
|
||||
| `MinimalStrokesMixin` | L7246 | `strokes`, `strokeWeight` |
|
||||
| `BlendMixin` | L6339 | `opacity`, `blendMode`, `isMask`, `effects` |
|
||||
| `CornerMixin` | L7537 | `cornerRadius`, `cornerSmoothing` |
|
||||
| `RectangleCornerMixin` | L7560 | Per-corner radii |
|
||||
| `ExportMixin` | L7577 | `exportSettings`, `exportAsync()` |
|
||||
| `ReactionMixin` | L7704 | `reactions` (prototyping) |
|
||||
| `PublishableMixin` | L7875 | `description`, `key`, `getPublishStatusAsync()` |
|
||||
| `VariantMixin` | L8182 | `variantProperties` |
|
||||
| `ComponentPropertiesMixin` | L8229 | `componentProperties`, `addComponentProperty()` |
|
||||
| `PluginDataMixin` | L5443 | `getSharedPluginData()`, `setSharedPluginData()` supported; `getPluginData()`, `setPluginData()` **NOT supported** |
|
||||
| `FramePrototypingMixin` | L7651 | `overflowDirection`, `numberOfFixedChildren` |
|
||||
| `BaseFrameMixin` | L7939 | ChildrenMixin + LayoutMixin + AutoLayoutMixin + GeometryMixin + … |
|
||||
| `DefaultFrameMixin` | L7997 | BaseFrameMixin + FramePrototypingMixin + ReactionMixin |
|
||||
| `DefaultShapeMixin` | L7928 | BlendMixin + GeometryMixin + LayoutMixin + ExportMixin + ReactionMixin |
|
||||
| `ExplicitVariableModesMixin` | L9084 | `setExplicitVariableModeForCollection()` |
|
||||
|
||||
---
|
||||
|
||||
## Paint & Fill (L4302)
|
||||
|
||||
| Type | L# | Notes |
|
||||
| --------------- | ----- | --------------------------------------------------------------------------------- |
|
||||
| `SolidPaint` | L4302 | `type:'SOLID'`, `color: RGB`, `opacity`, `visible`, `blendMode` |
|
||||
| `GradientPaint` | L4357 | `type: 'GRADIENT_LINEAR\|RADIAL\|ANGULAR\|DIAMOND'`, `gradientStops: ColorStop[]` |
|
||||
| `ImagePaint` | L4377 | `type:'IMAGE'`, `imageHash`, `scaleMode` |
|
||||
| `VideoPaint` | L4413 | `type:'VIDEO'` |
|
||||
| `PatternPaint` | L4449 | `type:'PATTERN'` |
|
||||
| `type Paint` | L4481 | Union of all five |
|
||||
| `ColorStop` | L4271 | `{ position: number, color: RGBA }` |
|
||||
| `ImageFilters` | L4290 | exposure, contrast, saturation, etc. |
|
||||
|
||||
> **CRITICAL**: Fills/strokes are **read-only arrays** — clone, modify, reassign.
|
||||
|
||||
---
|
||||
|
||||
## Effects (L3966)
|
||||
|
||||
| Type | L# |
|
||||
| ---------------------------------- | ----- |
|
||||
| `DropShadowEffect` | L3966 |
|
||||
| `InnerShadowEffect` | L4009 |
|
||||
| `BlurEffect` (Normal/Progressive) | L4048 |
|
||||
| `NoiseEffect` (Mono/Duo/Multitone) | L4105 |
|
||||
| `TextureEffect` | L4180 |
|
||||
| `GlassEffect` | L4209 |
|
||||
| `type Effect` | L4250 |
|
||||
|
||||
---
|
||||
|
||||
## Typography
|
||||
|
||||
| Type | L# | Notes |
|
||||
| ------------------- | ----- | -------------------------------------------------------------------------------------- |
|
||||
| `FontName` | L3697 | `{ family: string, style: string }` |
|
||||
| `TextNode` | L9493 | `characters`, `textAlignHorizontal`, `fontSize`, `fontName`, `getStyledTextSegments()` |
|
||||
| `StyledTextSegment` | L4882 | Per-range text properties |
|
||||
| `LetterSpacing` | L4826 | `{ value, unit: 'PIXELS'\|'PERCENT' }` |
|
||||
| `LineHeight` | L4830 | `{ value, unit } \| { unit: 'AUTO' }` |
|
||||
| `TextCase` | L3701 | `'ORIGINAL'\|'UPPER'\|'LOWER'\|'TITLE'\|'SMALL_CAPS'` |
|
||||
| `TextDecoration` | L3702 | `'NONE'\|'UNDERLINE'\|'STRIKETHROUGH'` |
|
||||
| `OpenTypeFeature` | L3728 | Ligatures, numerals, etc. |
|
||||
|
||||
---
|
||||
|
||||
## Variables & Bindings
|
||||
|
||||
| Type | L# | Notes |
|
||||
| ----------------------------- | ------ | ------------------------------------------------------------- |
|
||||
| `Variable` | L10204 | Core variable object |
|
||||
| `VariableCollection` | L10418 | Collection of variables + modes |
|
||||
| `VariableAlias` | L10172 | Reference to another variable |
|
||||
| `VariableValue` | L10176 | `boolean \| string \| number \| RGB \| RGBA \| VariableAlias` |
|
||||
| `VariableResolvedDataType` | L10171 | `'BOOLEAN' \| 'COLOR' \| 'FLOAT' \| 'STRING'` |
|
||||
| `VariableDataType` | L5023 | Includes `'VARIABLE_ALIAS' \| 'EXPRESSION'` |
|
||||
| `VariableScope` | L10177 | Where variable can be applied |
|
||||
| `CodeSyntaxPlatform` | L10203 | `'WEB' \| 'ANDROID' \| 'iOS'` |
|
||||
| `VariableBindableNodeField` | L5712 | Node fields that accept variable binding |
|
||||
| `VariableBindableTextField` | L5739 | Text-specific bindable fields |
|
||||
| `VariableBindablePaintField` | L5748 | `'color'` |
|
||||
| `VariableBindableEffectField` | L5751 | `'color'\|'radius'\|'spread'\|'offsetX'\|'offsetY'` |
|
||||
|
||||
---
|
||||
|
||||
## Styles
|
||||
|
||||
| Interface | L# | Notes |
|
||||
| ---------------- | ------ | ------------------------------------------------------ |
|
||||
| `BaseStyleMixin` | L10977 | `name`, `id`, `key`, `type`, `description`, `remove()` |
|
||||
| `PaintStyle` | L11002 | `type:'PAINT'`, `paints: Paint[]` |
|
||||
| `TextStyle` | L11018 | `type:'TEXT'`, font properties |
|
||||
| `EffectStyle` | L11087 | `type:'EFFECT'`, `effects: Effect[]` |
|
||||
| `GridStyle` | L11103 | `type:'GRID'`, `layoutGrids` |
|
||||
| `type BaseStyle` | L11119 | Union of all four |
|
||||
| `type StyleType` | L10955 | `'PAINT' \| 'TEXT' \| 'EFFECT' \| 'GRID'` |
|
||||
|
||||
---
|
||||
|
||||
## Primitives & Geometry
|
||||
|
||||
| Type | L# | Shape |
|
||||
| ---------------- | ----- | --------------------------------------------- |
|
||||
| `Vector` | L3667 | `{ x: number, y: number }` |
|
||||
| `Rect` | L3671 | `{ x, y, width, height }` |
|
||||
| `RGB` | L3680 | `{ r, g, b }` — **0–1 range, not 0–255** |
|
||||
| `RGBA` | L3688 | `{ r, g, b, a }` — **0–1 range** |
|
||||
| `Transform` | L3666 | `[[a,b,tx],[c,d,ty]]` 2×3 affine matrix |
|
||||
| `ArcData` | L3958 | `{ startingAngle, endingAngle, innerRadius }` |
|
||||
| `Constraints` | L4264 | `{ horizontal, vertical }: ConstraintType` |
|
||||
| `ConstraintType` | L4260 | `'MIN'\|'CENTER'\|'MAX'\|'STRETCH'\|'SCALE'` |
|
||||
| `VectorPath` | L4792 | `{ windingRule, data: string }` |
|
||||
| `VectorNetwork` | L4775 | vertices + segments + regions |
|
||||
| `Guide` | L4482 | `{ axis, offset }` |
|
||||
|
||||
---
|
||||
|
||||
## Prototyping
|
||||
|
||||
| Type | L# | Notes |
|
||||
| --------------------- | ----- | --------------------------------------------------------- |
|
||||
| `Reaction` | L5015 | trigger + action pair |
|
||||
| `Trigger` | L5146 | what initiates the reaction |
|
||||
| `Action` | L5064 | what happens |
|
||||
| `Transition` | L5145 | `SimpleTransition \| DirectionalTransition` |
|
||||
| `Easing` | L5182 | easing curve definition |
|
||||
| `Navigation` | L5178 | `'NAVIGATE'\|'SWAP'\|'OVERLAY'\|'SCROLL_TO'\|'CHANGE_TO'` |
|
||||
| `OverflowDirection` | L5215 | `'NONE'\|'HORIZONTAL'\|'VERTICAL'\|'BOTH'` |
|
||||
| `OverlayPositionType` | L5219 | overlay placement |
|
||||
|
||||
---
|
||||
|
||||
## Events & Changes
|
||||
|
||||
| Type | L# | Notes |
|
||||
| --------------------- | ----- | --------------------------------------------------------------- |
|
||||
| `ArgFreeEventType` | L11 | `'selectionchange'\|'currentpagechange'\|'close'\|timer events` |
|
||||
| `RunEvent` | L3321 | plugin run with parameters |
|
||||
| `DropEvent` | L3339 | drag-and-drop |
|
||||
| `DocumentChangeEvent` | L3359 | any document change |
|
||||
| `NodeChangeEvent` | L3626 | node property changes |
|
||||
| `NodeChangeProperty` | L3499 | all watchable property names |
|
||||
| `StyleChangeEvent` | L3365 | style create/delete/update |
|
||||
| `DocumentChange` | L3489 | `CreateChange \| DeleteChange \| PropertyChange` |
|
||||
| `TextReviewEvent` | L3657 | text review mode |
|
||||
|
||||
---
|
||||
|
||||
## Export
|
||||
|
||||
| Type | L# | Notes |
|
||||
| --------------------------- | ----- | --------------------------------------------- |
|
||||
| `ExportSettingsImage` | L4561 | PNG/JPG/WEBP/BMP |
|
||||
| `ExportSettingsSVG` | L4634 | |
|
||||
| `ExportSettingsPDF` | L4653 | |
|
||||
| `ExportSettingsREST` | L4667 | |
|
||||
| `ExportSettingsConstraints` | L4554 | `{ type: 'SCALE'\|'WIDTH'\|'HEIGHT', value }` |
|
||||
|
||||
---
|
||||
|
||||
## Key Sub-API Surfaces
|
||||
|
||||
**ClientStorageAPI (L2531):** `getAsync(key)`, `setAsync(key, value)`, `keysAsync()`, `deleteAsync(key)`
|
||||
|
||||
**ViewportAPI (L3086):** `center: Vector`, `zoom: number`, `scrollAndZoomIntoView(nodes)`, `bounds: Rect`
|
||||
|
||||
**UtilAPI (L2691):** `solidPaint(hex, opacity?)`, `rgba(r,g,b,a?)`, `rgb(r,g,b)`, `colorToHex(color)`, `loadImageAsync(url)`, `clone(val)`
|
||||
|
||||
**TeamLibraryAPI (L2372):** `getAvailableLibraryVariableCollectionsAsync()`, `importVariableByKeyAsync(key)`
|
||||
|
||||
**Image (L11120):** `hash`, `getBytesAsync()`, `getSizeAsync()`
|
||||
|
||||
---
|
||||
|
||||
## All Symbols (flat — grep these against the .d.ts file)
|
||||
|
||||
To find any symbol: `grep -n "^interface Foo\|^type Foo\|^declare type Foo" plugin-api-standalone.d.ts`
|
||||
|
||||
```
|
||||
PluginAPI VariablesAPI AnnotationsAPI TeamLibraryAPI
|
||||
UIAPI UtilAPI ViewportAPI ClientStorageAPI
|
||||
ConstantsAPI CodegenAPI PaymentsAPI TextReviewAPI
|
||||
ParametersAPI TimerAPI BuzzAPI DevResourcesAPI
|
||||
|
||||
DocumentNode PageNode FrameNode GroupNode
|
||||
ComponentNode ComponentSetNode InstanceNode RectangleNode
|
||||
EllipseNode LineNode PolygonNode StarNode
|
||||
VectorNode TextNode TextPathNode BooleanOperationNode
|
||||
SliceNode SectionNode TableNode TableCellNode
|
||||
StickyNode ConnectorNode ShapeWithTextNode StampNode
|
||||
CodeBlockNode EmbedNode LinkUnfurlNode MediaNode
|
||||
WidgetNode SlideNode SlideRowNode SlideGridNode
|
||||
TransformGroupNode HighlightNode WashiTapeNode
|
||||
|
||||
BaseNodeMixin SceneNodeMixin ChildrenMixin LayoutMixin
|
||||
AutoLayoutMixin AutoLayoutChildrenMixin GridLayoutMixin GridChildrenMixin
|
||||
GeometryMixin MinimalFillsMixin MinimalStrokesMixin BlendMixin
|
||||
MinimalBlendMixin CornerMixin RectangleCornerMixin ExportMixin
|
||||
ReactionMixin PublishableMixin VariantMixin ComponentPropertiesMixin
|
||||
PluginDataMixin DevResourcesMixin DevStatusMixin StickableMixin
|
||||
ConstraintMixin DimensionAndPositionMixin AspectRatioLockMixin FramePrototypingMixin
|
||||
BaseFrameMixin DefaultFrameMixin DefaultShapeMixin OpaqueNodeMixin
|
||||
VectorLikeMixin ComplexStrokesMixin IndividualStrokesMixin ContainerMixin
|
||||
AnnotationsMixin MeasurementsMixin ExplicitVariableModesMixin
|
||||
|
||||
Variable VariableCollection VariableAlias ExtendedVariableCollection
|
||||
LibraryVariableCollection LibraryVariable
|
||||
VariableValue VariableResolvedDataType VariableDataType VariableScope
|
||||
CodeSyntaxPlatform VariableBindableNodeField VariableBindableTextField
|
||||
VariableBindablePaintField VariableBindableEffectField VariableBindableLayoutGridField
|
||||
|
||||
SolidPaint GradientPaint ImagePaint VideoPaint
|
||||
PatternPaint Paint ColorStop ImageFilters
|
||||
DropShadowEffect InnerShadowEffect BlurEffect NoiseEffect
|
||||
TextureEffect GlassEffect Effect
|
||||
LayoutGrid RowsColsLayoutGrid GridLayoutGrid
|
||||
|
||||
PaintStyle TextStyle EffectStyle GridStyle
|
||||
BaseStyle BaseStyleMixin StyleType
|
||||
|
||||
FontName Font LetterSpacing LineHeight
|
||||
TextCase TextDecoration TextDecorationStyle FontStyle
|
||||
OpenTypeFeature StyledTextSegment LeadingTrim
|
||||
|
||||
Vector Rect RGB RGBA
|
||||
Transform ArcData Constraints ConstraintType
|
||||
VectorPath VectorNetwork VectorVertex VectorSegment
|
||||
VectorRegion Guide BlendMode MaskType
|
||||
|
||||
Reaction Trigger Action Transition
|
||||
Easing Navigation OverflowDirection OverlayPositionType
|
||||
OverlayBackground PublishStatus
|
||||
|
||||
ArgFreeEventType RunEvent DropEvent DocumentChangeEvent
|
||||
NodeChangeEvent NodeChangeProperty StyleChangeEvent DocumentChange
|
||||
TextReviewEvent SlidesViewChangeEvent CanvasViewChangeEvent
|
||||
|
||||
ExportSettingsImage ExportSettingsSVG ExportSettingsPDF ExportSettingsREST
|
||||
ExportSettingsConstraints
|
||||
|
||||
User ActiveUser BaseUser Image
|
||||
Video VersionHistoryResult FindAllCriteria
|
||||
```
|
||||
@@ -0,0 +1,205 @@
|
||||
# Text Style API Patterns
|
||||
|
||||
> Part of the [use_figma skill](../SKILL.md). How to create, apply, and inspect text styles using the Plugin API.
|
||||
>
|
||||
> For design system context (when to create text styles, how they relate to tokens, headless limitations), see [wwds-text-styles](working-with-design-systems/wwds-text-styles.md).
|
||||
|
||||
## Contents
|
||||
|
||||
- Listing Text Styles
|
||||
- Creating a Text Style
|
||||
- Probing Font Styles
|
||||
- Creating a Type Ramp (Multi-Step)
|
||||
- Importing Library Text Styles
|
||||
- Applying Text Styles to Nodes
|
||||
|
||||
## Listing Text Styles
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Lists all local text styles with their key properties.
|
||||
*
|
||||
* @returns {Promise<Array<{id: string, name: string, key: string, fontSize: number, fontName: FontName, lineHeight: LineHeight, letterSpacing: LetterSpacing}>>}
|
||||
*/
|
||||
async function listTextStyles() {
|
||||
const styles = await figma.getLocalTextStylesAsync();
|
||||
return styles.map(s => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
key: s.key,
|
||||
fontSize: s.fontSize,
|
||||
fontName: s.fontName,
|
||||
lineHeight: s.lineHeight,
|
||||
letterSpacing: s.letterSpacing
|
||||
}));
|
||||
}
|
||||
```
|
||||
|
||||
Full runnable script:
|
||||
|
||||
```javascript
|
||||
const results = await listTextStyles();
|
||||
return results;
|
||||
```
|
||||
|
||||
## Creating a Text Style
|
||||
|
||||
Font **MUST** be loaded before setting `fontName`. `lineHeight` and `letterSpacing` must be `{value, unit}` objects — bare numbers throw.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a text style with all typographic properties set.
|
||||
* Font MUST be loaded before calling.
|
||||
*
|
||||
* @param {string} name - Slash-delimited name, e.g. "body/base"
|
||||
* @param {{ family: string, style: string }} fontName
|
||||
* @param {number} fontSize - In pixels
|
||||
* @param {{ value: number, unit: 'PIXELS' | 'PERCENT' } | { unit: 'AUTO' }} lineHeight
|
||||
* @param {{ value: number, unit: 'PIXELS' | 'PERCENT' }} [letterSpacing]
|
||||
* @param {string} [description] - e.g. the CSS variable name "CSS: var(--font-body-base)"
|
||||
* @returns {TextStyle}
|
||||
*/
|
||||
function createTextStyleFull(name, fontName, fontSize, lineHeight, letterSpacing, description) {
|
||||
const style = figma.createTextStyle();
|
||||
style.name = name;
|
||||
style.fontName = fontName;
|
||||
style.fontSize = fontSize;
|
||||
style.lineHeight = lineHeight; // { unit: 'AUTO' } | { value, unit: 'PIXELS'|'PERCENT' }
|
||||
if (letterSpacing) style.letterSpacing = letterSpacing;
|
||||
if (description) style.description = description;
|
||||
return style;
|
||||
}
|
||||
```
|
||||
|
||||
## Probing Font Styles
|
||||
|
||||
Font style names vary per provider and per file (`"SemiBold"` vs `"Semi Bold"`). Always probe before hardcoding:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Probes available font styles for a given family.
|
||||
* Useful when font style names are unknown (e.g. "SemiBold" vs "Semi Bold").
|
||||
*
|
||||
* @param {string} family - Font family name, e.g. "Inter"
|
||||
* @param {string[]} stylesToTest - Candidate style names to probe
|
||||
* @returns {Promise<string[]>} - Style names that loaded successfully
|
||||
*/
|
||||
async function probeAvailableFontStyles(family, stylesToTest) {
|
||||
const available = [];
|
||||
for (const style of stylesToTest) {
|
||||
try {
|
||||
await figma.loadFontAsync({ family, style });
|
||||
available.push(style);
|
||||
} catch (_) {}
|
||||
}
|
||||
return available;
|
||||
}
|
||||
```
|
||||
|
||||
## Creating a Type Ramp (Multi-Step)
|
||||
|
||||
Handles font loading, deduplication, and idempotency. Each entry: `[name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]`.
|
||||
|
||||
**HEADLESS NOTE:** `setBoundVariable` on `TextStyle` is not supported in `use_figma`. This function sets raw values. To bind variables, do it interactively in Figma after creation.
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Creates a full type ramp from a token definition array.
|
||||
* Handles font loading, deduplication, and idempotency.
|
||||
*
|
||||
* Each entry: [name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]
|
||||
* - lineHeight: { unit: 'AUTO' } or { value: number, unit: 'PIXELS' | 'PERCENT' }
|
||||
*
|
||||
* @param {Array} defs - Array of [name, fontFamily, fontStyle, fontSize, lineHeight, cssVar] tuples
|
||||
* @returns {Promise<{ created: string[], skipped: string[] }>}
|
||||
*/
|
||||
async function createTypeRamp(defs) {
|
||||
const uniqueFonts = new Set();
|
||||
for (const [, family, style] of defs) {
|
||||
uniqueFonts.add(JSON.stringify({ family, style }));
|
||||
}
|
||||
await Promise.all(
|
||||
[...uniqueFonts].map(f => figma.loadFontAsync(JSON.parse(f)))
|
||||
);
|
||||
|
||||
const existing = new Set(
|
||||
(await figma.getLocalTextStylesAsync()).map(s => s.name)
|
||||
);
|
||||
|
||||
const created = [];
|
||||
const skipped = [];
|
||||
|
||||
for (const [name, family, style, fontSize, lineHeight, cssVar] of defs) {
|
||||
if (existing.has(name)) {
|
||||
skipped.push(name);
|
||||
continue;
|
||||
}
|
||||
const ts = figma.createTextStyle();
|
||||
ts.name = name;
|
||||
ts.fontName = { family, style };
|
||||
ts.fontSize = fontSize;
|
||||
ts.lineHeight = lineHeight ?? { unit: 'AUTO' };
|
||||
if (cssVar) ts.description = `CSS: var(${cssVar})`;
|
||||
created.push(name);
|
||||
}
|
||||
|
||||
return { created, skipped };
|
||||
}
|
||||
```
|
||||
|
||||
Full runnable script:
|
||||
|
||||
```javascript
|
||||
const defs = [
|
||||
['heading/xl', 'Inter', 'Bold', 48, { unit: 'PIXELS', value: 56 }, '--font-heading-xl'],
|
||||
['heading/lg', 'Inter', 'Bold', 36, { unit: 'PIXELS', value: 44 }, '--font-heading-lg'],
|
||||
['body/base', 'Inter', 'Regular', 16, { unit: 'AUTO' }, '--font-body-base'],
|
||||
['body/sm', 'Inter', 'Regular', 14, { unit: 'AUTO' }, '--font-body-sm'],
|
||||
['code/base', 'Roboto Mono', 'Regular', 14, { unit: 'AUTO' }, '--font-code-base'],
|
||||
];
|
||||
const result = await createTypeRamp(defs);
|
||||
return result;
|
||||
```
|
||||
|
||||
## Importing Library Text Styles
|
||||
|
||||
For text styles from **team libraries**, use `importStyleByKeyAsync`:
|
||||
|
||||
```javascript
|
||||
// Import a library text style by key
|
||||
const headingStyle = await figma.importStyleByKeyAsync("TEXT_STYLE_KEY");
|
||||
// Apply to a text node
|
||||
await textNode.setTextStyleIdAsync(headingStyle.id);
|
||||
```
|
||||
|
||||
`search_design_system` with `includeStyles: true` returns style keys you can import this way. Prefer importing library styles over creating new ones.
|
||||
|
||||
## Applying Text Styles to Nodes
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Applies a text style to all TEXT nodes on the current page that match a given name pattern.
|
||||
*
|
||||
* @param {string} styleId - The ID of a TextStyle.
|
||||
* @param {string} nodeNamePattern - Substring match against node names.
|
||||
* @returns {Promise<number>} - Number of nodes the style was applied to.
|
||||
*/
|
||||
async function applyTextStyleToMatchingNodes(styleId, nodeNamePattern) {
|
||||
const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] });
|
||||
let applied = 0;
|
||||
for (const node of textNodes) {
|
||||
if (node.name.includes(nodeNamePattern)) {
|
||||
await node.setTextStyleIdAsync(styleId);
|
||||
applied++;
|
||||
}
|
||||
}
|
||||
return applied;
|
||||
}
|
||||
```
|
||||
|
||||
Full runnable script:
|
||||
|
||||
```javascript
|
||||
const applied = await applyTextStyleToMatchingNodes('STYLE_ID', 'Heading');
|
||||
return { applied };
|
||||
```
|
||||
@@ -0,0 +1,82 @@
|
||||
# Validation Workflow & Error Recovery
|
||||
|
||||
> Part of the [use_figma skill](../SKILL.md). How to debug, validate, and recover from errors.
|
||||
|
||||
## Contents
|
||||
|
||||
- `get_metadata` vs `get_screenshot`
|
||||
- Error Recovery After Failed `use_figma`
|
||||
- Recommended Workflow
|
||||
|
||||
|
||||
## `get_metadata` vs `get_screenshot`
|
||||
|
||||
After each `use_figma` call, validate results using the right tool for the job. Do NOT reach for `get_screenshot` every time — it is expensive and should be reserved for visual checks.
|
||||
|
||||
### `get_metadata` — Use for intermediate validation (preferred)
|
||||
|
||||
`get_metadata` returns an XML tree of node IDs, types, names, positions, and sizes. Use it to confirm:
|
||||
|
||||
- **Structure & hierarchy**: correct parent-child relationships, component nesting, section contents
|
||||
- **Node counts**: expected number of variants created, children present
|
||||
- **Naming**: variant property names follow the `property=value` convention
|
||||
- **Positioning & alignment**: x/y coordinates, width/height values match expectations
|
||||
- **Layout properties**: auto-layout direction, sizing mode, padding, spacing
|
||||
- **Component set membership**: all expected variants are inside the ComponentSet
|
||||
|
||||
```
|
||||
Example: After creating a ComponentSet with 120 variants, call get_metadata on the
|
||||
ComponentSet node to verify all 120 children exist with correct names, sizes, and positions
|
||||
— without waiting for a full render.
|
||||
```
|
||||
|
||||
**When to use `get_metadata`:**
|
||||
- After creating/modifying nodes — to verify structure, counts, and names
|
||||
- After layout operations — to verify positions and dimensions
|
||||
- After combining variants — to confirm all components are in the ComponentSet
|
||||
- After binding variables — to verify node properties (use use_figma to read bound variables if needed)
|
||||
- Between multi-step workflows — to confirm step N succeeded before starting step N+1
|
||||
|
||||
### `get_screenshot` — Use after each major creation milestone
|
||||
|
||||
`get_screenshot` renders a pixel-accurate image. It is the only way to verify visual correctness (colors, typography rendering, effects, variable mode resolution). It is slower and produces large responses, so don't call it after every single `use_figma` — but do call it after each major milestone to catch visual problems early.
|
||||
|
||||
**When to use `get_screenshot`:**
|
||||
- **After creating a component set** — verify variants look correct, grid is readable, nothing is collapsed or overlapping
|
||||
- **After composing a layout** — verify overall structure and spacing
|
||||
- **After binding variables/modes** — verify colors and tokens resolved correctly
|
||||
- **After any fix or recovery** — verify the fix didn't introduce new visual issues
|
||||
- **Before reporting results to the user** — final visual proof
|
||||
|
||||
**What to look for in screenshots** — these are the most commonly missed issues:
|
||||
- **Cropped/clipped text** — line heights or frame sizing cutting off descenders, ascenders, or entire lines
|
||||
- **Overlapping content** — elements stacking on top of each other due to incorrect sizing or missing auto-layout
|
||||
- **Placeholder text** still showing ("Title", "Heading", "Button") instead of actual content
|
||||
|
||||
## Error Recovery After Failed `use_figma`
|
||||
|
||||
**`use_figma` is atomic — failed scripts do not execute.** If a script errors, no changes are made to the file. The file remains in exactly the same state as before the call. There are no partial nodes, no orphaned elements, and retrying after a fix is safe.
|
||||
|
||||
**Recovery steps when `use_figma` returns an error:**
|
||||
1. **STOP — do NOT immediately fix the code and retry.** Read the error message carefully first.
|
||||
2. **Understand the error.** Most errors are caused by wrong API usage, missing font loads, invalid property values, or referencing nodes that don't exist.
|
||||
3. **If the error is unclear**, call `get_metadata` or `get_screenshot` to understand the current file state and confirm nothing has changed.
|
||||
4. **Fix the script** based on the error message.
|
||||
5. **Retry** the corrected script.
|
||||
|
||||
## Recommended Workflow
|
||||
|
||||
```
|
||||
1. use_figma → Create/modify nodes
|
||||
2. get_metadata → Verify structure, counts, names, positions (fast, cheap)
|
||||
3. use_figma → Fix any structural issues found
|
||||
4. get_metadata → Re-verify fixes
|
||||
5. ... repeat as needed ...
|
||||
6. get_screenshot → Visual check after each major milestone
|
||||
|
||||
⚠️ ON ERROR at any step:
|
||||
a. Read the error message carefully
|
||||
b. get_metadata / get_screenshot → If the error is unclear, inspect file state
|
||||
c. Fix the script based on the error
|
||||
d. Retry the corrected script (safe — failed scripts don't modify the file)
|
||||
```
|
||||
@@ -0,0 +1,375 @@
|
||||
# Variable & Token API Patterns
|
||||
|
||||
> Part of the [use_figma skill](../SKILL.md). How to correctly create, bind, scope, and alias variables using the Plugin API.
|
||||
>
|
||||
> For design system context (aliasing strategy, mode decisions, code syntax philosophy, grouping conventions), see [wwds-variables](working-with-design-systems/wwds-variables.md).
|
||||
|
||||
## Contents
|
||||
|
||||
- Creating Variable Collections and Modes
|
||||
- Creating Variables (All Types)
|
||||
- Binding Variables to Node Properties
|
||||
- Variable Scopes: What They Are and How to Set Them
|
||||
- Variable Aliasing (VARIABLE_ALIAS)
|
||||
- Code Syntax (setVariableCodeSyntax)
|
||||
- Importing Library Variables
|
||||
- Discovering Existing Variables in the File
|
||||
- Effect Styles (For Shadows)
|
||||
|
||||
|
||||
## Creating Variable Collections and Modes
|
||||
|
||||
```javascript
|
||||
const collection = figma.variables.createVariableCollection("MyCollection");
|
||||
|
||||
// A new collection starts with 1 mode named "Mode 1" — always rename it
|
||||
collection.renameMode(collection.modes[0].modeId, "Light");
|
||||
|
||||
// Add additional modes (returns the new modeId)
|
||||
const darkModeId = collection.addMode("Dark");
|
||||
const lightModeId = collection.modes[0].modeId;
|
||||
```
|
||||
|
||||
**Mode limits are plan-dependent:** Free = 1 mode, Professional = up to 4, Organization/Enterprise = 40+. If you need many modes, split across multiple collections.
|
||||
|
||||
## Creating Variables (All Types)
|
||||
|
||||
`figma.variables.createVariable(name, collection, resolvedType)` — the second argument accepts a collection object or ID string (object preferred).
|
||||
|
||||
```javascript
|
||||
// COLOR — values use {r, g, b, a} (all 0–1 range, includes alpha)
|
||||
const colorVar = figma.variables.createVariable("my-color", collection, "COLOR");
|
||||
colorVar.setValueForMode(modeId, { r: 0.2, g: 0.36, b: 0.96, a: 1 });
|
||||
|
||||
// FLOAT — for spacing, radii, sizing, numeric values
|
||||
const floatVar = figma.variables.createVariable("my-spacing", collection, "FLOAT");
|
||||
floatVar.setValueForMode(modeId, 16);
|
||||
|
||||
// STRING — for font families, font style names, any text value
|
||||
const stringVar = figma.variables.createVariable("my-font", collection, "STRING");
|
||||
stringVar.setValueForMode(modeId, "Inter");
|
||||
|
||||
// BOOLEAN
|
||||
const boolVar = figma.variables.createVariable("my-flag", collection, "BOOLEAN");
|
||||
boolVar.setValueForMode(modeId, true);
|
||||
```
|
||||
|
||||
**Note:** Paint colors use `{r, g, b}` (no alpha), but COLOR variable values use `{r, g, b, a}` (with alpha). Don't mix them up.
|
||||
|
||||
## Binding Variables to Node Properties
|
||||
|
||||
### Color Bindings (Fills, Strokes)
|
||||
|
||||
`setBoundVariableForPaint` returns a **NEW paint** — you must capture the return value:
|
||||
|
||||
```javascript
|
||||
// Create a base paint, bind the variable, assign the result
|
||||
const basePaint = { type: 'SOLID', color: { r: 0, g: 0, b: 0 } };
|
||||
const boundPaint = figma.variables.setBoundVariableForPaint(basePaint, "color", colorVar);
|
||||
node.fills = [boundPaint];
|
||||
|
||||
// Only SOLID paints support color variable binding — gradients/images will throw
|
||||
```
|
||||
|
||||
### Numeric Bindings (Spacing, Radii, Sizing)
|
||||
|
||||
`setBoundVariable` binds FLOAT/STRING/BOOLEAN variables to node properties:
|
||||
|
||||
```javascript
|
||||
// Padding
|
||||
node.setBoundVariable("paddingTop", spacingVar);
|
||||
node.setBoundVariable("paddingBottom", spacingVar);
|
||||
node.setBoundVariable("paddingLeft", spacingVar);
|
||||
node.setBoundVariable("paddingRight", spacingVar);
|
||||
|
||||
// Gap
|
||||
node.setBoundVariable("itemSpacing", gapVar);
|
||||
node.setBoundVariable("counterAxisSpacing", gapVar);
|
||||
|
||||
// Corner radius — use individual corners, NOT cornerRadius
|
||||
node.setBoundVariable("topLeftRadius", radiusVar);
|
||||
node.setBoundVariable("topRightRadius", radiusVar);
|
||||
node.setBoundVariable("bottomLeftRadius", radiusVar);
|
||||
node.setBoundVariable("bottomRightRadius", radiusVar);
|
||||
|
||||
// Size
|
||||
node.setBoundVariable("width", sizeVar);
|
||||
node.setBoundVariable("height", sizeVar);
|
||||
node.setBoundVariable("minWidth", sizeVar);
|
||||
node.setBoundVariable("maxWidth", sizeVar);
|
||||
|
||||
// Other
|
||||
node.setBoundVariable("opacity", opacityVar);
|
||||
node.setBoundVariable("strokeWeight", strokeVar);
|
||||
```
|
||||
|
||||
**Not bindable via setBoundVariable:** `fontSize`, `fontWeight`, `lineHeight` — set these directly on text nodes.
|
||||
|
||||
### Effect Bindings
|
||||
|
||||
```javascript
|
||||
const effectCopy = JSON.parse(JSON.stringify(node.effects[0]));
|
||||
const newEffect = figma.variables.setBoundVariableForEffect(effectCopy, "color", colorVar);
|
||||
// ⚠️ Returns a NEW effect — must capture return value!
|
||||
node.effects = [newEffect];
|
||||
// Valid fields: "color" (COLOR), "radius" | "spread" | "offsetX" | "offsetY" (FLOAT)
|
||||
```
|
||||
|
||||
### Applying a Mode to a Frame
|
||||
|
||||
```javascript
|
||||
// All bound children of this frame will resolve to the specified mode's values
|
||||
frame.setExplicitVariableModeForCollection(collection, modeId);
|
||||
```
|
||||
|
||||
Without this, all nodes use the collection's default (first) mode.
|
||||
|
||||
## Variable Scopes: What They Are and How to Set Them
|
||||
|
||||
`variable.scopes` controls which Figma property pickers show the variable. The default is `["ALL_SCOPES"]` which shows it everywhere — this is almost never what you want.
|
||||
|
||||
```javascript
|
||||
variable.scopes = ["FRAME_FILL", "SHAPE_FILL"]; // only fill pickers
|
||||
variable.scopes = ["TEXT_FILL"]; // only text color picker
|
||||
variable.scopes = ["GAP"]; // only gap/spacing pickers
|
||||
variable.scopes = ["CORNER_RADIUS"]; // only radius pickers
|
||||
variable.scopes = []; // hidden from all pickers
|
||||
```
|
||||
|
||||
**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`
|
||||
|
||||
**Always set scopes explicitly** — `ALL_SCOPES` is the default but almost never what you want. For a comprehensive scope-to-use-case mapping table, see [token-creation.md § Variable Scopes — Complete Reference Table](../../figma-generate-library/references/token-creation.md).
|
||||
|
||||
**Always check the existing file's scope patterns before creating variables** — match whatever convention is already in use. See "Discovering Existing Variables" below.
|
||||
|
||||
## Variable Aliasing (VARIABLE_ALIAS)
|
||||
|
||||
A variable's value can reference another variable via alias. This is how semantic tokens reference primitive tokens:
|
||||
|
||||
```javascript
|
||||
// Set a variable's value as an alias to another variable
|
||||
semanticVar.setValueForMode(modeId, {
|
||||
type: 'VARIABLE_ALIAS',
|
||||
id: primitiveVar.id
|
||||
});
|
||||
```
|
||||
|
||||
When the primitive changes, the semantic variable updates automatically across all modes.
|
||||
|
||||
## Code Syntax (setVariableCodeSyntax)
|
||||
|
||||
Links a Figma variable back to its code counterpart. Call once per platform:
|
||||
|
||||
```javascript
|
||||
variable.setVariableCodeSyntax('WEB', 'var(--color-bg-default)');
|
||||
variable.setVariableCodeSyntax('ANDROID', 'colorBgDefault');
|
||||
variable.setVariableCodeSyntax('iOS', 'Color.bgDefault');
|
||||
|
||||
// Read back: variable.codeSyntax → { WEB: '...', ANDROID: '...', iOS: '...' }
|
||||
```
|
||||
|
||||
**When deriving CSS names from Figma names**, replace both slashes AND spaces with hyphens:
|
||||
|
||||
```javascript
|
||||
// WRONG — leaves spaces in CSS variable name
|
||||
`var(--${figmaName.replace(/\//g, '-').toLowerCase()})`
|
||||
|
||||
// CORRECT — replace all whitespace and slashes
|
||||
`var(--${figmaName.replace(/[\s\/]+/g, '-').toLowerCase()})`
|
||||
|
||||
// BEST — use the original CSS variable name from the source, not a derived one
|
||||
`var(${token.cssVar})`
|
||||
```
|
||||
|
||||
## Importing Library Variables
|
||||
|
||||
For variables from **team libraries** (not the current file), use `importVariableByKeyAsync`:
|
||||
|
||||
```javascript
|
||||
// Import a single variable by its key
|
||||
const colorVar = await figma.variables.importVariableByKeyAsync("VARIABLE_KEY");
|
||||
// Now use it like any local variable
|
||||
const paint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', colorVar
|
||||
);
|
||||
node.fills = [paint];
|
||||
```
|
||||
|
||||
To discover available library variable collections and their variables:
|
||||
|
||||
```javascript
|
||||
// List all available library variable collections
|
||||
const libCollections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();
|
||||
// Each has: name, key, libraryName
|
||||
|
||||
// Get variables in a specific library collection
|
||||
const libVars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(libCollections[0].key);
|
||||
// Each has: name, key, resolvedType
|
||||
// Import the ones you need:
|
||||
const imported = await figma.variables.importVariableByKeyAsync(libVars[0].key);
|
||||
```
|
||||
|
||||
**When to import vs. use local:** If `variable.remote === true`, it's from a library — you can reference it directly if already imported, or import by key. If `remote === false`, it's local to the file — use `getVariableByIdAsync` directly.
|
||||
|
||||
## Discovering Existing Variables in the File
|
||||
|
||||
**Always inspect the file's existing variables before creating new ones.** Different files use different naming conventions, scope patterns, and collection structures. Match what's already there.
|
||||
|
||||
### List collections with mode info
|
||||
|
||||
```javascript
|
||||
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 => ({ name: m.name, id: m.modeId }))
|
||||
}));
|
||||
return results;
|
||||
```
|
||||
|
||||
### Inspect scope patterns used in existing variables
|
||||
|
||||
```javascript
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const scopeGroups = {};
|
||||
for (const c of collections) {
|
||||
for (const id of c.variableIds) {
|
||||
const v = await figma.variables.getVariableByIdAsync(id);
|
||||
const key = JSON.stringify(v.scopes);
|
||||
if (!scopeGroups[key]) scopeGroups[key] = [];
|
||||
scopeGroups[key].push(v.name);
|
||||
}
|
||||
}
|
||||
return scopeGroups;
|
||||
```
|
||||
|
||||
### Build a name→variable lookup for reuse
|
||||
|
||||
```javascript
|
||||
const varByName = {};
|
||||
for (const v of await figma.variables.getLocalVariablesAsync()) {
|
||||
varByName[v.name] = v;
|
||||
}
|
||||
|
||||
// Bind to existing variable by name — no hex values needed
|
||||
function bindFill(node, varName) {
|
||||
const v = varByName[varName];
|
||||
if (!v) throw new Error(`Variable not found: ${varName}`);
|
||||
const paint = figma.variables.setBoundVariableForPaint(
|
||||
{ type: 'SOLID', color: { r: 0, g: 0, b: 0 } }, 'color', v
|
||||
);
|
||||
node.fills = [paint];
|
||||
}
|
||||
```
|
||||
|
||||
**Only create new variables for tokens that have no match in the file.** After building the lookup, compare against the needed tokens and create variables only for the delta.
|
||||
|
||||
## Listing Collections with Full Variable Details
|
||||
|
||||
The async API returns richer data including code syntax and scopes per variable:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Lists all local variable collections defined in the current Figma file,
|
||||
* including metadata for their modes and variables.
|
||||
*
|
||||
* @returns {Promise<Array<{
|
||||
* name: string,
|
||||
* id: string,
|
||||
* modes: Array<[name: string, modeId: string]>,
|
||||
* variables: Array<[name: string, id: string, codeSyntax: object, scopes: string[]]>
|
||||
* }>>}
|
||||
*/
|
||||
async function listVariableCollectionsAndVariables() {
|
||||
const collections = await figma.variables.getLocalVariableCollectionsAsync();
|
||||
const results = [];
|
||||
for (const collection of collections) {
|
||||
const vars = [];
|
||||
for (const id of collection.variableIds) {
|
||||
const v = await figma.variables.getVariableByIdAsync(id);
|
||||
vars.push([v.name, v.id, v.codeSyntax, v.scopes]);
|
||||
}
|
||||
results.push({
|
||||
name: collection.name,
|
||||
id: collection.id,
|
||||
modes: collection.modes.map(m => [m.name, m.modeId]),
|
||||
variables: vars
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
```
|
||||
|
||||
Full runnable script:
|
||||
|
||||
```javascript
|
||||
const results = await listVariableCollectionsAndVariables();
|
||||
return results;
|
||||
```
|
||||
|
||||
## Setting and Removing Code Syntax
|
||||
|
||||
Must be executed in the file the variable is defined in:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* Set the code syntax for a variable for a specific platform.
|
||||
*
|
||||
* @param {string} variableId
|
||||
* @param {'WEB'|'ANDROID'|'iOS'} platform
|
||||
* @param {string} syntax
|
||||
*/
|
||||
async function setVariableCodeSyntax(variableId, platform, syntax) {
|
||||
const variable = await figma.variables.getVariableByIdAsync(variableId);
|
||||
variable.setVariableCodeSyntax(platform, syntax);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove code syntax for a variable for one or more platforms.
|
||||
*
|
||||
* @param {string} variableId
|
||||
* @param {Array<'WEB'|'ANDROID'|'iOS'>} platforms — defaults to all three
|
||||
*/
|
||||
async function removeVariableCodeSyntax(variableId, platforms = ["WEB", "ANDROID", "iOS"]) {
|
||||
const variable = await figma.variables.getVariableByIdAsync(variableId);
|
||||
for (const platform of platforms) {
|
||||
variable.removeVariableCodeSyntax(platform);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value for a variable in a specific mode.
|
||||
* For aliases, value must be: { type: 'VARIABLE_ALIAS', id: '<variableId>' }
|
||||
*
|
||||
* @param {string} variableId
|
||||
* @param {string} modeId
|
||||
* @param {string|number|boolean|RGB|RGBA|{type: 'VARIABLE_ALIAS', id: string}} value
|
||||
*/
|
||||
async function setVariableValueForMode(variableId, modeId, value) {
|
||||
const variable = await figma.variables.getVariableByIdAsync(variableId);
|
||||
variable.setValueForMode(modeId, value);
|
||||
}
|
||||
```
|
||||
|
||||
## Effect Styles (For Shadows)
|
||||
|
||||
Shadows can't be stored as variables. Use effect styles. For comprehensive patterns, see [effect-style-patterns.md](effect-style-patterns.md).
|
||||
|
||||
```javascript
|
||||
const shadow = figma.createEffectStyle();
|
||||
shadow.name = "Shadow/Subtle";
|
||||
shadow.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 = shadow.id;
|
||||
```
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Working with design systems: Creating Components
|
||||
|
||||
When creating Figma components, you need to start by understanding the source and its intent.
|
||||
|
||||
If the user is asking you to create a component based on a design or specification, you need to understand the property model before you build anything. What variants are needed? What text, boolean, or instance swap properties exist? Getting the structure right upfront matters because restructuring a component after instances exist is destructive.
|
||||
|
||||
If you are given a code component as reference (React props, tokens, etc.), your goal is to reflect the property surface as closely as makes sense in Figma's model. Not all code properties translate directly — hover and focus states are not props in web code, but they are variants in Figma. Understand those gaps and make deliberate decisions about how to represent them.
|
||||
|
||||
Variants are the most important thing to get right. Each combination of variant values creates a node on the canvas. Redundant combinations still exist as explicit nodes — there is no way to conditionally exclude them. Define only the axes you actually need.
|
||||
|
||||
Non-variant properties (text, boolean, instance swap) should be added after the variant structure is established. These are defined at the component/component set level and referenced by descendant nodes via `componentPropertyReferences`. Always connect them — a property that isn't wired to a descendant is invisible to users of the component.
|
||||
|
||||
If the user asks you to make architectural decisions, lean toward fewer variants and more boolean/text properties where possible. Variants multiply combinatorially; the other property types do not. An optional slot property in code might be a combination of instance swap and boolean visibility.
|
||||
|
||||
When naming properties, casing is less important since translation layers like Code Connect can do the mapping to represent the code form. Feel free to take a sentence or capitalized case approach for better readability in Figma.
|
||||
|
||||
Keep in mind that components often need to be published and connected to Code Connect for the full design-to-code workflow to work. Creating the component is only one part of the system.
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# Working with design systems: Using Components
|
||||
|
||||
When using Figma components, you need to start by understanding the state of the source and the state of Figma.
|
||||
|
||||
For the source, you need to know what component is being referenced. This could come from a component key, a node ID, a name, or a Code Connect mapping. If you have a component key from a design system library, prefer `importComponentByKeyAsync` over finding by name, since names are not unique. If you only have a name, search the page or use `search_design_system` to find the right match.
|
||||
|
||||
For Figma, you need to know whether the component is local or in a library. Local components can be accessed directly by node ID. Published library components must be imported first — `importComponentByKeyAsync` or `importComponentSetByKeyAsync` — before an instance can be created.
|
||||
|
||||
Before setting properties on an instance, read `componentPropertyDefinitions` from the main component first. Property names are not simple strings — TEXT, BOOLEAN, and INSTANCE_SWAP properties have a `#uid` suffix (e.g. `"Label#1234"`). Only VARIANT properties are plain names (e.g. `"Size"`). Using the wrong key in `setProperties` will silently do nothing.
|
||||
|
||||
A component might have multiple text properties, which are not possible to derive from text node layer names. Look to the properties to help you understand what values to set, rather than thinking of setting text node characters directly.
|
||||
|
||||
When you need to set a nested instance swap (e.g. an icon property), you need the component key of the swap target, not just its name. Import the target component and pass its node ID.
|
||||
|
||||
Be aware that instances inside other instances are nested and changes made to a nested instance may be treated as overrides. If the intent is to change the default appearance, you need to modify the main component, not the instance.
|
||||
|
||||
When selecting which variant to use, read the `componentProperties` on the instance to see the current state, and `componentPropertyDefinitions` on the main component to see all available options.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# Components
|
||||
|
||||
Components overlap a lot with the idea of components in a codebase, but with some gaps and other Figma-specific use cases. Components in Figma can be reusable entities that do not have a comparable library pattern, or they can be published and distributed in a library that is aligned to a code forms.
|
||||
|
||||
Properties can vary from code in different ways, but alignment to code can still happen without a direct relationship. For example, an interactive pattern in code (like a button) can have many states. A lot of these states (active, focused etc) would be expressed in Figma as variants, which is a concept more closely aligned to properties in a code library. In the case of web this is confusing since hover is not a prop, it is a pseudo selector. At the same time, a color variant might be perfectly aligned between design and code (a property in both places). These discrepancies are accounted for in translation with Figma's Code Connect (deterministic context mapping), but in the case of these tools, must be understood to be properly used.
|
||||
|
||||
Figma has four property types, which can be inspected in the component definition's `componentPropertyDefinitions`. To fully understand the component, its descendants must be traversed. Property types include:
|
||||
|
||||
- Variant
|
||||
- This is reflected as permutations of the component in a Component Set on the canvas. Each variant is explicitly visualized, including an redundant permutations ("Small + Primary + Disabled" may look the same as "Small Secondary Sisabled"). These permutations create different variants implicitly in Figma and it is handled through layer naming (`Variant=Primary,Size=Small,State=Disabled`).
|
||||
- Text/String
|
||||
- Text properties are stored on the component parent, but can be mapped to Text node descendants.
|
||||
- `node.componentPropertyReferences.characters` on a descendant text node are how you determine where the text property is referenced (can be multiple, though unlikely).
|
||||
- Boolean
|
||||
- Boolean properties are stored on the component parent, but can be mapped to any node descendant that can have its visibility toggled.
|
||||
- `node.componentPropertyReferences.visible` on a descendant node are how you determine where the boolean property is referenced.
|
||||
- Instance Swap
|
||||
- Instance swap properties are stored on the component parent, but can be mapped to Instance node descendants.
|
||||
- `node.componentPropertyReferences.mainComponent` on a descendant instance node are how you determine where the instance property is referenced. A classic example of this is an icon property.
|
||||
|
||||
## Descriptions
|
||||
|
||||
Components, component sets, and instances all inherit `PublishableMixin`, which includes a writable `description` string. Setting a description is important for any component intended to be used by others — it appears in Figma's dev mode and component panel, and is surfaced in MCP context when reading component metadata.
|
||||
|
||||
Descriptions should explain the component's intent and any non-obvious usage constraints. They are not a substitute for Code Connect annotations, but they are always visible without any tooling setup.
|
||||
|
||||
```js
|
||||
component.description =
|
||||
"Primary action button. Use for the single most important action on a page.";
|
||||
```
|
||||
|
||||
Variant components (children of a component set) also have a `description` field, but in practice the component set description is what users see. Set it on the component set, not on individual variant nodes.
|
||||
|
||||
To read descriptions when auditing:
|
||||
|
||||
```js
|
||||
// Get all component sets and their descriptions
|
||||
figma.root
|
||||
.findAllWithCriteria({ types: ["COMPONENT_SET"] })
|
||||
.map((n) => ({ name: n.name, description: n.description }));
|
||||
```
|
||||
|
||||
## Usage guidelines
|
||||
|
||||
- [Creating components](wwds-components--creating.md): What you must consider when creating new components.
|
||||
- [Using components](wwds-components--using.md): What you must consider when trying to use the right components.
|
||||
|
||||
## Code patterns
|
||||
|
||||
For runnable code examples (creating, importing, discovering, inspecting components), see [component-patterns.md](../component-patterns.md).
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# Working with design systems: Effect Styles
|
||||
|
||||
Effect styles in Figma are named, reusable definitions of one or more visual effects — drop shadows, inner shadows, and blurs. They are the closest equivalent to a shadow or elevation token in a design system.
|
||||
|
||||
Effect styles are distinct from variables. There is no single variable type that represents a shadow. However, individual numeric and color properties within an effect _can_ be bound to variables, allowing shadow values to participate in a token system.
|
||||
|
||||
## Model
|
||||
|
||||
An `EffectStyle` has one core writable property beyond the base style fields:
|
||||
|
||||
| Property | Type | Notes |
|
||||
| ------------- | ----------------------- | ----------------------------------------------------- |
|
||||
| `name` | `string` | Slash-delimited for grouping (e.g. `"Elevation/200"`) |
|
||||
| `effects` | `ReadonlyArray<Effect>` | **Read-only array** — clone, modify, reassign |
|
||||
| `description` | `string` | Inherited from `BaseStyleMixin` |
|
||||
|
||||
### Effect types
|
||||
|
||||
An `Effect` is a discriminated union. The most common types:
|
||||
|
||||
| `type` | Key properties |
|
||||
| ----------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `DROP_SHADOW` | `color: RGBA`, `offset: Vector`, `radius: number`, `spread: number`, `visible: boolean`, `blendMode` |
|
||||
| `INNER_SHADOW` | Same as `DROP_SHADOW` |
|
||||
| `LAYER_BLUR` | `radius: number`, `visible: boolean` |
|
||||
| `BACKGROUND_BLUR` | `radius: number`, `visible: boolean` |
|
||||
|
||||
All colors are in 0–1 range (`RGBA`), not 0–255.
|
||||
|
||||
### Variable bindings on effects
|
||||
|
||||
Effect properties that can be bound to variables (via `setBoundVariableForEffect(effect, field, variable)` on a node, or inline when constructing):
|
||||
|
||||
`color`, `radius`, `spread`, `offsetX`, `offsetY`
|
||||
|
||||
Note: `setBoundVariableForEffect` returns a **new** effect object — you must capture it and reassign the `effects` array.
|
||||
|
||||
### Applying an effect style to a node
|
||||
|
||||
Assign the style's `id` to the node's `effectStyleId`. The node's `effects` property will then reflect the style's values.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **`effects` is read-only**: You cannot mutate the array in place. Clone it, modify the clone, then reassign: `style.effects = [...style.effects, newEffect]`.
|
||||
- **Effects stack in order**: The order of effects in the array matters visually. Drop shadows render bottom-to-top.
|
||||
- **Colors are RGBA 0–1**: `{ r: 0, g: 0, b: 0, a: 0.15 }` — not hex, not 0–255.
|
||||
- **`getLocalEffectStyles()` is deprecated**: Always use `getLocalEffectStylesAsync()`.
|
||||
- **Styles are not automatically applied**: Creating an `EffectStyle` has no effect on any node until you assign its ID to a node.
|
||||
|
||||
## Code patterns
|
||||
|
||||
For runnable code examples (listing, creating, applying effect styles), see [effect-style-patterns.md](../effect-style-patterns.md).
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
# Working with design systems: Text Styles
|
||||
|
||||
Text styles in Figma are named, reusable typography definitions. They are the closest equivalent to a type ramp in a design token library. A text style bundles font family, size, weight, line height, letter spacing, and other typographic properties into a single named entity that can be applied to text nodes.
|
||||
|
||||
Text styles are distinct from variables. You cannot put typography into a single variable — there is no composite variable type. However, individual properties on a text style _can_ be bound to variables (e.g. binding `fontSize` to a size variable, or `fontFamily` to a string variable), which allows the style to participate in a token system.
|
||||
|
||||
## Model
|
||||
|
||||
A `TextStyle` has the following writable properties:
|
||||
|
||||
| Property | Type | Notes |
|
||||
| ------------------ | ---------------- | ---------------------------------------------------------------------------- |
|
||||
| `name` | `string` | Slash-delimited for grouping (e.g. `"Heading/XL"`) |
|
||||
| `fontSize` | `number` | In pixels |
|
||||
| `fontName` | `FontName` | `{ family: string, style: string }` — **font must be loaded before setting** |
|
||||
| `letterSpacing` | `LetterSpacing` | `{ value: number, unit: 'PIXELS' \| 'PERCENT' }` |
|
||||
| `lineHeight` | `LineHeight` | `{ value: number, unit: 'PIXELS' \| 'PERCENT' }` or `{ unit: 'AUTO' }` |
|
||||
| `textCase` | `TextCase` | `'ORIGINAL' \| 'UPPER' \| 'LOWER' \| 'TITLE' \| 'SMALL_CAPS'` |
|
||||
| `textDecoration` | `TextDecoration` | `'NONE' \| 'UNDERLINE' \| 'STRIKETHROUGH'` |
|
||||
| `paragraphSpacing` | `number` | |
|
||||
| `paragraphIndent` | `number` | |
|
||||
| `description` | `string` | Inherited from `BaseStyleMixin` |
|
||||
|
||||
### lineHeight and letterSpacing format
|
||||
|
||||
These properties must be objects — not bare numbers:
|
||||
|
||||
```js
|
||||
// WRONG — bare number throws
|
||||
style.lineHeight = 1.5;
|
||||
style.letterSpacing = 0;
|
||||
|
||||
// CORRECT
|
||||
style.lineHeight = { unit: "AUTO" }; // auto line height
|
||||
style.lineHeight = { value: 24, unit: "PIXELS" }; // fixed pixel height
|
||||
style.lineHeight = { value: 150, unit: "PERCENT" }; // 150% line height
|
||||
|
||||
style.letterSpacing = { value: 0, unit: "PIXELS" }; // zero tracking
|
||||
style.letterSpacing = { value: -2, unit: "PIXELS" }; // tight tracking
|
||||
style.letterSpacing = { value: 5, unit: "PERCENT" }; // percent-based tracking
|
||||
```
|
||||
|
||||
When reading a `lineHeight` back, always check `unit` first — `{ unit: 'AUTO' }` has no `value` key.
|
||||
|
||||
### Variable bindings on text styles
|
||||
|
||||
The following fields can be bound to variables via `style.setBoundVariable(field, variable)`:
|
||||
|
||||
`fontFamily`, `fontSize`, `fontStyle`, `fontWeight`, `letterSpacing`, `lineHeight`, `paragraphSpacing`, `paragraphIndent`
|
||||
|
||||
To unbind: `style.setBoundVariable(field, null)`
|
||||
|
||||
**Important: `setBoundVariable` is NOT available on `TextStyle` in headless `use_figma` mode.**
|
||||
|
||||
It is only available in interactive plugin context (UI plugins, Figma editor). When running through `use_figma` (MCP, assistant headless runtime), calling `ts.setBoundVariable(...)` will throw `"not a function"`. In this context, set raw values directly instead:
|
||||
|
||||
```js
|
||||
// In use_figma (headless) — variable binding not available
|
||||
const ts = figma.createTextStyle();
|
||||
ts.fontSize = 24; // set directly; cannot bind to a variable
|
||||
|
||||
// In a real interactive plugin — variable binding works
|
||||
const ts = figma.createTextStyle();
|
||||
ts.setBoundVariable("fontSize", fontSizeVariable);
|
||||
```
|
||||
|
||||
If live variable binding on text styles is required, the recommended approach is to:
|
||||
|
||||
1. Create the text styles with raw values via `use_figma`
|
||||
2. Open the file in Figma and bind variables interactively via the Styles panel, OR
|
||||
3. Use an interactive plugin that runs in the Figma editor (not headless)
|
||||
|
||||
### Applying a text style to a node
|
||||
|
||||
Once you have a `TextStyle`, apply it to a `TextNode` by assigning its `id` to the node's `textStyleId` property. You can also use the async setter `setTextStyleIdAsync(id)`. Setting `textStyleId` on a node does **not** require the font to be loaded — only editing the text content or font properties directly does.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **Font must be loaded before setting `fontName`**: Call `await figma.loadFontAsync({ family, style })` before creating or modifying a text style's font.
|
||||
- **Font style names are file-dependent**: Font style names like `"SemiBold"` vs `"Semi Bold"` vary by font provider and Figma file. Always probe by calling `loadFontAsync` and catching errors to discover the correct style string rather than guessing.
|
||||
- **`setBoundVariable` not available headless**: `TextStyle.setBoundVariable()` throws `"not a function"` in `use_figma` / headless mode. Set raw values instead and bind interactively if needed.
|
||||
- **Styles are not automatically applied**: Creating a `TextStyle` has no effect on any node until you assign its ID to a text node.
|
||||
- **`getLocalTextStyles()` is deprecated**: Always use `getLocalTextStylesAsync()`.
|
||||
- **Names are not unique**: Two text styles can share the same name. Match by ID or `key` when looking up a known style, not by name alone.
|
||||
- **Slash grouping is visual only**: `"Heading/XL"` and `"HeadingXL"` are different names; the slash is just a UI affordance.
|
||||
- **`lineHeight` and `letterSpacing` must be objects**: `style.lineHeight = 1.5` throws. Always use `{ value, unit }` format or `{ unit: 'AUTO' }`.
|
||||
|
||||
## Code patterns
|
||||
|
||||
For runnable code examples (listing, creating, probing fonts, type ramps, applying styles), see [text-style-patterns.md](../text-style-patterns.md).
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Working with design systems: Creating Variables
|
||||
|
||||
When creating Figma variables, you need to start by understanding the state of the source data.
|
||||
|
||||
If the user is asking you to create variables based on values, they likely want you to indicate the structure. Whether or not you use semantic aliasing primitive will be based on the inputs you are given about the source data.
|
||||
|
||||
If you are given code inputs (JSON, CSS, etc) your goal should be to reflect the existing patterns as closely as possible, but also embrace the design context as distinct from code. For example, casing is less important since you have code syntax that can directly represent the code form. Feel free to take a sentence or capitalized case approach for better readability in Figma.
|
||||
|
||||
It is important to understand the underlying structure before you create anything. If there is an implied aliased setup, you want to get that right. You may also need to anticipate modes to know how to split things up. Sizes and Colors likely have different mode requirements in complex systems, so you want to consider that as you create the structure.
|
||||
|
||||
If someone asks you to just make a decision based on best practices, that answer will be relative to the complexity of the environment. A simple theme is great best practice for simple needs. Similarly, a complex extended collection setup for someone on an enterprise plan might also be best practice as well.
|
||||
|
||||
Keep in mind that systems might also require you to handle text and effect styles for some of the things specified in token libraries.
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# Working with design systems: Using Variables
|
||||
|
||||
When using Figma variables, you need to start by understanding the state of the source and the state of Figma.
|
||||
|
||||
For the source, you need to know the breadth of variables code representation. CSS, JSON, theme providers etc will all be able to indicate what the user will expect you to cover in Figma. Some beginner users might not even know what does and doesn't exist in Figma, and if you cant discover that on your own, you will need their help making the right decision.
|
||||
|
||||
For Figma, you need to know what collections exist, what their modes are, and what values and names and code syntaxes are in them. This will help you make sure you are using the right things. For properties that "should" have variables but don't, you likely will need to ask the user what to do. Your understanding of Figma's current state should come first.
|
||||
|
||||
You can use code syntax and your understanding of the environment you are expected to be referencing to know which variable in Figma to use. You can also use Figma's variable scopes as indicators if they are specified. It is best to audit those up front.
|
||||
|
||||
When using variables you should also be aware of mode mismatches, the default mode in Figma may not be the mode referenced by the user in their expectations. Similarly, many collections may refer to values, but the most specific collection is what you should be using. For example, a semantic collection that aliases a primitive collection, the semantic collection would be what you reference. A component token collection (eg. button/background/primary) might alias a semantic collection, and it is the component collection you need to reference. In some other examples, there may be no aliasing and you're simply value matching.
|
||||
|
||||
Gap and padding values for frames are really important and often have to be interpreted semantically or based on layout component values.
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
# Working with design systems: Variables
|
||||
|
||||
Variables overlap a lot with the idea of tokens in a codebase, but with some gaps and other Figma-specific use cases. Variables are single value, number, string, color, boolean.
|
||||
|
||||
In Figma you can do conditional logic and use variables to get basic prototyping functionality. String values can also be used as sophisticated placeholder setups that have different modes for different languages. Not everything you use a variable for in Figma would be used exactly the same way in code. However, for design systems, they are often synced to code in some way.
|
||||
|
||||
One gap is the lack of composite tokens. You can't put a box shadow behind a single variable. That is an [effect style](wwds-effect-styles.md), but style values can be bound to variables. Similarly for a type ramp, you have to use [Text Styles](wwds-text-styles.md).
|
||||
|
||||
## Model
|
||||
|
||||
### Collections
|
||||
|
||||
Collections can be thought of a groups in Figma. An example Collection would be "Colors" where there might be a light and dark "Mode." Each value would have two definitions.
|
||||
|
||||
### Extended Collections
|
||||
|
||||
Extended collections allow you to create a colleciton based on another collection and only override _some_ of the values. Just like inheritance and overrides in CSS. This aligns well for scenarios like branded color themes.
|
||||
|
||||
### Modes
|
||||
|
||||
Modes in Figma can be thought of like light and dark, but users can specify modes for anything, including sizes, languages (string variables exist in Figma too).
|
||||
|
||||
### Aliasing
|
||||
|
||||
Aliasing in Figma variables is simply when you point a variable to another variable. Common example is pointing a semantic variable to a primitive variable. Some teams also do component level tokens which adds a third component specific layer.
|
||||
|
||||
**Decision rule:** If the source data has two tiers (primitives + semantics), create all primitives first, then create semantic variables that alias into them. If the source data is a single flat tier, create flat variables with no aliases. When in doubt, ask.
|
||||
|
||||
### Code Syntax
|
||||
|
||||
Code syntax is a surface area in Figma for codebase translation context. You can set WEB, iOS, and ANDROID code syntax on any variable, and when that variable is referenced in other places (visually in Figma's dev mode, as design context via MCP), this codebase form will appear. These are best thought of as "instance" documentation, eg. `var(--the-thing)` instead of `--the-thing` in the case of CSS.
|
||||
|
||||
### Scope
|
||||
|
||||
`variable.scopes: VariableScope[]` specifies which properties in Figma the variable can be used for. This is important when you create and when you use variables. **Always set specific scopes rather than leaving the default `ALL_SCOPES`** — it pollutes every property picker with irrelevant tokens. The more specific the better. For the canonical scope-to-use-case mapping, see [token-creation.md § Variable Scopes — Complete Reference Table](../../figma-generate-library/references/token-creation.md).
|
||||
|
||||
Common scope values:
|
||||
|
||||
- `ALL_SCOPES` — unrestricted; **avoid this** — it is the default but almost never the right choice. Only acceptable for very simple files with a handful of variables where the overhead of precise scoping isn't justified
|
||||
- `FRAME_FILL`, `SHAPE_FILL`, `TEXT_FILL`, `STROKE_COLOR` — color bindings (use specific fill scopes; `ALL_FILLS` covers all three fill scopes together)
|
||||
- `TEXT_CONTENT` — string variables for text layers
|
||||
- `FONT_SIZE`, `FONT_WEIGHT`, `LINE_HEIGHT`, `LETTER_SPACING` — typography
|
||||
- `CORNER_RADIUS`, `WIDTH_HEIGHT`, `GAP` — layout/spacing
|
||||
- `OPACITY` — layer opacity
|
||||
|
||||
### Grouping
|
||||
|
||||
Variable names in Figma are slash delimited and each slash represents a group that is visualized in Figma. When you are doing matching, consider a part of a code prefix might be the name of the collection, not a top level group. Sometimes you will have prefixes in code that aren't in Figma, and that can be ok, just be sure to ask if it is unclear. You can always validate existing variables by referencing the code syntax.
|
||||
|
||||
## Common gotchas
|
||||
|
||||
- **`createVariableCollection` always creates a default mode** — you will need to rename it (or delete it and add your own) rather than creating from scratch.
|
||||
- **Duplicate variable names throw silently** — Figma does not error; it creates a second variable with the same name. Always check for existence before creating.
|
||||
- **Variable aliases require the target to be in the same file** — cross-file aliasing is not supported via the plugin API. If you need to alias to a library variable, import it first.
|
||||
- **`setValueForMode` with an alias requires the exact shape** — `{ type: 'VARIABLE_ALIAS', id: '<variableId>' }`. Any deviation will silently set the wrong value or throw.
|
||||
|
||||
## Usage guidelines
|
||||
|
||||
- [Creating variables](wwds-variables--creating.md): What you must consider when creating new variables.
|
||||
- [Using variables](wwds-variables--using.md): What you must consider when trying to use the right variables.
|
||||
|
||||
## Code patterns
|
||||
|
||||
For runnable code examples (creating collections, binding variables, scopes, aliasing, discovering existing variables), see [variable-patterns.md](../variable-patterns.md).
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
# Working with design systems
|
||||
|
||||
When working with design systems in Figma, there can be many nuances when deciding how to do the right thing. Figma's model for patterns is form-agnostic, this is one of its strengths, allowing people to refer to a pattern in a spec that may take distinct forms in different codebases. However, this can result in complex procedures and nuances when translating something to Figma and back. Figma has components, tokens, and other reusable patterns (text and effect styles, prototyping actions, etc). The way that Figma's paradigms function can be difficult to translate one to one.
|
||||
|
||||
To make translation of patterns work between design and code forms, it is important that teams think about alignment while also embracing the function of representation (design) and implementation (production) forms independently as complementary pieces of a shared puzzle.
|
||||
|
||||
For the process of design, the desirable state of a system is something that is structured with experimentation in mind, something that is highly visual, easy to iterate, test, and confirm new ideas. Depending on the product and team, exploration might be mandatory to be done within the confines of an existing system, for other scenarios, exploration of new territory is the priority, a place for the system to grow into, or a new system to be made. Figma's platform allows for teams to validate, align, and collaborate on new ideas, then solidify them in product designs, which are ultimately specs. That work is supported by design libraries and that process can include code in prototypes and other less permanent forms as much as it does Figma's native paradigms.
|
||||
|
||||
For the process of implementation, the desirable state for production code is rigidity, efficiency, and related to secure data and functional layers. Developer experience implementing a design and the designer experience surfacing and committing to an idea are paths from distinct points of to the same shared outcome. Their optimization looks different, and that is reflected when you engage with Figma's APIs.
|
||||
|
||||
The key is not to avoid gaps, but to make sure they are definitively bridgable. Translation layers help agents and people go between representational and production forms.
|
||||
|
||||
The Figma paradigms you will need to understand when working with design systems. In each file below there will be further links to instructions for using and creating:
|
||||
|
||||
- [Components](wwds-components.md)
|
||||
- [Variables](wwds-variables.md)
|
||||
- [Effect Styles](wwds-effect-styles.md)
|
||||
- [Text Styles](wwds-text-styles.md)
|
||||
|
||||
Things you might be asked to do with respect to design systems:
|
||||
|
||||
- Create patterns in Figma that match patterns in code
|
||||
- Likely (but not exclusively) to get up to speed so that visual riffing can be done in Figma
|
||||
- Create variables based on a stylesheet, JSON format, some other theme definition
|
||||
- Create Figma text styles that match a type hierarchy defined somewhere
|
||||
- Create components based on existing code components
|
||||
- Sync between code and design forms
|
||||
- Making sure that Figma's concepts match a production form
|
||||
- Use an existing Figma design library to create something
|
||||
- This something could be matching an existing code form, an image, or just a prompt
|
||||
- Clean up a design to match some code pattern
|
||||
|
||||
## Things to remember
|
||||
|
||||
Many people will use these tools to try out ideas, and not everything you get asked to do will feel realistic for the environment you are running in. It is important to contextualize that, but then also know when you are definitively working in a production environment and there is a very real task you need to perform consistently.
|
||||
|
||||
Not everyone asking you to do something knows what they should be doing. You must figure out if the request is to generically perform design systems actions, uphold existing the rules that are codified in Figma or in a codebase, demonstrate an idea, enforce existing guidelines, etc.
|
||||
|
||||
Not every environment you are working in has the same degree of expertise and maturity. Some systems will be very complex and the priority and have a lot of things to parse through to get to the right outcome. Some scenarios will be very immature and even starting from scratch. Something as simple as creating a component could be very elementary or very sophisticated depending on the environment. The instructions you find here are attempting to be unbiased.
|
||||
|
||||
For example, how you reflect the "hover" state of a button could be left entirely up to you to make a reasonable decision for a user that is playing around with getting a decent example scaffolded using best practices, but it could also be something that exists definitively in the codebase and you need to go match it. That codebase definition could be refering to design tokens that do not yet exist in code that change dark and light mode values. In this second example you are now needing to do a bunch of variables work just to add a hover state to a component with proper dark and light mode support, where in the first scenario, you can kinda just do whatever is easiest. This is the line you will be walking, and making good judgement here is about doing whatever is the smartest thing in the environment you are in.
|
||||
Reference in New Issue
Block a user