diff --git a/plugins/figma/plugin.lock.json b/plugins/figma/plugin.lock.json index e7af750d..b05a6707 100644 --- a/plugins/figma/plugin.lock.json +++ b/plugins/figma/plugin.lock.json @@ -7,7 +7,7 @@ "skills": [ { "id": "figma-code-connect", - "vendoredPath": "skills/figma-code-connect-components", + "vendoredPath": "skills/figma-code-connect", "source": { "type": "github", "repo": "figma/mcp-server-guide", diff --git a/plugins/figma/skills/figma-code-connect-components/references/mapping-checklist.md b/plugins/figma/skills/figma-code-connect-components/references/mapping-checklist.md deleted file mode 100644 index 8f4f0264..00000000 --- a/plugins/figma/skills/figma-code-connect-components/references/mapping-checklist.md +++ /dev/null @@ -1,7 +0,0 @@ -# Code Connect Mapping Checklist - -- Confirm Figma component is published -- Convert URL node-id (`-`) to tool nodeId (`:`) -- Check existing mappings first -- Compare props/variants between design and code -- Ask for confirmation when multiple matches are plausible diff --git a/plugins/figma/skills/figma-code-connect-components/scripts/normalize_node_id.py b/plugins/figma/skills/figma-code-connect-components/scripts/normalize_node_id.py deleted file mode 100755 index 31b8ef80..00000000 --- a/plugins/figma/skills/figma-code-connect-components/scripts/normalize_node_id.py +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env python3 -"""Normalize a Figma node-id between URL and tool formats.""" - -from __future__ import annotations - -import sys - - -def main() -> int: - if len(sys.argv) != 2: - print("Usage: normalize_node_id.py ", file=sys.stderr) - return 2 - node = sys.argv[1].strip() - if not node: - print("Empty node-id", file=sys.stderr) - return 1 - if ":" in node: - print(node.replace(":", "-")) - else: - print(node.replace("-", ":")) - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/plugins/figma/skills/figma-code-connect-components/LICENSE.txt b/plugins/figma/skills/figma-code-connect/LICENSE.txt similarity index 100% rename from plugins/figma/skills/figma-code-connect-components/LICENSE.txt rename to plugins/figma/skills/figma-code-connect/LICENSE.txt diff --git a/plugins/figma/skills/figma-code-connect-components/SKILL.md b/plugins/figma/skills/figma-code-connect/SKILL.md similarity index 56% rename from plugins/figma/skills/figma-code-connect-components/SKILL.md rename to plugins/figma/skills/figma-code-connect/SKILL.md index d27f609b..2394c2ce 100644 --- a/plugins/figma/skills/figma-code-connect-components/SKILL.md +++ b/plugins/figma/skills/figma-code-connect/SKILL.md @@ -1,14 +1,16 @@ --- name: figma-code-connect -description: Creates and maintains Figma Code Connect template files that map Figma components to code snippets. Use when the user mentions Code Connect, Figma component mapping, design-to-code translation, or asks to create/update .figma.js files. +description: Creates and maintains Figma Code Connect template files that map Figma components to code snippets. Use when the user mentions Code Connect, Figma component mapping, design-to-code translation, or asks to create/update .figma.ts or .figma.js files. disable-model-invocation: false --- +# Code Connect + ## Overview -Create parserless Code Connect template files (`.figma.js`) that map Figma components to code snippets. Given a Figma URL, follow the steps below to create a template. +Create Code Connect template files (`.figma.ts`) that map Figma components to code snippets. Given a Figma URL, follow the steps below to create a template. -> **Note:** This project may also contain parser-based `.figma.tsx` files (using `figma.connect()`, published via CLI). This skill covers **parserless templates only** — `.figma.js` files that use the MCP tools to fetch component context from Figma. +> **Note:** This project may also contain parser-based `.figma.tsx` files (using `figma.connect()`, published via CLI). This skill covers **templates files only** — `.figma.ts` files that use the MCP tools to fetch component context from Figma. ## Prerequisites @@ -16,6 +18,14 @@ Create parserless Code Connect template files (`.figma.js`) that map Figma compo - **Components must be published** — Code Connect only works with components published to a Figma team library. If a component is not published, inform the user and stop. - **Organization or Enterprise plan required** — Code Connect is not available on Free or Professional plans. - **URL must include `node-id`** — the Figma URL must contain the `node-id` query parameter. +- **TypeScript types** — for editor autocomplete and type checking in `.figma.ts` files `@figma/code-connect/figma-types` must be added to `types` in `tsconfig.json`: + ```json + { + "compilerOptions": { + "types": ["@figma/code-connect/figma-types"] + } + } + ``` ## Step 1: Parse the Figma URL @@ -64,7 +74,8 @@ The response contains the Figma component's **property definitions** — note ea - **TEXT** — text content (labels, titles, placeholders) - **BOOLEAN** — toggles (show/hide icon, disabled state) - **VARIANT** — enum options (size, variant, state) -- **INSTANCE_SWAP** — swappable component slots (icon, avatar) +- **INSTANCE_SWAP** — swappable nested instances tied to a specific component (icon, avatar) +- **SLOT** — flexible content regions (freeform layout, mixed children); use `getSlot()` in templates (not the same as INSTANCE_SWAP) Save this property list — you will use it in Step 5 to write the template. @@ -84,28 +95,28 @@ Read `figma.config.json` for import path aliases — the `importPaths` section m Read the code component's source to understand its props interface — this informs how to map Figma properties to code props in Step 5. -## Step 5: Create the Parserless Template (.figma.js) +## Step 5: Create the Template File (.figma.ts) ### File location -Place the file alongside existing Code Connect templates (`.figma.tsx` or `.figma.js` files). Check `figma.config.json` `include` patterns for the correct directory. Name it `ComponentName.figma.js`. +Place the file alongside existing Code Connect templates (`.figma.tsx` or `.figma.ts` files). Check `figma.config.json` `include` patterns for the correct directory. Name it `ComponentName.figma.ts`. ### Template structure -Every parserless template follows this structure: +Every template file follows this structure: -```js +```ts // url=https://www.figma.com/file/{fileKey}/{fileName}?node-id={nodeId} // source={path to code component from Step 4} // component={code component name from Step 4} -const figma = require('figma') +import figma from 'figma' const instance = figma.selectedInstance // Extract properties from the Figma component (see property mapping below) // ... export default { - example: figma.tsx``, // Required: code snippet + example: figma.code``, // Required: code snippet imports: ['import { Component } from "..."'], // Optional: import statements id: 'component-name', // Required: unique identifier metadata: { // Optional @@ -124,17 +135,18 @@ Use the property list from Step 3 to extract values. For each Figma property typ | TEXT | `instance.getString('Name')` | Labels, titles, placeholder text | | BOOLEAN | `instance.getBoolean('Name', { true: ..., false: ... })` | Toggle visibility, conditional props | | VARIANT | `instance.getEnum('Name', { 'FigmaVal': 'codeVal' })` | Size, variant, state enums | -| INSTANCE_SWAP | `instance.getInstanceSwap('Name')` | Icon slots, swappable children | +| INSTANCE_SWAP | `instance.getInstanceSwap('Name')` | Swapped instance for a fixed component slot (then `hasCodeConnect()` / `executeTemplate()`) - do not confuse with the SLOT property below | +| SLOT | `instance.getSlot('Name')` | Freeform slot content only when the Figma property type is **SLOT** | (child layer) | `instance.findInstance('LayerName')` | Named child instances without a property | | (text layer) | `instance.findText('LayerName')` → `.textContent` | Text content from named layers | **TEXT** — get the string value directly: -```js +```ts const label = instance.getString('Label') ``` **VARIANT** — map Figma enum values to code values: -```js +```ts const variant = instance.getEnum('Variant', { 'Primary': 'primary', 'Secondary': 'secondary', @@ -148,31 +160,84 @@ const size = instance.getEnum('Size', { ``` **BOOLEAN** — simple boolean or mapped to values: -```js +```ts // Simple boolean const disabled = instance.getBoolean('Disabled') -// Mapped to code values -const hasIcon = instance.getBoolean('Has Icon', { - true: figma.tsx``, - false: undefined, +// Mapped to code values (e.g. when the code prop is an enum, not a boolean) +const size = instance.getBoolean('Show Label', { true: 'large', false: 'small' }) +``` + +**Map Figma properties to code props where there's a valid correspondence.** Figma properties and code props don't always line up 1:1 — some Figma properties map directly (by name, or via the API methods above), others have no code equivalent. Where a mapping exists, use it; where none fits, omit the Figma property rather than invent a code prop. Never emit an attribute whose name doesn't appear in the code component's `Props` interface. + +### Exhaustive variant handling + +When a VARIANT property has multiple possible values, the `getEnum` mapping **must list every value** returned by `get_context_for_code_connect`. Don't omit values — an unmapped value silently returns `undefined`, producing broken output. + +```ts +// WRONG — omits 'Warning', which will render as undefined +const status = instance.getEnum('Status', { + 'Success': 'success', + 'Error': 'error', }) + +// CORRECT — every value is mapped +const status = instance.getEnum('Status', { + 'Success': 'success', + 'Error': 'error', + 'Warning': 'warning', + 'Info': 'info', +}) +``` + +When **two or more VARIANT properties combine** to produce different code output, generate exhaustive conditional branches. For example, 2 variants × 2 values = 4 branches: + +```ts +const type = instance.getEnum('Type', { 'Filled': 'filled', 'Outlined': 'outlined' }) +const status = instance.getEnum('Status', { 'Success': 'success', 'Error': 'error' }) + +let colorClass +if (type === 'filled' && status === 'success') { + colorClass = 'bg-green-500 text-white' +} else if (type === 'filled' && status === 'error') { + colorClass = 'bg-red-500 text-white' +} else if (type === 'outlined' && status === 'success') { + colorClass = 'bg-transparent border-green-500' +} else if (type === 'outlined' && status === 'error') { + colorClass = 'bg-transparent border-red-500' +} ``` +If the combinations produce **repetitive** output (e.g., `Size` doesn't change the snippet structure — it's just passed through as a prop), a single `getEnum` mapping per variant is sufficient — no need for cross-product branches. + **INSTANCE_SWAP** — access swappable component instances: -```js +```ts const icon = instance.getInstanceSwap('Icon') let iconCode -if (icon && icon.hasCodeConnect()) { +if (icon && icon.type === 'INSTANCE') { iconCode = icon.executeTemplate().example } ``` +**SLOT** — `getSlot(propName)` is only valid when the Figma component property reported in Step 3 has type **`SLOT`**. Do not use `getSlot()` for **INSTANCE_SWAP** properties (those use `getInstanceSwap()`). Slots are explicit “content regions” in the component definition, not generic nested instances. + +- **Signature:** `getSlot(propName: string): ResultSection[] | undefined` +```ts +// Figma property "Content" must be type SLOT in component properties +const content = instance.getSlot('Content') + +export default { + example: figma.code`${content}`, + // ... +} +``` + ### Interpolation in tagged templates When interpolating values in tagged templates, use the correct wrapping: - **String values** (`getString`, `getEnum`, `textContent`): wrap in quotes → `variant="${variant}"` - **Instance/section values** (`executeTemplate().example`): wrap in braces → `icon={${iconCode}}` +- **Slot sections** (`getSlot()` result — `ResultSection[] | undefined`): interpolate directly inside `` figma.code`...` `` (same shape as nested snippet sections), e.g. `` figma.code`` `` — do not treat as a plain string - **Boolean bare props**: use conditional → `${disabled ? 'disabled' : ''}` ### Finding descendant layers @@ -181,38 +246,63 @@ When you need to access children that aren't exposed as component properties: | Method | Use when | |---|---| -| `instance.getInstanceSwap('PropName')` | A component property exists for this slot | +| `instance.getInstanceSwap('PropName')` | Figma property type is **INSTANCE_SWAP** (fixed swapped instance) | +| `instance.getSlot('PropName')` | Figma property type is **SLOT** (freeform content region) | | `instance.findInstance('LayerName')` | You know the child layer name (no component property) | | `instance.findText('LayerName')` → `.textContent` | You need text content from a named text layer | | `instance.findConnectedInstance('id')` | You know the child's Code Connect `id` | | `instance.findConnectedInstances(fn)` | You need multiple connected children matching a filter | | `instance.findLayers(fn)` | You need any layers (text + instances) matching a filter | +### Nested configurable instances + +A component may contain child instances that are **not exposed as component properties** (no INSTANCE_SWAP) but are still **independently configurable** — they have their own variants, properties, or swap slots. These must be resolved dynamically, not hardcoded. + +1. **Check whether the child already has a Code Connect template** — use `get_code_connect_suggestions` or check existing `.figma.ts` files in the project. +2. **If no template exists, create one** for the child so it renders correctly both standalone and when nested. +3. **Reference the child from the parent** using `findInstance()` or `findConnectedInstance()`, then call `executeTemplate()`. + +```ts +// Parent template — the Badge child isn't a prop, but it's configurable +const badge = instance.findInstance('Status Badge') +let badgeCode +if (badge && badge.type === 'INSTANCE') { + badgeCode = badge.executeTemplate().example +} + +export default { + example: figma.code`${badgeCode}`, + // ... +} +``` + +This applies to icons, badges, labels, and any other nested instance that is configurable by itself — always connect them and render dynamically, never hardcode their content. + ### Nested component example For multi-level nested components or metadata prop passing between templates, see [advanced-patterns.md](references/advanced-patterns.md). -```js +```ts const icon = instance.getInstanceSwap('Icon') let iconSnippet -if (icon && icon.hasCodeConnect()) { +if (icon && icon.type === 'INSTANCE') { iconSnippet = icon.executeTemplate().example } export default { - example: figma.tsx``, + example: figma.code``, // ... } ``` ### Conditional props -```js +```ts const variant = instance.getEnum('Variant', { 'Primary': 'primary', 'Secondary': 'secondary' }) const disabled = instance.getBoolean('Disabled') export default { - example: figma.tsx` + example: figma.code` ` + + // CORRECT — resolves dynamically, works for any swapped icon + const icon = instance.findInstance('Icon') + let iconCode + if (icon && icon.type === 'INSTANCE') { + iconCode = icon.executeTemplate().example + } + example: figma.code`...` ``` +8. **Attempt to represent every Figma property via a code prop.** The code component's `Props` interface (from Step 4) is the authoritative list of attribute names. For each Figma property, figure out the right way to represent it using the API methods from Step 5 — direct name match, value transformation, or whatever fits. If no code prop fits at all, omit it — don't invent a prop name. + ## Complete Worked Example Given URL: `https://figma.com/design/abc123/MyFile?node-id=42-100` @@ -355,13 +473,13 @@ Response includes properties: **Step 4:** Search codebase → find `Button` component. Read its source to confirm props: `variant`, `size`, `disabled`, `icon`, `children`. Import path: `"primitives"`. -**Step 5:** Create `src/figma/primitives/Button.figma.js`: +**Step 5:** Create `src/figma/primitives/Button.figma.ts`: -```js +```ts // url=https://figma.com/design/abc123/MyFile?node-id=42-100 // source=src/components/Button.tsx // component=Button -const figma = require('figma') +import figma from 'figma' const instance = figma.selectedInstance const label = instance.getString('Label') @@ -378,17 +496,17 @@ const disabled = instance.getBoolean('Disabled') const hasIcon = instance.getBoolean('Has Icon') const icon = hasIcon ? instance.getInstanceSwap('Icon') : null let iconCode -if (icon && icon.hasCodeConnect()) { +if (icon && icon.type === 'INSTANCE') { iconCode = icon.executeTemplate().example } export default { - example: figma.tsx` + example: figma.code` diff --git a/plugins/figma/skills/figma-code-connect-components/agents/openai.yaml b/plugins/figma/skills/figma-code-connect/agents/openai.yaml similarity index 100% rename from plugins/figma/skills/figma-code-connect-components/agents/openai.yaml rename to plugins/figma/skills/figma-code-connect/agents/openai.yaml diff --git a/plugins/figma/skills/figma-code-connect-components/references/advanced-patterns.md b/plugins/figma/skills/figma-code-connect/references/advanced-patterns.md similarity index 100% rename from plugins/figma/skills/figma-code-connect-components/references/advanced-patterns.md rename to plugins/figma/skills/figma-code-connect/references/advanced-patterns.md diff --git a/plugins/figma/skills/figma-code-connect-components/references/api.md b/plugins/figma/skills/figma-code-connect/references/api.md similarity index 97% rename from plugins/figma/skills/figma-code-connect-components/references/api.md rename to plugins/figma/skills/figma-code-connect/references/api.md index 9ab37f40..fe74f311 100644 --- a/plugins/figma/skills/figma-code-connect-components/references/api.md +++ b/plugins/figma/skills/figma-code-connect/references/api.md @@ -281,6 +281,18 @@ if (icon) { } ``` +#### `getSlot(propName: string): ResultSection[] | undefined` + +Reads a Figma component property whose type is **`SLOT`** (not **INSTANCE_SWAP**). Returns a `ResultSection[]` containing a slot section (`type: 'SLOT'`). Returns `undefined` if the property is missing or invalid. Do not call `executeTemplate()` on this value — unlike `getInstanceSwap()`, it is not an `InstanceHandle`. + +```javascript +const content = instance.getSlot('Content') + +export default { + example: figma.code`${content}` +} +``` + #### `getPropertyValue(propName: string): string | boolean` Gets the raw property value without mapping. diff --git a/plugins/figma/skills/figma-create-new-file/SKILL.md b/plugins/figma/skills/figma-create-new-file/SKILL.md index 8f74967b..73fa2ee4 100644 --- a/plugins/figma/skills/figma-create-new-file/SKILL.md +++ b/plugins/figma/skills/figma-create-new-file/SKILL.md @@ -1,6 +1,7 @@ --- 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 diff --git a/plugins/figma/skills/figma-generate-design/SKILL.md b/plugins/figma/skills/figma-generate-design/SKILL.md index ca805d1c..5ed4e9e1 100644 --- a/plugins/figma/skills/figma-generate-design/SKILL.md +++ b/plugins/figma/skills/figma-generate-design/SKILL.md @@ -1,12 +1,12 @@ --- 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." +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', 'convert this modal/dialog/drawer/panel to Figma'. This is the preferred workflow skill whenever the user wants to build or update a full page, modal, dialog, drawer, sidebar, panel, or any composed multi-section view in Figma from code or a description. Discovers design system components, variables, and styles from Code Connect files, existing screens, and library search, then imports them and assembles views incrementally section-by-section using design system tokens instead of hardcoded values." disable-model-invocation: false --- -# Build / Update Screens from Design System +# Build / Update Screens and Views 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. +Use this skill to create or update **screens, views, and multi-section UI containers** in Figma by **reusing the published design system** — components, variables, and styles — rather than drawing primitives with hardcoded values. This includes full pages, modals, dialogs, drawers, sidebars, panels, and any composed view with multiple sections. 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. @@ -14,10 +14,10 @@ Use this skill to create or update full-page screens in Figma by **reusing the p ## Skill Boundaries -- Use this skill when the deliverable is a **Figma screen** (new or updated) composed of design system component instances. +- Use this skill when the deliverable is a **composed Figma view** (new or updated) — full-page screens, modals, dialogs, drawers, sidebars, panels, or any multi-section container — built from 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). +- If the user wants to write **Code Connect mappings**, switch to [figma-code-connect](../figma-code-connect/SKILL.md). ## Prerequisites @@ -26,7 +26,7 @@ Use this skill to create or update full-page screens in Figma by **reusing the p - 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 +- Source code or description of the screen/view to build/update ## Parallel Workflow with generate_figma_design (Web Apps Only) @@ -35,32 +35,64 @@ When building a screen from a **web app** that can be rendered in a browser, the 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. +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. If the capture contains images, transfer them to your use_figma output by copying `imageHash` values from the capture's image fills (see Step 5 for details). 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. +**This parallel workflow is MANDATORY when the source contains images.** The `use_figma` Plugin API cannot fetch external image URLs — it can only set image fills by copying `imageHash` values from nodes already in the file. `generate_figma_design` rasterizes all visible images into Figma, providing the hashes you need. If you skip the capture when images are present, image frames will be left blank. + +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 +> **Hard gates — forbidden shortcuts:** +> +> - **Forbidden:** `search_design_system` for component keys until 2a-i is complete and 2a-ii is attempted or logged N/A (e.g. "empty file, no existing screens"). +> - **Forbidden:** Any `use_figma` call that mutates the canvas (Step 3+) until all Step 2 rows in the checklist below are filled in. + +### Step 1: Understand the Deliverable 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). +1. If building from code, read the relevant source files to understand the structure, sections, and which components are used. +2. Identify the major sections of the view (e.g., for a page: Header, Hero, Content Panels, Footer; for a modal: Title Bar, Form Sections, Action Bar; for a sidebar: Navigation, Content Area, Footer Actions). 3. For each section, list the UI components involved (buttons, inputs, cards, navigation pills, accordions, etc.). +4. **Check whether the view contains any images** (e.g., ``, ``, background images, product photos, avatars, icons loaded from URLs). If it does and this is a web app, you **must** run the parallel `generate_figma_design` capture workflow — start it immediately alongside Step 2 so the capture runs while you discover components. See "Parallel Workflow with generate_figma_design" above. -### Step 2: Discover Design System — Components, Variables, and Styles +### Step 2: Collect Component Keys, 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: + +**2a-i — REQUIRED: Check Code Connect for needed components.** Starting from the component list you built in Step 1, check whether each component has a Code Connect file in the codebase. Code Connect files live next to the component source and are named by platform: + +- **TypeScript/JS**: `*.figma.ts`, `*.figma.js` +- **React (parser-based)**: `*.figma.tsx` +- **Kotlin/Compose**: `.kt` files containing `@FigmaConnect` +- **Swift**: `.swift` files containing `FigmaConnect` + +For each component you need (e.g., Button, Card, Input), search for its Code Connect file — glob or grep by component name (e.g., `**/Button.figma.tsx`, `**/Card.figma.ts`). Only read files that match components you actually need. + +From each matching Code Connect file, extract the Figma component URL. Parse `fileKey` and `nodeId` from the URL (convert hyphens to colons: `123-456` → `123:456`). Then resolve component keys via `use_figma`: + +**Example:** Code Connect file contains `// url=https://figma.com/design/ABC123/File?node-id=609-35535`. Parse `fileKey` = `ABC123`, `nodeId` = `609:35535`. Run `use_figma` against the **library file** (fileKey `ABC123`, not the target file) to resolve the key: + +```js +const node = await figma.getNodeByIdAsync("609:35535"); +const set = node?.parent?.type === "COMPONENT_SET" ? node.parent : node; +return { componentKey: set.key }; +``` + +Batch multiple lookups in a single call. Use the returned keys with `importComponentSetByKeyAsync()` in Step 4. + +Mark resolved components. If all components are resolved, skip 2a-ii and 2a-iii. If none of the needed components have Code Connect files, proceed to 2a-ii. + +**2a-ii — REQUIRED if unresolved components remain: Inspect existing screens.** Check if the target file already contains screens using the same design system. 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"); @@ -77,7 +109,9 @@ frame.findAll(n => n.type === "INSTANCE").forEach(inst => { 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. +Match results against your unresolved components. Mark any newly resolved. If all components are resolved, skip 2a-iii. + +**2a-iii — LAST RESORT: `search_design_system`.** Only if components remain unresolved after completing both 2a-i and 2a-ii. **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. @@ -164,11 +198,11 @@ Import library styles with `figma.importStyleByKeyAsync(key)`, then apply with ` 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 +### Step 3: Create the 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: +Create the wrapper in its own `use_figma` call. Position it away from existing content and return its ID: ```js // Find clear space @@ -177,14 +211,20 @@ 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"; +const wrapper = figma.createAutoLayout("VERTICAL"); + +// --- Size the wrapper based on container type --- +// Full page: wrapper.resize(1440, 100); wrapper.name = "Homepage"; +// Modal/dialog: wrapper.resize(640, 100); wrapper.name = "Settings Modal"; +// Drawer/sidebar: wrapper.resize(360, 100); wrapper.name = "Navigation Drawer"; +// Panel: wrapper.resize(400, 100); wrapper.name = "Details Panel"; +// Adapt width to match the source code's actual dimensions. + +wrapper.name = "VIEW_NAME"; wrapper.primaryAxisAlignItems = "CENTER"; wrapper.counterAxisAlignItems = "CENTER"; -wrapper.resize(1440, 100); +wrapper.resize(WIDTH, 100); wrapper.layoutSizingHorizontal = "FIXED"; -wrapper.layoutSizingVertical = "HUG"; wrapper.x = maxX + 200; wrapper.y = 0; @@ -210,9 +250,8 @@ const bgColorVar = await figma.variables.importVariableByKeyAsync("BG_COLOR_VAR_ const spacingVar = await figma.variables.importVariableByKeyAsync("SPACING_VAR_KEY"); // Build section frame with variable bindings (not hardcoded values) -const section = figma.createFrame(); +const section = figma.createAutoLayout(); section.name = "Header"; -section.layoutMode = "HORIZONTAL"; section.setBoundVariable("paddingLeft", spacingVar); section.setBoundVariable("paddingRight", spacingVar); const bgPaint = figma.variables.setBoundVariableForPaint( @@ -262,25 +301,54 @@ When translating code components to Figma instances, check the component's defau | Build manually | Import from design system | |----------------|--------------------------| -| Page wrapper frame | **Components**: buttons, cards, inputs, nav, etc. | +| 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 +### Step 5: Validate the Full View and Transfer Images -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. +After composing all sections, call `get_screenshot` on the wrapper frame and compare against the source. Fix any issues with targeted `use_figma` calls — don't rebuild the entire view. -**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: +**Screenshot individual sections, not just the full view.** A full-view 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 +- **Blank image placeholders** — if images are missing, you need to transfer them from the `generate_figma_design` capture (see below) + +#### Transfer images from the generate_figma_design capture + +If you ran `generate_figma_design` in parallel (mandatory when the source contains images), transfer the captured images into your design system output: + +1. Find all image nodes in the capture output by searching for fills with `type === "IMAGE"`: + ```js + const capture = await figma.getNodeByIdAsync("CAPTURE_NODE_ID"); + const imageNodes = []; + capture.findAll(n => { + if (n.fills && Array.isArray(n.fills)) { + for (const fill of n.fills) { + if (fill.type === "IMAGE") { + imageNodes.push({ name: n.name, id: n.id, imageHash: fill.imageHash }); + return true; + } + } + } + return false; + }); + return imageNodes; + ``` +2. Match each captured image to the corresponding frame in your use_figma output (by position, name, or order). +3. Apply the image hash to the target frame: + ```js + targetFrame.fills = [{ type: "IMAGE", imageHash: "hash_from_capture", scaleMode: "FILL" }]; + ``` +4. Delete the `generate_figma_design` capture output after all images are transferred. + +### Step 6: Updating an Existing View When updating rather than creating from scratch: @@ -320,7 +388,7 @@ For detailed API patterns and gotchas, load these from the [figma-use](../figma- ## Error Recovery -Follow the error recovery process from [figma-use](../figma-use/SKILL.md#6-error-recovery--self-correction): +Follow the error recovery process from [figma-use](../figma-use/SKILL.md#7-error-recovery--self-correction): 1. **STOP** on error — do not retry immediately. 2. **Read the error message carefully** to understand what went wrong. diff --git a/plugins/figma/skills/figma-generate-diagram/LICENSE.txt b/plugins/figma/skills/figma-generate-diagram/LICENSE.txt new file mode 100644 index 00000000..5dcf1aa2 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/LICENSE.txt @@ -0,0 +1,2 @@ +Use of these Figma skills and related files ("Materials") is governed by the Figma Developer Terms (available at https://www.figma.com/legal/developer-terms/). By accessing, downloading, or using these Materials — including through automated systems or AI agents — you agree to the Figma Developer Terms. +These Materials are currently offered as a Beta feature. Figma may modify, suspend, or discontinue them at any time without notice. diff --git a/plugins/figma/skills/figma-generate-diagram/SKILL.md b/plugins/figma/skills/figma-generate-diagram/SKILL.md new file mode 100644 index 00000000..724c4d6e --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/SKILL.md @@ -0,0 +1,112 @@ +--- +name: figma-generate-diagram +description: "MANDATORY prerequisite — load this skill BEFORE every `generate_diagram` tool call. Routes to type-specific guidance (generic flowchart, architecture flowchart) and tells you when to proceed directly, when to use a different diagram type, or when the tool isn't the right fit at all." +disable-model-invocation: false +--- + +# generate-diagram + +**You MUST load this skill before every `generate_diagram` tool call.** Skipping it causes preventable rendering failures and low-quality output. + +`generate_diagram` takes Mermaid.js syntax and produces an editable FigJam diagram. This skill routes you to the right per-type guidance and sets universal constraints. + +## Step 1: Is `generate_diagram` the right tool? + +### Supported diagram types + +`flowchart`, `sequenceDiagram`, `stateDiagram` / `stateDiagram-v2`, `gantt`, `erDiagram`. + +### Unsupported — don't call the tool + +If the user wants any of these, tell them directly that `generate_diagram` doesn't support it instead of calling the tool and failing: +- **Pie chart, mindmap, venn diagram, class diagram, journey, timeline, quadrant, C4, git graph, requirement diagram** + +### When to push the user to edit in Figma + +The tool cannot: +- Change fonts on an existing diagram +- Move individual shapes +- Edit a diagram node-by-node after generation + +If the user asks for any of those on an existing diagram, recommend they open the diagram in Figma and edit there. For content-level changes, it's usually faster to regenerate. + +## Step 2: Pick the diagram type + +Lightweight routing — use the first match. + +| User wants… | Type | Next step | +|---|---|---| +| Services + datastores + queues + integrations | **Architecture flowchart** | Read [references/architecture.md](./references/architecture.md) | +| Decision tree, process flow, pipeline, dependency graph, user journey | **Flowchart** | Read [references/flowchart.md](./references/flowchart.md) | +| Interactions between parties over time (API calls, auth, messaging) | **Sequence diagram** | Read [references/sequence.md](./references/sequence.md) | +| Data model, tables, keys, cardinality | **ER diagram** | Read [references/erd.md](./references/erd.md) | +| Named states with transitions between them | **State diagram** | Read [references/state.md](./references/state.md) | +| Project schedule with dates, milestones | **Gantt chart** | Read [references/gantt.md](./references/gantt.md) | + +If a flowchart is requested and it describes software infrastructure (services, datastores, queues, external integrations), route to `architecture.md` — not `flowchart.md`. When in doubt, ask the user. + +## Step 3: Universal constraints (apply to every diagram type) + +1. **No emojis** in any part of the Mermaid source. The tool rejects them. +2. **No `\n`** in labels. Use newlines only when absolutely required and only via actual line breaks (not the escape sequence). +3. **No HTML tags** in labels. +4. **Reserved words** — don't use `end`, `subgraph`, `graph` as node IDs. +5. **Node IDs**: camelCase (`userService`), no spaces. Underscores can break edge routing in some processors. +6. **Special characters in labels** must be wrapped in quotes: `A["Process (main)"]`, `-->|"O(1) lookup"|`. +7. **Sequence diagrams** — do not use notes. +8. **Gantt charts** — do not use color styling. + +## Step 4: Garbage in, garbage out + +The quality of the generated diagram is bounded by the quality of the Mermaid you produce, which is bounded by the context you have. Before writing Mermaid, make sure you have enough real information to describe the subject accurately — and use whatever the current environment gives you to gather it. + +Depending on what's available, useful sources of context include: + +- **Source code** — grep/read the relevant files so the diagram reflects real service names, real edge labels, real data stores, real entry points. Walking actual routes/handlers/consumers beats recreating from memory. +- **User-provided documents** — a PRD, spec, meeting notes, transcript, research synthesis, onboarding doc, process write-up. Ask the user to paste or attach it if the subject isn't code. +- **Existing Figma or FigJam files** — if the new diagram should align with one the user already has, read it with `get_figjam` or `get_design_context` (see the `figma-use` and `figma-use-figjam` skills). +- **Other MCP servers or tools you have available** — issue trackers, docs sites, CRMs, analytics, internal wikis, design systems, database schemas, etc. If a connected tool holds the ground truth for what you're diagramming, pull from it rather than guessing. +- **The user themselves** — when the description is thin or ambiguous (unclear direction of flow, unclear scope, unclear which entities matter), ask one or two focused questions before generating. Examples: "What are the 3–5 main steps?", "Who owns each step?", "What triggers the next step?". One good question beats one wasted diagram. + +Don't invent edges, labels, or entities to "round out" a diagram. Missing information is better than hallucinated information — leave a gap and flag it to the user. + +## Step 5: Will the diagram need more than Mermaid can express? + +Mermaid can't do everything. Sticky-note annotations tied to specific nodes, per-node domain coloring on ERDs, callouts with attached data — these all require composing `generate_diagram` with `use_figma` (via the [figma-use-figjam](../figma-use-figjam/SKILL.md) skill). This is the **hybrid workflow**. + +It's a judgment call, not a default. Deploy it when the user's ask clearly benefits — skip it when the base diagram is obviously enough. Signals that say yes: user explicitly asked for notes, colors, callouts, or "X attached to each node"; they shared data that maps to specific nodes; the diagram is a shareable artifact, not a thinking sketch. Signals that say no: short/self-explanatory request, small diagram, user exploring or testing. + +**If hybrid is warranted, read [references/workflow.md](./references/workflow.md) before calling `generate_diagram`** — it covers the pattern, two core recipes (annotations + color-coding), communication style, and failure handling. If not, proceed directly to Step 6. + +## Step 6: Calling the tool + +Required: +- `name`: a descriptive title (shown to the user) +- `mermaidSyntax`: the Mermaid source + +Optional: +- `userIntent`: a short sentence describing what the user is trying to accomplish — helps telemetry and downstream tuning +- `useArchitectureLayoutCode`: **only for architecture diagrams**; value is specified in `references/architecture.md` +- `fileKey`: if the user wants the diagram added to an existing FigJam file instead of a new one + +Do **not** call `create_new_file` before `generate_diagram` — the tool creates its own file. + +## Step 7: After generation + +- The tool returns a link (or widget) the user can click to open the diagram in FigJam. Show it as a markdown link unless the client renders an inline widget. +- If extensions are warranted (see Step 5), compose with `use_figma` now — the pattern and recipes are in [references/workflow.md](./references/workflow.md). +- If the user is dissatisfied after 2 attempts at the same diagram, stop regenerating. Ask what specifically is wrong, or suggest they open it in Figma and edit manually rather than burning more tool calls. + +### Reuse the same file when iterating or adding related diagrams + +Every call to `generate_diagram` without a `fileKey` creates a new FigJam file in the user's drafts. Regenerating 4 times = 4 draft files to clean up. Prefer reusing the existing file when: + +- The user is iterating on the same diagram ("try again with…", "change the labels…"). +- The user wants a follow-up diagram that lives alongside the first (e.g. a sequence diagram next to a flowchart of the same system). + +How to reuse: + +1. **Pass `fileKey`** on subsequent `generate_diagram` calls. Extract from a `figma.com/board/{fileKey}/...` URL. The diagram is added to the existing file rather than creating a new one. +2. If you want to replace the previous diagram rather than adding next to it, use the `use_figma` tool (see the `figma-use-figjam` skill) to delete the old diagram's nodes first, then call `generate_diagram` with the same `fileKey`. Or leave the old diagram and place the new one beside it — readers often benefit from seeing the history of attempts. + +Ask the user which they prefer the first time you iterate — "regenerate over the old one, or keep both side-by-side?" — and remember their answer for subsequent iterations in the session. diff --git a/plugins/figma/skills/figma-generate-diagram/agents/openai.yaml b/plugins/figma/skills/figma-generate-diagram/agents/openai.yaml new file mode 100644 index 00000000..adcdc046 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Generate Diagram" + short_description: "Create editable FigJam diagrams from Mermaid" + default_prompt: "Generate an editable FigJam diagram from this request, choosing the right Mermaid diagram type and syntax." diff --git a/plugins/figma/skills/figma-generate-diagram/references/architecture.md b/plugins/figma/skills/figma-generate-diagram/references/architecture.md new file mode 100644 index 00000000..f85498d2 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/architecture.md @@ -0,0 +1,149 @@ +# Architecture Diagrams + +Use this reference when the user asks for a **software architecture diagram** — a view showing services, datastores, message queues, external integrations, and how they connect. These are flowcharts rendered by a bespoke grid-based layout (not ELK), controlled by the `useArchitectureLayoutCode` parameter on `generate_diagram`. + +For generic flowcharts (decision trees, process flows, dependency graphs), use [flowchart.md](./flowchart.md) instead. + +--- + +## Rules (read all before writing any Mermaid) + +1. Always `flowchart LR` (left-to-right). +2. Every node = one independently deployable unit. Never decompose a service into internal modules. +3. **Every node MUST be inside a subgraph.** No `classDef`, `class`, or `style` statements needed. +4. **Subgraph IDs must be EXACTLY one of:** `client`, `gateway`, `service`, `datastore`, `external`, `async`. Use display labels for titles: `subgraph service ["Core Services"]`. **WRONG:** `subgraph Services`. **RIGHT:** `subgraph service`. +5. Core flow edges use `-->` or `<-->` and must form a DAG across client -> gateway -> service -> datastore (no cycles) or the tool will error. +6. Bidirectional edges must be written in forward direction: `client <-->|"WS"| gateway` not `gateway <-->|"WS"| client`. Bidirectional async (`<-.->`) is not supported and will fall back to a forward edge. +7. **Backward edges** use `<---`: write left node first, arrow points left. `orderService <---|"Refund"| paymentService`. +8. **ALL edges touching an async or external node MUST use dotted syntax (`-.->`)** for both directions. Never use `==>`. +9. Never connect edges to subgraph IDs — only individual node IDs. +10. Never create two edges between the same pair of nodes. Combine into one edge with a merged label. +11. **Never connect datastore or async nodes to each other directly.** A service always mediates. WRONG: `kafka -.-> sqs`. RIGHT: `worker -.->|"Consumes"| kafka` then `worker -.->|"Produces"| sqs`. + +## Subgraph Categories + +Layout order: client -> gateway -> service -> datastore. External and async are placed above/below the service+datastore lanes. Colors and shapes are auto-assigned — use plain `[text]` syntax for all nodes. + +| Subgraph ID | What Belongs Here | +|---|---| +| `client` | Web/mobile/desktop apps, CLI, end users | +| `gateway` | CDN, load balancer, API gateway, reverse proxy | +| `service` | Microservices, monoliths, serverless, ETL, async workers, cron jobs | +| `datastore` | Databases, caches, object storage (PostgreSQL, Redis, S3, Elasticsearch) | +| `external` | Feature flags, monitoring, payment, OAuth, third-party SaaS | +| `async` | Message infrastructure: Kafka, RabbitMQ, SQS, Pub/Sub, EventBridge, Redis Streams | + +## Async Subgraph + +**Async nodes = independently deployable message infrastructure.** + +Does NOT belong in `async`: +- Consumer workers -> `service` +- DB replication features (WAL, CDC) -> omit or use a dotted edge label from `datastore` +- Logical splits of a single broker -> use one node + +Canonical pattern: `service -.->|"Produce"| queue` and `queue -.->|"Consume"| service` + +## Node Granularity + +"Can I deploy, restart, or scale this independently?" Yes = node. No = omit. + +## Edge Types + +| Category | Syntax | Use For | +|----------|--------|---------| +| **Forward** | `-->` | Normal left-to-right data flow | +| **Bidirectional** | `<-->` | WebSocket, gRPC streaming (write in forward direction) | +| **Backward** | `<---` | Return flows, invalidation (left node first) | +| **Async/External** | `-.->` | Any edge touching async or external nodes | + +### Edge Decision + +1. Either endpoint is async or external? -> `-.->` +2. Real-time bidirectional channel? -> `<-->` +3. Backward-flowing between core nodes? -> `<---` (left node first) +4. Otherwise -> `-->` + +### Key Patterns + +| Pattern | Syntax | Label Examples | +|---------|--------|---------------| +| client -> gateway | `-->` | "HTTPS" | +| service -> datastore | `-->` | "Read/Write", "Query" | +| service -> async | `-.->` | "Produce Events" | +| async -> service | `-.->` | "Consume", "Fan Out" | +| service -> external | `-.->` | "ServiceName: Purpose" | +| service <- service | `<---` | "Invalidate", "Refund" | + +> **External edges** render to the section boundary. Include the service name in the label: `"ServiceName: Purpose"`. + +### Best Practices + +1. **One flow per diagram.** Focus on the architecture the user asked about. +2. **Max 15-20 edges.** Omit edges unrelated to the requested flow. +3. **Label every cross-subgraph edge.** Use a verb from the source node's perspective, with specifics when relevant (e.g., "Reads Users", "Writes Orders", "Produces"). 1-4 words max. +4. **Bidirectional = one `<-->` edge.** Never split into separate `-->` and `-.->`. + +## Validation Checklist + +Before finalizing the diagram, verify: +1. **Forward and bidirectional edges must form a DAG.** Any edges that would form a cycle must be represented as a backward edge. +2. **Every service has both input and output.** For each service node, ask: "Where does it get data from?" and "Where does it return data to?" If either answer is missing, add the edge. +3. **Walk each service node one by one.** List every service node, then for each one confirm it has at least one incoming edge and one outgoing edge. If not, fix it before calling generate_diagram. +4. **Do not hallucinate labels or edges.** Ask the user questions when there is ambiguity. + +## Mermaid Syntax Rules + +1. Node IDs: camelCase, no spaces or underscores (`userService` not `user service` or `user_service`). The layout code splits on `_` internally, so underscores in IDs will break edge routing. +2. Labels with special chars: wrap in double quotes (`A["Process (main)"]`). +3. Edge labels with special chars: wrap in quotes (`-->|"O(1) lookup"|`). +4. Avoid reserved words as node IDs: `end`, `subgraph`, `graph`. +5. No HTML tags or emojis in labels. + +## Complete Example + +```mermaid +flowchart LR + subgraph client ["Client Apps"] + web[Web App] + mobile[Mobile App] + end + subgraph gateway ["API Layer"] + alb[Load Balancer] + end + subgraph service ["Core Services"] + auth[Auth Service] + orders[Order Service] + notify[Notification Service] + end + subgraph datastore ["Data Stores"] + pg[PostgreSQL] + redis[Redis] + end + subgraph external ["External"] + stripe[Stripe] + end + subgraph async ["Event Streaming"] + orderQ[Order Queue] + end + + web -->|"HTTPS"| alb + mobile -->|"HTTPS"| alb + web <-->|"WebSocket"| alb + alb -->|"Routes /auth"| auth + alb -->|"Routes /orders"| orders + auth -->|"Reads Sessions"| redis + orders -->|"Writes Orders"| pg + orders -.->|"Produces"| orderQ + notify -.->|"Consumes"| orderQ + orders -.->|"Stripe: Charges"| stripe +``` + +## Calling generate_diagram + +When calling `generate_diagram` for an architecture diagram, pass: + +- `name`: A descriptive diagram name +- `mermaidSyntax`: Your Mermaid syntax following all rules above +- `useArchitectureLayoutCode`: `"FIGMA_DIAGRAM_2026"` +- `userIntent` (optional): What the user is trying to accomplish diff --git a/plugins/figma/skills/figma-generate-diagram/references/erd.md b/plugins/figma/skills/figma-generate-diagram/references/erd.md new file mode 100644 index 00000000..b65150d5 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/erd.md @@ -0,0 +1,313 @@ +# Entity-Relationship Diagrams + +Use this reference for **ER diagrams** — data models showing entities (tables), their attributes (columns), and the relationships (foreign keys, cardinalities) between them. + +Typical subjects: database schemas, domain models, API resource graphs, data-lake structures, any diagram where the important thing is "these entities relate to each other in these ways, with these fields." + +If the subject is a static architecture of services (not data) → architecture flowchart. If it's a state machine → state diagram. + +--- + +## 1. When to use an ER diagram + +Good fits: + +- **Database schemas** — tables + columns + foreign keys +- **Domain models** — user/account/subscription/order style relationships +- **API resource graphs** — which resources reference which +- **Normalized data-model documentation** — explaining 1:N, N:M relationships +- **Reverse-engineered data models** — extracted from an existing DB for a design review + +Bad fits (route to a different diagram type): + +- Services, queues, datastores + how they connect → architecture flowchart +- Process flow / decisions → flowchart +- Timeline or schedule → gantt +- Interactions over time → sequence diagram +- Entity lifecycle (one entity's states) → state diagram + +## 2. Required skeleton + +``` +erDiagram + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE_ITEM : contains + + CUSTOMER { + string name + string email + } + ORDER { + string orderNumber + date placedAt + } + LINE_ITEM { + string productId + int quantity + } +``` + +Every chart needs: the `erDiagram` keyword and at least one entity. Relationships are optional but usually the whole point. + +## 3. Entities + +Three declaration forms: + +### Simple — just a name + +``` +USER +``` + +Renders as a plain rectangle (no attributes). + +### With attributes — renders as a table + +``` +USER { + string name + string email + int age +} +``` + +When an entity has attributes, it renders as a **table** in FigJam: header row with the entity name, body rows for each attribute. Entities with zero attributes render as plain rectangles. + +Mixing both is fine — some entities as tables with fields, others as rectangles for high-level concepts you haven't detailed yet. + +### With a display alias + +``` +USER["User Account"] +``` + +The bracket-quoted alias is what renders; the left identifier (`USER`) is the reference used in relationships. + +**Gotcha — don't use aliases in relationship lines.** This fails to parse: + +``` +// DOESN'T WORK +USER["User Account"] ||--o{ ORDER["Order"] : places +``` + +Declare aliases in a separate entity block, then reference by plain ID in relationships: + +``` +// WORKS +USER["User Account"] { + string email +} +ORDER["Order"] { + string number +} +USER ||--o{ ORDER : places +``` + +## 4. Attributes + +Format: `type name [keys] ["comment"]` + +``` +USER { + string id PK + string email UK + string teamId FK + string avatarUrl "Full URL to Gravatar" + int loginCount + string refCode PK, UK "Primary + unique alt" +} +``` + +### Types + +Free-form — Mermaid doesn't enforce a type system. Common choices: `string`, `int`, `float`, `decimal`, `bool`, `date`, `datetime`, `uuid`, `json`, `text`. Pick a vocabulary and stick with it across one diagram. + +### Keys + +- `PK` — primary key +- `FK` — foreign key +- `UK` — unique key +- Multiple keys per attribute with commas: `PK, FK` or `PK, UK` + +Keys render as small badges next to the attribute name in the table. + +### Comments + +Optional, wrapped in double quotes at the end of the attribute line. Useful for a short clarifier ("Nullable", "Soft-deleted", "Index on created_at"). + +## 5. Relationships + +Format: `ENTITY_A ENTITY_B : label` + +``` +CUSTOMER ||--o{ ORDER : places +``` + +- Left entity (`CUSTOMER`) is the "A side"; right (`ORDER`) is the "B side" +- Label is a short verb phrase from A's perspective ("places", "owns", "has") + +### Cardinality pairs + +Each side of the pair describes how many of THAT side participate. The symbol nearer to an entity describes its own cardinality. + +| Pair | Meaning | Example use | +| ------------ | ---------------------------- | -------------------------------------------- | +| `\|\|--\|\|` | Exactly one ↔ exactly one | 1:1 mandatory (user ↔ profile) | +| `\|\|--o\|` | Exactly one ↔ zero or one | Optional 1:1 | +| `\|\|--o{` | Exactly one ↔ zero or more | Classic 1:N (user → posts) | +| `\|\|--\|{` | Exactly one ↔ one or more | 1:N with required child (order → line items) | +| `}o--o{` | Zero or more ↔ zero or more | Optional N:M | +| `}\|--\|{` | One or more ↔ one or more | N:M both required | + +### Identifying vs non-identifying + +- **`--`** (double dash) = **identifying** relationship — renders as a **solid line**. Use when the child cannot exist without the parent. +- **`..`** (double dot) = **non-identifying** — renders as a **dotted line**. Use for weaker/optional relationships. + +``` +ORDER ||--|{ LINE_ITEM : contains // solid — line items require an order +ORDER }|..|{ PROMO_CODE : applied_with // dotted — many-to-many, optional +``` + +### Labels + +Short verb or verb phrase from the left entity's perspective: `places`, `owns`, `contains`, `references`. 1–3 words. Drop articles. Unlabeled relationships are allowed but discouraged — the label is what gives the diagram meaning. + +## 6. Direction + +Optional: + +``` +erDiagram + direction LR // or TB, BT, RL + ... +``` + +`LR` (left-to-right) works well for most schemas with 4–8 entities; `TB` suits taller hierarchies. Omit to let Mermaid default. + +## 7. What's NOT supported + +- **Styling** — `classDef`, `class Foo styleName`, `:::styleName` inline, and `style EntityId fill:#hex,stroke:#hex`. Silently dropped (no color applied). Unlike state diagrams, styling statements don't create phantom entities — they just have no effect. Color-code entities via `use_figma` post-generation instead (§9). +- **Inheritance / subtype relationships** — Mermaid has no native syntax; model the parent-child relationship as a normal 1:1 and annotate in the label. +- **Notes** — no `note` construct in ERDs. Add callouts via `use_figma`. +- **Aliases in relationship lines** — see §3 gotcha. Declare entities with aliases first, then reference by ID. + +## 8. Layout (same ELK as flowcharts) + +ER diagrams render via the **same ELK layered layout** as flowcharts. The principles from [flowchart.md §5 (ELK survival guide)](./flowchart.md#5-elk-survival-guide) apply: + +- **Simple cycles render fine** — circular FKs (user → team → users) don't need workarounds at small scale. +- **Pain scales with size** — 20+ entities in one diagram starts to crowd. Split into sub-domains (one diagram per bounded context). +- **Tables with many attributes stretch vertically** — affecting the whole layout. Trim to the important 5–10 columns. + +## 9. Hybrid workflow: `generate_diagram` first, then `use_figma` + +`generate_diagram` produces a clean, laid-out ER schema — tables with attributes, cardinality caps, connected relationships. Most of what our renderer doesn't support (color-coded entities, notes, phase/domain highlighting, annotations on specific columns) can be added on top with `use_figma`. + +**Default workflow when the schema needs more than bare tables + relationships:** + +1. **Scaffold with `generate_diagram`** — entities (with or without attributes), relationships, cardinalities, labels. Skip the features that get dropped (styling). +2. **Extend with `use_figma`** — open the same file (via `fileKey`) and add: + - Sticky notes or text blocks for **annotations** on specific entities, columns, or relationships + - Background rectangles behind **domain groupings** (auth entities vs. billing entities vs. content entities) + - **Color-coding** entities by category (core / lookup / junction / audit) using replacement shapes or rectangles layered behind the tables + - **Sequence numbers** or badges for migration order, deprecation status, etc. + +Loading [figma-use](../../figma-use/SKILL.md) and [figma-use-figjam](../../figma-use-figjam/SKILL.md) covers how to make those edits. + +### Signals the request needs the hybrid workflow + +- The user uses words like "color-code", "group by domain", "highlight deprecated", "annotate this column", "separate the core tables from the audit tables". +- The user wants visual distinction between entity roles (fact tables vs dimensions, read-only vs mutable, soft-deleted vs archived). +- The user wants to combine the schema with surrounding narrative, migration notes, or decision log on the same board. + +### When to skip `generate_diagram` entirely + +Only if the baseline layout isn't useful — e.g. the user wants a **radial schema diagram**, a **Visio-style database map**, or a **heavily-stylized slide visual**. In those cases, go straight to `use_figma`. + +### Be pragmatic, not performative + +Scaffold first, extend directly if the user's request is specific; otherwise scaffold and ask one follow-up: "I've set up the schema — want me to color-code the domains / add notes on the soft-delete columns / group the audit tables behind a highlight?" + +## 10. Best practices + +1. **Start with relationships, then fill in attributes**. Getting the cardinalities right is more important than listing every column. +2. **Trim attribute lists**. 5–10 columns per entity is the sweet spot. Full schemas belong in migration files or the DDL, not the diagram. +3. **Mark keys consistently** — always `PK` for primary keys, `FK` for foreign keys. It's the most common reader question. +4. **Use identifying (`--`) vs non-identifying (`..`) deliberately** — solid for required parent/child, dotted for optional/weak. Don't default one when the other is more truthful. +5. **One diagram per bounded context**. A full 40-entity schema is unreadable; draw auth, billing, content, etc. as separate diagrams and link them with a short label when they share an entity. +6. **Label every relationship**. `places`, `owns`, `belongs to` — the label is what makes an ERD communicate, not just a pile of boxes. +7. **Use aliases for display-friendly names** when entity IDs are SQL-style (`user_acct` → alias `"User Account"`). But remember §3 — declare aliased entities separately, reference by ID in relationships. + +## 11. Validation checklist + +Before calling `generate_diagram`: + +1. `erDiagram` keyword on line 1. +2. Every relationship uses a valid cardinality pair (§5 table) and either `--` or `..`. +3. Entities referenced in relationships are declared (with or without attributes, or implicit via the relationship line itself — but not with a bracket alias). +4. **No alias syntax in relationship lines** — aliases must be on the entity declaration only (§3 gotcha). +5. Attribute types are internally consistent (don't mix `string`/`String`/`VARCHAR` across the same diagram). +6. Keys are marked consistently — `PK`, `FK`, `UK`, or comma-combined. +7. No `classDef`, `class`, `:::`, or `style` lines (dropped — §7). +8. Under ~15–20 entities, or split by domain. + +## 12. Complete example + +A small e-commerce schema with 1:1, 1:N, and N:M relationships, identifying vs non-identifying links, keys, comments, and a bracket-aliased entity: + +```mermaid +erDiagram + CUSTOMER ||--|| PROFILE : has + CUSTOMER ||--o{ ORDER : places + ORDER ||--|{ LINE_ITEM : contains + ORDER }|..|{ PROMO_CODE : applied_with + PRODUCT ||--o{ LINE_ITEM : listed_in + + CUSTOMER { + string id PK + string email UK + string name + datetime createdAt + } + PROFILE { + string customerId PK, FK + string avatarUrl "Gravatar URL" + string bio + } + ORDER { + string id PK + string customerId FK + decimal total + string status "pending | paid | shipped | delivered" + datetime placedAt + } + LINE_ITEM { + string id PK + string orderId FK + string productId FK + int quantity + decimal unitPrice + } + PRODUCT { + string id PK + string sku UK + string name + decimal price + } + PROMO_CODE["Promo Code"] { + string code PK + decimal discount + date expiresAt + } +``` + +## 13. Calling generate_diagram + +Pass: + +- `name` — a descriptive diagram name +- `mermaidSyntax` — your ER-diagram source +- `userIntent` — what the user is trying to accomplish + +Do **not** pass `useArchitectureLayoutCode` — that's architecture-diagram only. diff --git a/plugins/figma/skills/figma-generate-diagram/references/flowchart.md b/plugins/figma/skills/figma-generate-diagram/references/flowchart.md new file mode 100644 index 00000000..0d780119 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/flowchart.md @@ -0,0 +1,392 @@ +# Flowcharts (non-architecture) + +Use this reference for **generic flowcharts** — decision trees, process flows, pipelines, dependency graphs, user journeys, anything that is not a software architecture diagram (those go to [architecture.md](./architecture.md)). + +These diagrams render via ELK (Eclipse Layout Kernel) with an orthogonal, layered layout. The rules below are tuned to produce diagrams that read well, use FigJam's shape vocabulary, and avoid the layout traps ELK struggles with. + +--- + +## 1. Direction: pick once, up front + +- `flowchart LR` — **default**. Best for sequential processes, pipelines, dependency chains, most 2–3 level decision trees. +- `flowchart TD` (or `TB`) — switch when you have hierarchies, taxonomies, deep narrow trees, or many sibling nodes at the same level (keeps width manageable). + +Never change direction mid-diagram. Pick before writing. + +## 2. Shapes: use the vocabulary, don't over-decorate + +Mermaid exposes dozens of shape names; most silently fall back to a plain rectangle in FigJam. The table below lists **only the shapes that render as a distinct FigJam shape** — prefer these when they carry meaning. + +| Mermaid short form | `@{shape: ...}` form | Renders as | Use for | +| ------------------ | ------------------------- | --------------------------- | -------------------------------- | +| `A[Text]` | `shape: rect` / `square` | Rectangle | Generic process / step (default) | +| `A(Text)` | `shape: rounded` | Rounded rectangle | Softer step, grouped process | +| `A([Text])` | `shape: stadium` | Rounded rectangle (stadium) | Start / end of a flow | +| `A((Text))` | `shape: circle` | Ellipse | Entry/exit points, events | +| `A{Text}` | `shape: diamond` | Diamond | **Decisions** (yes/no, branch) | +| `A{{Text}}` | `shape: hexagon` / `hex` | Hexagon | Preparation, setup, handoff | +| `A[[Text]]` | `shape: subroutine` | Predefined process | Called function / sub-procedure | +| `A[(Text)]` | `shape: cylinder` / `cyl` | Database | Any datastore (DB, cache, store) | +| `A[/Text/]` | `shape: lean-r` | Parallelogram right | Input | +| `A[\Text\]` | `shape: lean-l` | Parallelogram left | Output | +| `A[/Text\]` | `shape: trap-t` | Trapezoid | Manual operation | +| `A[\Text/]` | `shape: trap-b` | Trapezoid | Manual operation (inverse) | +| `A>Text]` | `shape: odd` | Chevron | Tag, marker, flag | +| — | `shape: doc` | Document | File, report, artifact | +| — | `shape: docs` | Documents (multiple) | Collection of files | +| — | `shape: tri` | Triangle (up) | Hierarchy root, warning | +| — | `shape: flip-tri` | Triangle (down) | Inverse hierarchy | +| — | `shape: notch-pent` | Pentagon | Milestone, status | +| — | `shape: comment` | Speech bubble | Annotation | +| — | `shape: cross-circ` | Summing junction | Merge / combine | + +**Shapes the parser accepts but that render as plain rectangles** — don't bother using these for visual distinction: `text`, `notch-rect`, `lin-rect`, `fork`, `hourglass`, `brace-r`, `braces`, `bolt`, `delay`, `das`, `curv-trap`, `div-rect`, `win-pane`, `sl-rect`, `processes`, `flag`, `bow-rect`, `tag-rect`, `subproc`. + +### The "shape carries the label" principle + +Don't repeat a shape's semantics in its text: + +- BAD: `db[(Database: PostgreSQL)]` — the cylinder already says "database" +- GOOD: `db[(PostgreSQL)]` +- BAD: `d{Decision: user authenticated?}` — the diamond already says "decision" +- GOOD: `d{User authenticated?}` + +All-rectangles is boring but often correct. All-decorative-shapes is worse — it turns the diagram into shape soup and distracts from flow. + +## 3. Edges: strokes, end caps, labels + +### Strokes + +| Syntax | FigJam stroke | Use for | +| ---------- | ------------- | --------------------------------------------- | +| `A --> B` | Normal | Default data/control flow | +| `A -.-> B` | Dotted | Async, conditional, optional, "happens later" | +| `A ==> B` | Thick | Critical path / emphasized flow | + +### End caps + +| Syntax | End cap | Use | +| --------- | ------- | --------------------------- | +| `A --> B` | Arrow | Default | +| `A --o B` | Circle | Composition, "ends at" | +| `A --x B` | Cross | Termination, error, blocked | +| `A --- B` | None | Plain association | + +### Both-ended + +| Syntax | Meaning | +| ---------- | ------------------------------------------ | +| `A <--> B` | Bidirectional (write in forward direction) | +| `A o--o B` | Both circles | +| `A x--x B` | Both crosses | + +### Labels + +Syntax: `A -->|"label text"| B`. Wrap in quotes when there are special chars. + +Label rules: + +- 1–4 words, from the **source's** perspective (action verbs: "Writes", "Validates", "Returns 401") +- No trailing periods +- No emojis (tool rejects) +- Don't label the obvious — unlabeled edges are fine when the flow is clear + +### Backward edges + +ELK lays out left-to-right (or top-to-bottom). A "backward" edge forces ELK to bend around existing nodes and usually looks messy. Two options: + +1. Reverse the syntax: `B <-- A` (left node first, arrow points left). ELK still bends, but at least labels correctly. +2. **Preferred when the back-reference is to a shared node**: duplicate the target (see §5). + +### Chaining fan-out and fan-in + +Mermaid accepts an `&` shortcut that's less verbose than listing each edge: + +``` +A --> B & C & D // one-to-many +A & B & C --> sink // many-to-one +A & B --> C & D // many-to-many +``` + +Use this when the list is short and the intent is obvious. For larger or labeled groups, the explicit form reads better. + +### Comments + +`%% comment text` — parsed as a comment, ignored by the renderer. Useful in longer diagrams to label sections of Mermaid for the next agent/reader that touches the file. Do not overuse, since the user won't usually directly read the mermaid you write. + +## 4. Subgraphs: group related nodes + +Subgraphs are labeled containers. Syntax: + +``` +subgraph api ["API Layer"] + auth[Auth] + users[Users] +end +``` + +### When to use + +- Clear logical boundary (subsystem, phase, team ownership) +- 3+ related nodes that share an input or output +- Don't subgraph a pair — not worth the visual weight + +### Cross-subgraph edges + +Work cleanly thanks to `elk.hierarchyHandling: INCLUDE_CHILDREN`. Connect a node inside one subgraph to a node inside another, or to a top-level node — ELK keeps routing orthogonal. + +Always connect **node-to-node**. Connecting to a subgraph ID (`api --> db`) works but routes unpredictably; connect to a specific node inside instead. + +### Nested subgraphs + +Supported. Keep nesting to 2 levels max — deeper nesting crowds labels and confuses ELK's spacing. + +### Style subgraphs so they stand out + +FigJam's canvas is near-white. Unstyled subgraphs show only a thin outline and can blend into the background, especially with a dotted grid. Apply a light fill to each subgraph so boundaries read at a glance: + +``` +style tier1 fill:#FFECBD,stroke:#FFC943 +style tier2 fill:#C2E5FF,stroke:#3DADFF +style eng fill:#DCCCFF,stroke:#874FFF +``` + +Pick soft tints — not saturated colors. The [FigJam built-in palette](#6-colors-use-sparingly-semantically) light fills work well. When a diagram has multiple subgraphs, give each a different tint; when it has one, a neutral `#F5F5F5` fill with a darker stroke is usually enough. + +Don't style subgraphs so heavily that they overpower the nodes inside them — subgraphs are containers, not the content. + +### Per-subgraph direction + +Override the parent's direction inside a subgraph when one cluster reads differently: + +``` +flowchart LR + subgraph phases ["Phases"] + direction TB + p1[Phase 1] --> p2[Phase 2] --> p3[Phase 3] + end +``` + +Use sparingly — mixed directions can disorient the reader. + +## 5. ELK survival guide + +ELK is more capable than you'd think. Small and medium diagrams render well out of the box — a linear pipeline with a loopback, a handful of services fanning into one, a long retry cycle, a few cross-subgraph edges — none of these need special care. **Don't pre-emptively contort the Mermaid** to avoid these patterns. + +The guidance below is for the minority of cases where a diagram has visibly crossing edges, long horseshoe bends, or crowded subgraphs. Reach for it when something specific looks bad, not by default. Also note: not every visual oddity is ELK's fault — the FigJam renderer occasionally reparents coordinates in ways that override ELK's bend points. If a diagram looks slightly off, don't assume your Mermaid is wrong. + +### Cycles + +Draw cycles when they reflect the real flow. A single cycle — even one that spans the full length of the diagram back to the start — typically renders fine. Retry loops, state-machine transitions, reopen-ticket flows: don't avoid them. + +The only time cycles start to hurt is when **many** cycles are tangled through the same nodes in an already-dense region. If that's happening, split the diagram or duplicate a shared node (below) — otherwise leave cycles alone. + +### Duplicate shared nodes when fan-in becomes a problem + +A shared node with up to ~5 inbound edges renders cleanly — ELK fans in without drama, even across subgraphs. **Don't pre-emptively duplicate.** + +Duplication starts to earn its keep when: + +- **Roughly 6+ inbound edges** converge on a single node — past this, arrows start stacking at the target and readability drops. +- The shared node sits many layers away from some of its sources, producing long crossings across other important content. +- The inbound edges visually cut across other subgraphs or flows in a way that obscures them. + +Pattern — only apply when fan-in is _actually_ causing a rendering problem: + +``` +// Before — one shared Logger with many inbound edges crowding the target +a --> logger +b --> logger +... (6+ sources) +f --> logger +g --> logger + +// After — duplicated inline per source +a --> aLog[Logger] +b --> bLog[Logger] +... (one Logger per source) +f --> fLog[Logger] +g --> gLog[Logger] +``` + +The reader sees "Logger" repeated and understands it's one shared concept. Readability beats node-count minimization — but only when readability is actually suffering. + +### Balance your layers + +If one layer has 20 nodes and neighboring layers have 2 each, ELK spreads the wide layer horizontally and the diagram becomes a thin strip. Either split the diagram or re-cluster into subgraphs. + +### Avoid empty or trivial subgraphs + +A subgraph with one child wastes space. A subgraph with no internal structure adds noise. Use subgraphs only when they clarify boundaries. + +### Self-loops + +Supported (`a --> a`). Leave headroom; tight grids of self-loops render awkwardly. + +### Iterating when something looks off + +If a first render comes back cluttered in a specific area (crossing edges around one node, a long horseshoe cycle, a cramped subgraph), the usual fixes in priority order: + +1. **Split the diagram** — is this really one diagram, or two? +2. **Duplicate the most-referenced node** in the cluttered area. +3. **Introduce or tighten a subgraph** to cluster the nodes involved. +4. **Flip direction** (LR ↔ TD) if the aspect ratio is fighting the content. + +## 6. Colors: use sparingly, semantically + +Syntax: + +``` +style A fill:#E6F4EA,stroke:#137333 +``` + +Or via classDef: + +``` +classDef warn fill:#FCE8E6,stroke:#C5221F +class A,B warn +``` + +Only `fill` and `stroke` are applied. Other CSS-like properties (font size, stroke-width, stroke-dasharray) are ignored. Keep it to fills and strokes. + +**Use color to encode meaning**, not for decoration: + +- Status (green = success, red = error, amber = warning) +- Ownership (team A vs team B) +- Subsystem grouping when subgraphs aren't appropriate + +**Don't**: + +- Paint every node +- Use bright saturated palettes — FigJam's canvas is neutral and bright fills fight with it +- Rely on color alone for meaning (shape + label should still read without color) + +Prefer soft fills with darker matching strokes. The table below is the **FigJam built-in color palette** — these hex pairs match FigJam's native shape presets, so diagrams using them feel consistent with the canvas and with other FigJam content. The agent is free to pick any hex, but these are strong defaults. + +**Light fills (use dark text — `#1E1E1E`):** + +| Name | Fill | Stroke | Typical use | +| ------------ | --------- | --------- | -------------------------- | +| Light green | `#CDF4D3` | `#66D575` | Success, completed, go | +| Light teal | `#C6FAF6` | `#5AD8CC` | Secondary success, info | +| Light blue | `#C2E5FF` | `#3DADFF` | Neutral highlight, focus | +| Light violet | `#DCCCFF` | `#874FFF` | Special / callout | +| Light pink | `#FFC2EC` | `#F849C1` | Accent, creative | +| Light red | `#FFCDC2` | `#FF7556` | Error, blocked, critical | +| Light orange | `#FFE0C2` | `#FF9E42` | Warning, attention | +| Light yellow | `#FFECBD` | `#FFC943` | Caution, pending | +| Light gray | `#D9D9D9` | `#B3B3B3` | Muted, deprecated, context | + +**Saturated fills (paired with darker stroke — use for strong emphasis; FigJam uses white text on these, but Mermaid can't set text color, so prefer light fills when labels are dense):** + +| Name | Fill | Stroke | +| ------ | --------- | --------- | +| Green | `#66D575` | `#3E9B4B` | +| Blue | `#3DADFF` | `#007AD2` | +| Red | `#FF7556` | `#DC3009` | +| Orange | `#FF9E42` | `#EB7500` | +| Yellow | `#FFC943` | `#E8A302` | + +Example: + +``` +style ok fill:#CDF4D3,stroke:#66D575 +style broken fill:#FFCDC2,stroke:#FF7556 +style pending fill:#FFECBD,stroke:#FFC943 +``` + +## 7. Text quality + +- **Node labels**: 1–4 words, a noun phrase or short imperative +- **Edge labels**: 1–4 words, verb from the source's perspective +- No trailing periods +- No emojis (tool rejects) +- No HTML tags +- Don't use `\n` in labels — omit line breaks unless absolutely necessary; ELK sizes shapes based on label and long labels stretch them awkwardly +- Node IDs: camelCase (`userService`). Underscores can break edge routing. +- Avoid `end`, `subgraph`, `graph` as node IDs (reserved) +- Labels with special chars (parens, colons, slashes): wrap in quotes — `A["Process (main)"]`, `-->|"O(1) lookup"|` + +## 8. Density and when to split + +Soft caps: + +- Up to ~20 nodes — usually fine +- 20–30 — consider introducing subgraphs +- 30+ — split into multiple diagrams + +Reasons to split into multiple diagrams: + +- Multiple phases that don't interact (one diagram per phase) +- Different audiences (ops view vs. user view of the same system) +- Different scenarios through the same system (happy path vs. error path) + +`generate_diagram` can be called repeatedly; multiple diagrams in a single FigJam file is a legitimate pattern. Name them distinctly. + +## 9. Validation checklist (before calling the tool) + +1. **Cycle check**: cycles exist only where they genuinely represent the flow. A single cycle or a couple of short retry loops are fine at any size. Multiple cycles tangled through shared nodes, especially inside an already-dense region, warrant splitting the diagram or duplicating a shared node. +2. **No orphans**: every node has at least one incoming or outgoing edge (excepting clear start/end nodes). +3. **Every process has input and output**: walk each node; if it's missing either, either the edge is missing or the node shouldn't be there. +4. **Label audit**: shape doesn't repeat in the label; edge labels from source side; under 4 words; no periods/emojis. +5. **Shape audit**: each non-rectangle shape earns its distinctness. Default to rectangle when unsure. +6. **Color audit**: color encodes meaning, or there's no color. Not every node is colored. +7. **Subgraph audit**: each subgraph has 3+ children with a clear shared boundary; each subgraph has a light tint applied via `style` so it stands out from the FigJam canvas. +8. **Density check**: ≤ ~25 nodes or the diagram is split. + +## 10. Complete example + +A CI/CD pipeline with decisions, a datastore, a subgraph, and semantic color: + +```mermaid +flowchart LR + dev[/Developer commit/] + ci[CI Build] + test{Tests pass?} + fix[Fix and retry] + + subgraph deploy ["Deploy Pipeline"] + stage[Staging] + approve{Approve?} + prod[Production] + end + + cache[(Build Cache)] + notify[/Slack notify/] + + dev --> ci + ci -->|"Uses"| cache + ci --> test + test -->|"No"| fix + fix -.-> dev + test -->|"Yes"| stage + stage --> approve + approve -->|"Yes"| prod + approve -->|"No"| fix + prod -.->|"Deploy event"| notify + + style deploy fill:#C2E5FF,stroke:#3DADFF + style approve fill:#FFECBD,stroke:#FFC943 + style prod fill:#CDF4D3,stroke:#66D575 + style fix fill:#FFCDC2,stroke:#FF7556 +``` + +## 11. When a flowchart is NOT the right choice + +Route back to [SKILL.md](../SKILL.md) and pick a different diagram type if the user wants: + +- **Interactions over time between parties** → sequence diagram +- **Data model / entity relationships** → ER diagram +- **State machine with explicit states and transitions** → state diagram +- **Project schedule with dates** → gantt chart +- **Software architecture (services, datastores, queues)** → [architecture.md](./architecture.md) +- **Pie, mindmap, venn, class diagram, journey, timeline, quadrant** → not supported by `generate_diagram`; tell the user directly + +## 12. Calling generate_diagram + +Pass: + +- `name`: descriptive diagram name +- `mermaidSyntax`: your Mermaid flowchart +- `userIntent` (optional): what the user is trying to accomplish +- **Do NOT pass** `useArchitectureLayoutCode` for generic flowcharts diff --git a/plugins/figma/skills/figma-generate-diagram/references/gantt.md b/plugins/figma/skills/figma-generate-diagram/references/gantt.md new file mode 100644 index 00000000..492c9b71 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/gantt.md @@ -0,0 +1,242 @@ +# Gantt Charts + +Use this reference for **gantt charts** — project timelines, roadmaps, phased work, sprint plans, launch calendars, anything where the primary dimension is time and items have a start, duration, and optionally a dependency on other items. + +If the user wants an abstract dependency graph (A depends on B) without specific dates, use a **flowchart** instead. Gantt is for time-on-an-axis. + +--- + +## 1. When to use a gantt chart + +Good fits: + +- **Project roadmaps** — initiatives across quarters or months +- **Release plans** — milestones leading to a launch +- **Sprint / iteration plans** — tasks across a 1–4 week window +- **Event schedules** — intra-day or multi-day agendas + +Bad fits (route to a different diagram type): + +- Abstract dependency trees without dates → flowchart +- API call sequence between services → sequence diagram +- State machine → state diagram +- Data model → ER diagram + +## 2. Required skeleton + +``` +gantt + title Project Timeline + dateFormat YYYY-MM-DD + section Phase 1 + Research :r1, 2026-01-05, 10d + Prototype :p1, after r1, 7d + section Phase 2 + Build :b1, 2026-01-25, 3w + Launch prep :l1, after b1, 5d +``` + +Every chart needs: the `gantt` keyword, a `dateFormat` directive (`YYYY-MM-DD` for date charts, `HH:mm` for intra-day — see §3), and at least one task with a real start. `title` is optional but strongly recommended. + +## 3. dateFormat + +Two reliable formats, pick one based on the chart's time scale: + +- **`dateFormat YYYY-MM-DD`** — the default. Use for any chart with day-or-larger granularity (sprints, roadmaps, launch plans). +- **`dateFormat HH:mm`** — intra-day only. Tasks are time-of-day starts; see §8 for the full setup. + +Other formats (`DD/MM/YYYY`, `MM-DD-YYYY`, full datetimes) may parse but can hit the preprocessing layer and produce unexpected output. Stick to the two forms above. + +## 4. Sections + +``` +section
+``` + +Sections are horizontal lanes in the rendered chart. Use them to group tasks by: + +- **Phase** (Discovery / Build / Launch) +- **Team or owner** (Design / Eng / Marketing) +- **Workstream** (Frontend / Backend / Infra) + +Every task after a `section` declaration belongs to that section until the next `section`. You can omit sections entirely for short charts, and tasks will render in one lane. + +## 5. Task syntax + +Canonical form: + +``` + :, , , +``` + +Tags and ID are optional; ``, start, and duration/end are the minimum. Start can be an absolute date or a dependency on another task. + +### Forms (all supported) + +| Form | Example | When to use | +| --------------------------- | ----------------------------------- | --------------------------------------- | +| Absolute start + duration | `Kickoff :2026-01-05, 3d` | Simple timeline entry | +| Named + absolute + duration | `Kickoff :k1, 2026-01-05, 3d` | You'll reference this task from another | +| Single-dep + duration | `Design :d1, after k1, 5d` | Starts when `k1` ends | +| Multi-dep + duration | `Build :b1, after d1 r1, 2w` | Starts after the latest of `d1` or `r1` | +| Explicit end date | `Phase :p1, 2026-01-05, 2026-02-01` | You know both endpoints | +| Milestone (absolute) | `Launch :milestone, 2026-03-01, 0d` | Zero-duration marker | + +### Duration units + +`y` (years), `M` (months — **capital M**, lowercase `m` means minutes), `w` (weeks), `d` (days), `h` (hours), `m` (minutes), `s` (seconds), `ms` (milliseconds). Decimals are allowed (`1.5d`). + +For most roadmaps, `d` and `w` are the right units. Use `M` and `y` for multi-year horizons. Use `h` and `m` only for intra-day charts (§8). + +## 6. Task tags (states) + +Tags go before the id / start, separated by commas. Multiple tags stacked (e.g. `:active, crit, t1, …`) are supported. + +``` +Task name :done, t1, 2026-01-05, 5d +Task name :active, crit, t2, 2026-01-05, 5d +Ship :milestone, 2026-03-01, 0d +``` + +Supported tags: + +| Tag | Meaning | Use for | +| ----------- | ---------------------------- | ------------------------------------------------------- | +| `done` | Completed | Showing historical context on a forward-looking roadmap | +| `active` | In progress at chart's "now" | The one or two tasks currently happening | +| `crit` | Critical path | Genuinely critical items — overuse drains the meaning | +| `milestone` | Zero-duration marker | Launches, gates, review points (see §7) | + +**Do not** use the `vert` tag (vertical marker line). The parser accepts it, but our handler deliberately skips it — the task won't render. + +## 7. Milestones + +Three equivalent forms — pick whichever fits: + +``` +Launch :milestone, 2026-03-01, 0d // tag + absolute date +Ship :2026-03-01, 0d // zero duration is treated as a milestone +Ship :milestone, after l2, 0d // tag + after dependency +``` + +Milestones render as a single-point marker, not a bar. Keep names short (1–3 words) — the marker is small and long text crowds it. + +## 8. Intra-day charts (time-of-day) + +For event schedules and hour-scale timelines, switch `dateFormat` to `HH:mm`. Task starts become times-of-day, and the axis auto-switches to hour segments: + +```mermaid +gantt + title Launch day run-of-show + dateFormat HH:mm + section Morning + Team sync :09:00, 30m + Final QA :09:30, 1h + section Afternoon + Launch window :milestone, 14:00, 0m + Monitoring :14:00, 3h +``` + +Use `h` and `m` durations. Don't mix `dateFormat YYYY-MM-DD` with `HH:mm` task starts — task starts must match the declared `dateFormat` or the parser rejects the chart. + +## 9. What's NOT supported + +Our renderer is a subset of full Mermaid gantt. The following are **silently ignored or actively stripped** — don't include them, they waste tokens and can confuse readers who paste the Mermaid elsewhere: + +- `classDef`, `class`, any styling — **stripped by preprocessing**. No colors; the tool description confirms "In gantt charts, do not use color styling." +- `tickInterval`, `axisFormat` — ignored. Axis unit (hour / day / week / month / year) is auto-selected based on total chart duration. +- `excludes`, `includes`, `weekend` — ignored. Weekends are not skipped; excluded dates are not honored. +- `todayMarker` — not rendered. +- `click` handlers — FigJam diagrams are static. +- `vert` — parsed but not rendered. Tasks tagged `vert` are silently dropped. +- Compact mode / YAML settings — ignored. + +## 10. Limitations and gotchas + +- **Axis unit is auto-selected**. You don't control it directly — it's inferred from the total chart time range. Shorter charts get finer units (hour / day), longer ones get coarser (month / year). Design the date range to get the unit you want. +- **Multi-year charts work**. No automatic clamp; you can render 3+ year roadmaps, and the axis will coarsen to year-level segments. +- **Minimum task width is enforced**. Very short tasks in a long chart get widened to stay readable; the visual proportion won't match the exact date math. +- **Overlapping tasks stack vertically** within a section, not horizontally. ELK-style intelligent packing does not apply here. +- **Task names: keep them short**. Long names stretch the left gutter; 2–5 words is the sweet spot. + +## 11. When gantt syntax isn't enough — build a custom timeline instead + +Gantt is a great fit for the 80% case: phases, sequenced tasks, milestones, a clean time axis. But the renderer is intentionally narrow, and there's a class of timeline request it can't satisfy — for example: + +- Color-coded phases, tasks, or milestones +- Annotations, callouts, or sticky notes tied to specific dates +- Custom icons or images on milestones +- Dependency arrows drawn between lanes +- Non-uniform lane heights, or lanes grouped under a header +- Weekends/holidays visually excluded from the axis +- Narrative text or diagrams placed alongside the timeline +- Any styling beyond what Mermaid gantt allows (which is effectively none) + +When a user asks for something in this territory, **don't stretch the gantt syntax to pretend it supports it** — `generate_diagram` will silently drop or strip the relevant directives and the output will mislead the user. + +Instead, build the timeline directly on a FigJam canvas using the `use_figma` tool. The [figma-use](../../figma-use/SKILL.md) and [figma-use-figjam](../../figma-use-figjam/SKILL.md) skills cover how to: create a new FigJam file, place shapes and connectors, position nodes on a time axis, add sticky notes and annotations, color-code elements, and group content into sections. Load those skills and compose the timeline to the user's actual spec. + +Signals it's time to switch from `generate_diagram` to `use_figma`: + +- The user's request includes words like "color-code", "annotate", "highlight", "callout", "attach a note", "icon", "group under". +- The user has already tried `generate_diagram` once and is asking for refinements the syntax can't express. +- The user wants a timeline visualization that isn't strictly a gantt — horizontal roadmap swimlanes, a journey map with emotional beats, a dated storyboard, etc. +- The user has a reference file or mock they want you to match closely, and gantt's auto-layout won't hit it. + +Trade-offs worth naming up front: a hand-built FigJam timeline is more flexible but slower to produce, and iterating on it is manual rather than a one-line Mermaid edit. If the user just needs a quick schedule, gantt wins. If they want a presentation-quality timeline with real visual design, `use_figma` is the right tool. + +## 12. Best practices + +1. **One chart per coherent timeframe**. A 12-week sprint plan and a 3-year roadmap don't belong in the same chart — different axis units make both look wrong. +2. **Use sections liberally** for charts with 8+ tasks. One-lane charts beyond that length become hard to scan. +3. **Name IDs meaningfully** when you'll reference them with `after`. `d1`, `b1` are fine for short charts; `design_research`, `build_api` are better for longer ones that you'll iterate on. +4. **Prefer `after` dependencies over explicit dates** when tasks are sequential. If one slips, only the anchor task changes — the rest shift automatically. +5. **Reserve `crit`** for the genuine critical path — items where a slip delays the project. If everything is critical, nothing is. +6. **Keep it under ~25 tasks**. Past that, split into phase-specific charts. + +## 13. Validation checklist + +Before calling `generate_diagram`: + +1. `dateFormat` is declared — `YYYY-MM-DD` for date charts, `HH:mm` for intra-day. +2. The first task has an absolute start (not just `after` — the chart needs an anchor). +3. Every `after ` references a task ID defined earlier in the chart. +4. Task starts match the declared `dateFormat` (don't mix ISO dates with `HH:mm` starts). +5. No `classDef`, `class`, `style`, `click`, `tickInterval`, `axisFormat`, `excludes`, `todayMarker`, or `vert` lines (they'll be stripped or ignored). +6. Task names are short; IDs are terse but unambiguous. +7. Milestones use the `milestone` tag, a zero duration, or both. + +## 14. Complete example + +A realistic product-launch roadmap with phases, state tags, dependencies, and a milestone: + +```mermaid +gantt + title Q1 Launch Plan + dateFormat YYYY-MM-DD + section Discovery + User research :done, r1, 2026-01-05, 2w + Synthesis :done, s1, after r1, 1w + section Design + Concepts :active, d1, after s1, 2w + Design review :d2, after d1, 3d + Hi-fi designs :d3, after d2, 2w + section Build + API scaffolding :b1, after d2, 2w + UI build :b2, after d3, 3w + Integration :b3, after b2, 1w + section Launch + Internal beta :l1, after b3, 1w + Fixes :crit, l2, after l1, 5d + Ship :milestone, after l2, 0d +``` + +## 15. Calling generate_diagram + +Pass: + +- `name` — a descriptive diagram name +- `mermaidSyntax` — your gantt source +- `userIntent` (optional) — what the user is trying to accomplish + +Do **not** pass `useArchitectureLayoutCode` — that's architecture-diagram only. diff --git a/plugins/figma/skills/figma-generate-diagram/references/sequence.md b/plugins/figma/skills/figma-generate-diagram/references/sequence.md new file mode 100644 index 00000000..af787e86 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/sequence.md @@ -0,0 +1,251 @@ +# Sequence Diagrams + +Use this reference for **sequence diagrams** — interactions over time between parties (services, users, systems). API request/response flows, auth handshakes, multi-service choreography, RPC call traces, event cascades. + +The renderer is a **narrow subset** of full Mermaid sequence — read §5 carefully, because several features people commonly reach for (notes, loops, alt/else, activation boxes, colored blocks, autonumber) are silently dropped by our handler. The good news: most of those can be added back on top of the generated diagram with `use_figma` — see §7 for the hybrid workflow. + +--- + +## 1. When to use a sequence diagram + +Good fits: + +- **API call flows** — client → gateway → service → datastore, showing request and response messages +- **Auth handshakes** — OAuth, SAML, OIDC, session exchanges +- **Event choreography** — producers, brokers, consumers reacting over time +- **Multi-service workflows** — where the order of messages between services is the point +- **Protocol traces** — WebSocket, gRPC streaming, custom RPC + +Bad fits (route to a different diagram type): + +- Static architecture without time order → architecture flowchart +- Branching workflow with decisions and states → flowchart +- State transitions of a single entity → state diagram +- Data model → ER diagram + +## 2. Required skeleton + +``` +sequenceDiagram + title Login flow + participant User + participant WebApp + participant API + participant Database + + User->>WebApp: Open login page + WebApp->>API: POST /login + API->>Database: SELECT user + Database-->>API: User row + API-->>WebApp: 200 + session + WebApp-->>User: Redirect home +``` + +Every chart needs: the `sequenceDiagram` keyword and at least one message. `title` is optional but recommended. Participants are optional too — any unknown ID referenced in a message is implicitly created — but declaring them explicitly lets you control order. + +**Important**: whatever ID you use is what renders. Aliases (`as "Display Name"`) are silently dropped by our parser — see §3. + +## 3. Participants + +### Aliases (`as "Display Name"`) are silently dropped + +Our parser ignores the `as` clause. Whatever **ID** you choose is what renders in the diagram — the alias never appears. + +``` +participant api as "API Service" // renders as "api" +participant API // renders as "API" +participant ClientApp // renders as "ClientApp" +``` + +**Consequence**: pick IDs that read well on their own. Use readable PascalCase or camelCase (`ClientApp`, `AuthServer`, `Database`), not cryptic short forms (`a`, `p1`) expecting an alias to decorate them. + +Avoid spaces in IDs — they'll break the parse. If the user wants "Auth Server" as a display name, use `AuthServer` or `auth_server` as the ID. + +### Explicit declaration + +``` +participant ClientApp +participant AuthServer +participant Database +``` + +Participants render in the left-to-right order they're declared. + +### Participant types: all render the same + +Mermaid supports two keyword forms (`participant`, `actor`) and a JSON-config form for six more types: + +``` +actor User +participant WebApp +participant DB@{"type": "database"} +participant Q@{"type": "queue"} +// also: "boundary", "control", "entity", "collections" +``` + +Syntax notes: + +- Use `@{"type": "..."}` **immediately after the ID** — no comma, no space. +- `participant id, {"type": "..."}` (the comma form documented in some Mermaid sources) does **not** work here. + +**Our renderer draws all of these as the same rectangle.** There's no visual difference between an `actor`, a `database`, a `queue`, or a plain `participant` in the output. The type metadata is parsed and passed through but not rendered distinctly. + +Consequence: **don't bother with type annotations** — they're visual noise in the Mermaid source with no payoff. Just use `participant` for everything. + +If the user specifically wants visually distinct participant shapes (a cylinder for a database, a horizontal cylinder for a queue, a stick figure for a user), generate the base sequence here and then use `use_figma` to swap in the right shapes on top — see §7. + +### Implicit participants + +Any ID referenced in a message is auto-created if not declared. Fine for quick diagrams; for anything larger, declare explicitly so you control order. Either way, the ID is the display name. + +## 4. Messages + +Canonical form: + +``` +->>: +``` + +### Arrow types + +Our handler maps Mermaid's arrow syntaxes to **8 distinct visual outcomes**: + +| Visual | Syntaxes that produce it | Use for | +| --------------------------- | ------------------------ | ----------------------------------------------- | +| Solid, triangle head | `A->>B`, `A-xB` | **Default** — synchronous forward call, request | +| Solid, thin point | `A-)B` | Async fire-and-forget | +| Solid, no head | `A->B` | Rare — usually prefer `->>` | +| Dotted, triangle head | `A-->>B`, `A--xB` | **Default for return** — response, reply | +| Dotted, thin point | `A--)B` | Async return / callback | +| Dotted, no head | `A-->B` | Rare | +| Solid, triangles both ends | `A<<->>B` | Bidirectional sync channel | +| Dotted, triangles both ends | `A<<-->>B` | Bidirectional async channel | + +Note: `-x` and `->>` render identically (both solid + triangle), and `--x` and `-->>` render identically (both dotted + triangle). The cross visual is not supported for sequence messages. Use the `->>`/`-->>` form for clarity. + +**Pattern**: use `->>` for forward calls (request), `-->>` for return messages (response). That alone covers 80% of sequence diagrams and reads clearly. + +### Message labels + +Put the label after the colon. Labels are plain text — no quoting needed. + +- Short, imperative for forward calls: `POST /login`, `validateToken`, `fetch user` +- Short, noun phrase for returns: `200 OK`, `User{id, name}`, `session token` +- Include status codes, endpoint paths, and key identifiers — these are what make a sequence diagram useful. + +**Semicolon preprocessing**: semicolons inside a message label are rewritten to periods by our preprocessor (except at end of statement). Write `Items: a, b` rather than `Items; a; b`. + +## 5. What's NOT supported + +Our renderer is a **substantial** subset of full Mermaid sequence. The following are parsed but **silently dropped by our processor** — the rendered diagram will not contain them: + +- **Notes** — `Note over X: text`, `Note left of X`, `Note right of X`. All dropped. (Tool description confirms: "In sequence diagrams, do not use notes.") +- **Activation / deactivation** — `activate X` / `deactivate X`, and the `+`/`-` shorthand on arrows (`A->>+B: call`). The activation rectangles don't render. +- **Loops** — `loop ... end`. The inner messages still render, but the loop wrapper/label is gone. +- **Alternatives** — `alt ... else ... end`. Inner messages render flat, with no branch indication. +- **Optional** — `opt ... end`. Same — contents render, wrapper is gone. +- **Parallel** — `par ... and ... end`. Parallel messages render as a linear sequence. +- **Critical / break** — `critical ... option ... end`, `break ... end`. Dropped. +- **Colored blocks** — `rect rgb(...) ... end`. No background highlighting. +- **Autonumber** — `autonumber`. Messages are not numbered. +- **Links** — `link X: ...`, `links X: ...`. Not supported. +- **Box groupings** — `box ... end` around participants. Not rendered. + +**If the user asks for any of these**, don't stretch the Mermaid syntax trying to imitate them — the output will silently omit the feature. The better move in most cases is to generate the core sequence (participants + messages) with this tool, then layer the missing pieces on top with `use_figma`. See §7 for the hybrid workflow. + +## 6. Best practices + +1. **Pick the two key arrow types** — `->>` for forward, `-->>` for return. Mix a third (like `-)` for async) only when it encodes real semantics. +2. **Declare participants explicitly** for any diagram with 3+ participants. Auto-discovery by first mention is fine for 2, but order control matters past that. +3. **One flow per diagram.** If you have a happy path and an error path, draw two diagrams, not one with `alt/else` (which won't render as a branch anyway). +4. **Label every message.** Unlabeled arrows in a sequence diagram are nearly useless — the label is the whole point. +5. **Keep labels short.** 1–5 words. Include the specifics that matter (endpoint path, status code, return type) and drop the rest. +6. **Cap at ~15 messages.** Past that, split into multiple diagrams (per phase, per outcome, per actor cluster). +7. **Readable participant IDs.** The ID renders directly (aliases are dropped — §3), so choose something that reads well: `API`, `Database`, `AuthServer`, `ClientApp`. Avoid cryptic short forms (`a`, `p1`) and avoid overly long ones (`AuthenticationServiceV2`). 1–2 words in PascalCase is the sweet spot. + +## 7. Hybrid workflow: `generate_diagram` first, then `use_figma` for everything else + +`generate_diagram` produces a clean baseline — participants arranged in columns, labeled messages in order, consistent layout. That's the hard part. Most of what our renderer _doesn't_ support (notes, colored regions, step numbers, distinct participant shapes, annotations, callouts) is exactly the kind of layered-on content that `use_figma` handles well once a baseline exists. + +**Default workflow for any sequence that needs more than raw messages:** + +1. **Scaffold with `generate_diagram`** — generate the participants + messages as a clean Mermaid sequence. Skip the features that get dropped (notes, loops, alt/else wrappers, activation bars, rects, autonumber). The output is a FigJam file with a laid-out sequence. +2. **Extend with `use_figma`** — open the same file (via `fileKey`) and add the pieces the Mermaid syntax couldn't express: + - Sticky notes or text blocks for **annotations** anchored to specific messages + - Rectangles behind groups of messages for **phase highlighting** + - Vertical rectangles on a lifeline for **activation bars** + - Sequence numbers (`1.`, `2.`, …) placed next to messages + - Replacement shapes for participants — cylinder for database, horizontal cylinder for queue, person icon for actor + - Labeled groups (e.g. a rectangle around a block of messages labeled "retry loop") to stand in for `loop`/`alt`/`opt` + - Surrounding narrative, adjacent diagrams, or screenshots on the same board + +Loading [figma-use](../../figma-use/SKILL.md) and [figma-use-figjam](../../figma-use-figjam/SKILL.md) covers how to make those edits. + +### When to skip `generate_diagram` entirely + +Only if the baseline the tool would produce isn't useful. For example: + +- The user wants a **non-standard layout** (swimlane-style timeline, a radial sequence, a hand-drawn-style sketch) that doesn't resemble Mermaid's output. +- The user has a **specific reference mock** they want matched closely, and the auto-layout would fight it. +- The sequence is **tiny** (2–3 messages) and it's faster to place shapes manually than to prompt two tools. + +In those cases, go straight to `use_figma`. + +### Signals the request needs the hybrid workflow (not pure `generate_diagram`) + +- The user uses words like "note", "annotate", "callout", "highlight the loop", "show the alt/else branches", "activation box", "color-code the phases", "number the steps". +- The user has already generated a sequence and is asking for refinements (notes, rects, activations) that the renderer can't produce. +- The user wants to combine the sequence with adjacent content (architecture diagram, narrative, screenshots) on the same board. +- The user wants visually distinct participant shapes (database cylinder, queue cylinder, stick-figure actor). + +### Be pragmatic, not performative + +Don't over-explain the workflow to the user. If the request is specific, just scaffold and extend — call both tools in order. If it's ambiguous, scaffold first and ask something like "I've set up the base sequence — want me to add notes / phase highlighting / activation bars / step numbers?" + +## 8. Validation checklist + +Before calling `generate_diagram`: + +1. `sequenceDiagram` keyword on line 1 (after any leading whitespace). +2. Participant IDs are readable on their own (no cryptic `a`, `p1`). The ID is what renders — aliases are dropped. +3. No `as "Display Name"` aliases (they'll be stripped — §3). +4. No `@{"type": "..."}` annotations on participants — they parse but don't render distinctly (§3). Use plain `participant`. +5. No `Note`, `activate`/`deactivate`, `+`/`-` activation shorthand, `loop`, `alt`, `opt`, `par`, `critical`, `break`, `rect`, `autonumber`, or `link` lines (they'll be dropped — §5). +6. Every message has a label. +7. Labels have no semicolons (they'll be rewritten to periods). +8. Under ~15 messages, or the diagram is split. +9. Arrow types chosen deliberately — `->>` for forward, `-->>` for return, others only when they carry meaning. + +## 9. Complete example + +An OAuth authorization-code flow — a classic sequence-diagram use case: + +```mermaid +sequenceDiagram + title OAuth authorization code flow + participant User + participant ClientApp + participant AuthServer + participant ResourceAPI + + User->>ClientApp: Click Sign in + ClientApp->>AuthServer: GET /authorize + AuthServer-->>User: Login prompt + User->>AuthServer: Submit credentials + AuthServer-->>ClientApp: 302 with code + ClientApp->>AuthServer: POST /token + AuthServer-->>ClientApp: access_token + ClientApp->>ResourceAPI: GET /resource + ResourceAPI-->>ClientApp: 200 + data + ClientApp-->>User: Render page +``` + +## 10. Calling generate_diagram + +Pass: + +- `name` — a descriptive diagram name +- `mermaidSyntax` — your sequence source +- `userIntent` — what the user is trying to accomplish + +Do **not** pass `useArchitectureLayoutCode` — that's architecture-diagram only. diff --git a/plugins/figma/skills/figma-generate-diagram/references/state.md b/plugins/figma/skills/figma-generate-diagram/references/state.md new file mode 100644 index 00000000..d3d07a3e --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/state.md @@ -0,0 +1,310 @@ +# State Diagrams + +Use this reference for **state diagrams** — the states of a single entity or system and the transitions between them. Think: an order moving from Pending → Paid → Shipped → Delivered, a user account through Active / Suspended / Deleted, a TCP connection, a deploy pipeline, a state machine of any kind. + +If the subject is **interactions between multiple parties over time**, use a sequence diagram instead. If it's a **decision tree with branches but no real state concept**, use a flowchart. + +--- + +## 1. When to use a state diagram + +Good fits: + +- **Entity lifecycles** — orders, accounts, subscriptions, tickets, documents +- **State machines** — protocol states (TCP, auth), workflow states, process states +- **Feature flags / rollout states** — experimental / on / off / archived +- **Review / approval flows** — draft / submitted / approved / published +- **Session lifecycles** — idle / active / expired / revoked + +Bad fits (route to a different diagram type): + +- Interactions between parties over time → sequence diagram +- Decision tree / pipeline without stateful entities → flowchart +- Services + datastores → architecture flowchart +- Timeline with dates → gantt chart +- Data model → ER diagram + +## 2. Required skeleton + +``` +stateDiagram-v2 + direction LR + [*] --> Draft + Draft --> Review: submit + Review --> Approved: approve + Review --> Draft: reject + Approved --> Published: publish + Published --> [*] +``` + +Every chart needs: the `stateDiagram-v2` keyword on line 1 and at least one transition. `direction` is optional — `LR` (left-to-right) is the usual choice; `TB` (top-to-bottom) suits deeper hierarchies. Prefer `stateDiagram-v2` over the legacy `stateDiagram` for consistency. + +## 3. States + +Three declaration forms, all supported: + +### Simple ID + +``` +Draft --> Review +``` + +The state ID is used as both the handle and the display text. Fine for short names that read well (`Draft`, `Active`, `Failed`). + +### ID + description (colon syntax) + +``` +Draft: Draft (editable) +Draft --> Review +``` + +The ID (`Draft`) is what you reference in transitions; the description (`Draft (editable)`) is what renders. Use this when the display text includes spaces, punctuation, or detail that you don't want in every transition line. + +### `state "description" as id` + +``` +state "Waiting for approval" as Pending +Pending --> Approved +``` + +Functionally equivalent to the colon form — pick one style per diagram and stick with it. + +### Display-name normalization + +Our preprocessor strips quotes and normalizes state IDs internally. **The description is always what renders**. For simple-ID states, the ID is the description; for colon or `as` forms, the description you provide is the display name. Don't chase special quoting — just write plain descriptions. + +### Spaces in state names + +Simple-ID form can't have spaces (`Under Review` would break). Use the colon or `as` form: + +``` +Pending: Under Review +Pending --> Approved +``` + +## 4. Start and end: `[*]` + +`[*]` is both start and end, distinguished by arrow direction: + +``` +[*] --> Draft // start transition +Published --> [*] // end transition +``` + +Multiple entries and exits are allowed. You can mix them freely — `[*] --> Draft` and `[*] --> Recovered` both point to start-adjacent states. + +Inside a composite state, `[*]` denotes the entry and exit points of that composite. See §6. + +## 5. Transitions + +``` +From --> To +From --> To: label +``` + +- Use `-->` (double dash, not `->`). +- Add a label with `:` — usually the event or action that triggers the transition (`submit`, `approve`, `timeout`, `retry`). +- Keep labels short (1–3 words). Unlabeled transitions are fine when the target name tells the whole story (`Draft --> Review`). + +**Self-transitions** work: + +``` +Active --> Active: heartbeat +``` + +**Cycles** are legitimate in state diagrams (a ticket can reopen, an account can be suspended and reactivated). The ELK layout handles cycles reasonably, especially in small-to-medium diagrams — see §9 for layout notes. Don't avoid cycles if they represent the real state machine. + +## 6. Composite (nested) states + +Group related substates inside a parent state with `{ ... }`: + +``` +stateDiagram-v2 + [*] --> Active + Active --> [*] + + state Active { + [*] --> Idle + Idle --> Working: task arrives + Working --> Idle: task done + Working --> [*] + } +``` + +The composite renders as a **subgraph** (box) containing its children. The inner `[*]` markers are scoped to the composite — they represent entry/exit of the `Active` state, not of the whole diagram. + +### Nesting + +Nested composites work. Keep nesting to **2 levels max** — deeper nesting crowds the ELK layout. + +### Concurrent regions inside a composite — the `--` separator + +Splitting a composite into concurrent regions with `--` is supported; each region renders as its own nested subgraph inside the parent composite: + +``` +state Running { + state "Reads" as ReadPath + [*] --> ReadPath + ReadPath --> [*] + -- + state "Writes" as WritePath + [*] --> WritePath + WritePath --> [*] +} +``` + +Each region has its own `[*]` entry and exit scoped to that region. Use this when two independent sub-flows run simultaneously inside a single outer state. + +### Cross-composite transitions — stay simple + +Mermaid forbids transitions directly between nested states in *different* composites. Transition to or from the outer composite instead: + +``` +// WORKS — outer composite to/from +Active --> Suspended +Suspended --> Active + +// DOESN'T WORK — reaching into another composite's internals +Active.Working --> Suspended.Held +``` + +## 7. Special states: choice, fork, join + +### Choice — conditional branching + +``` +state DecideRoute <> +Received --> DecideRoute +DecideRoute --> Fast: priority=high +DecideRoute --> Normal: priority=low +``` + +Renders as a diamond. Use for a branch-by-condition that happens at a single point. + +### Fork / join — parallel paths + +``` +state StartParallel <> +state EndParallel <> + +[*] --> StartParallel +StartParallel --> PathA +StartParallel --> PathB +PathA --> EndParallel +PathB --> EndParallel +EndParallel --> [*] +``` + +Fork splits into parallel paths; join merges them back. Renders as distinct bar shapes. Use sparingly — they're specialized and can confuse readers unfamiliar with UML state-machine notation. + +## 8. What's NOT supported + +- **Notes** — `note left of X`, `note right of X`, `note above/below`. **Avoid strictly.** These interact badly with our preprocessor: the `X: text` inside a note is recognized as a state definition, producing phantom duplicate states. The resulting diagram is actively wrong, not just missing the note. If the user wants notes, generate the diagram without them and add real sticky notes or text blocks via `use_figma` (§10). +- **Styling** — `classDef`, `class Foo styleName`, `:::styleName` inline, and `style StateId fill:#hex,stroke:#hex`. **Avoid strictly.** These are not applied, AND the state names referenced in these statements get registered as standalone states, creating phantom orphan boxes above or beside the real diagram. The failure mode is the same shape as notes: not just missing, actively wrong. Color-code states via `use_figma` post-generation instead (§10). +- **Transitions reaching into another composite's children** — forbidden by Mermaid itself; transition to/from the outer composite. + +If the user wants notes or color-coded states, see §10 for the hybrid workflow. + +## 9. Layout (same ELK as flowcharts) + +State diagrams render via the **same ELK layered layout** as flowcharts. The layout principles from [flowchart.md §5 (ELK survival guide)](./flowchart.md#5-elk-survival-guide) apply directly: + +- **Simple cycles render fine** — retry loops, reopen transitions, reactivation paths. Don't contort the state machine to avoid them. +- **Subgraphs cluster cleanly** — composite states use this automatically. +- **Fan-in gets messy past ~5 inbound edges** — if many states converge to one `Failed` or `Terminated` state, consider splitting or duplicating (but be careful: state semantics usually preclude duplicating a state). +- **Pain scales with size** — 20+ states in one diagram starts to crowd. Split into phases/subsystems or introduce more composite grouping. + +Additionally, for state diagrams specifically: +- **Style composites so they stand out.** Like subgraphs in flowcharts, composites show only a thin outline by default and can blend into the canvas. The flowchart guidance on subgraph styling applies (use light tints from the FigJam palette). Note: our preprocessor doesn't extract `classDef`/`class`/`style` statements, so this styling must be applied via the hybrid workflow (§10). +- **Self-transitions render with tight spacing.** A self-loop (`Working --> Working: heartbeat`) will render, but the loop arc and its label can end up crowded against the state. Don't avoid self-transitions — they represent real state-machine behavior — but tell the user that if the spacing looks tight, they can drag the loop or label manually in Figma. + +## 10. Hybrid workflow: `generate_diagram` first, then `use_figma` + +State diagrams generated via `generate_diagram` produce a clean, laid-out state machine — the hard part. Most of what our renderer doesn't support (notes, colored states, step annotations, phase highlighting) can be added on top with `use_figma`. + +**Default workflow when the request needs more than bare states and transitions:** + +1. **Scaffold with `generate_diagram`** — states, transitions, composites, concurrent regions (`--`), special states (choice/fork/join), start/end. Skip the features that get dropped (notes, classDef). +2. **Extend with `use_figma`** — open the same file (via `fileKey`) and add: + - Sticky notes or text blocks for **annotations** anchored to specific states or transitions + - Background rectangles behind groups of states for **phase highlighting** + - Tinted fills on composites/subgraphs so boundaries stand out + - **Color-coding** states by category (terminal / active / error) + - **Sequence numbers** on transitions for step-by-step walkthroughs + +Loading [figma-use](../../figma-use/SKILL.md) and [figma-use-figjam](../../figma-use-figjam/SKILL.md) covers how to make those edits. + +### Signals the request needs the hybrid workflow + +- The user uses words like "note", "annotate", "highlight", "color the error states", "shade the happy path", "number the transitions". +- The user wants to combine the state diagram with surrounding narrative or another diagram on the same board. + +### When to skip `generate_diagram` entirely + +Only if the baseline layout isn't useful — e.g. the user wants a non-standard layout (circular state wheel, hand-drawn sketch, a heavily-stylized enterprise template). In those cases, go straight to `use_figma`. + +### Be pragmatic, not performative + +Scaffold first, extend directly if the user's request is specific; otherwise scaffold and ask one follow-up: "I've set up the base state machine — want me to add notes / color-code the states / highlight the error paths?" + +## 11. Best practices + +1. **Use `stateDiagram-v2`**, not the legacy `stateDiagram`. +2. **State names are nouns** (`Draft`, `Active`, `Archived`) — not actions. Actions are transition labels. +3. **Transition labels are events or triggers** (`submit`, `approve`, `timeout`) — short, 1–3 words. +4. **Start every diagram with `[*] -->`** — explicit entry is clearer than implicit. +5. **Terminate paths with `--> [*]`** when a state is genuinely terminal. Not every diagram needs an end marker; some state machines are perpetual. +6. **Group with composites** when 3+ substates share a shared lifecycle (e.g. `Active { Idle, Working }` vs. `Suspended`). Don't composite a pair. +7. **Cap at ~15–20 states.** Past that, split by phase or by entity, or push detail into composites. +8. **One state machine per diagram.** If you have a coarse overview and a zoomed-in view of one composite, draw two diagrams, not one with deeply nested internals. + +## 12. Validation checklist + +Before calling `generate_diagram`: + +1. `stateDiagram-v2` on line 1 (not just `stateDiagram`). +2. All transitions use `-->` (double dash). +3. Every `[*]` marker is on one side of a transition — never on its own. +4. State IDs are simple words (no spaces); descriptions via `:` or `as` when longer text is needed. +5. No `note left of`, `note right of`, etc. — they corrupt the diagram by creating phantom states (§8). Add real notes via `use_figma` later. +6. No `classDef`, `class`, `:::`, or `style` styling lines — they don't color anything AND create phantom orphan states (§8). Apply colors via `use_figma` later. +7. No transitions reaching into another composite's children. +8. Composite nesting is ≤ 2 levels. +9. Under ~20 states, or the diagram is split. + +## 13. Complete example + +A publishing workflow — draft → review → approved/rejected → published, with a composite for the review sub-states: + +```mermaid +stateDiagram-v2 + direction LR + + [*] --> Draft + Draft --> Review: submit + + state Review { + [*] --> PendingReview + PendingReview --> InReview: assigned + InReview --> ChangesRequested: request changes + ChangesRequested --> InReview: resubmit + InReview --> [*] + } + + Review --> Approved: approve + Review --> Draft: reject + Approved --> Published: publish + Published --> Archived: archive + Published --> [*] + Archived --> [*] +``` + +## 14. Calling generate_diagram + +Pass: + +- `name` — a descriptive diagram name +- `mermaidSyntax` — your state-diagram source +- `userIntent` (optional) — what the user is trying to accomplish + +Do **not** pass `useArchitectureLayoutCode` — that's architecture-diagram only. diff --git a/plugins/figma/skills/figma-generate-diagram/references/workflow.md b/plugins/figma/skills/figma-generate-diagram/references/workflow.md new file mode 100644 index 00000000..4688cc80 --- /dev/null +++ b/plugins/figma/skills/figma-generate-diagram/references/workflow.md @@ -0,0 +1,130 @@ +# Hybrid Diagram Workflow + +Mermaid's syntax can't express everything a good diagram needs — annotations tied to specific data, domain color-coding, callouts that live _next_ to the diagram rather than inside it. This reference covers the **hybrid workflow**: use `generate_diagram` to scaffold the structural diagram, then use `use_figma` (via the [figma-use-figjam](../../figma-use-figjam/SKILL.md) skill) to layer on what Mermaid can't do. + +**This is a judgment tool, not a procedure.** The hybrid workflow costs extra tokens and latency. Deploy it when the user's ask genuinely benefits — not on every diagram. When in doubt, ship the base diagram first; the user can tell you what's missing. + +## 1. When to reach for the hybrid workflow + +Signals that say **yes, go hybrid**: + +- User explicitly asks for something Mermaid can't do — _"add notes explaining the branches"_, _"color-code by team"_, _"callout the drop-offs with conversion numbers"_, _"annotate the critical path with the SLA"_. +- User shared attachable data (quotes, metrics, research notes, ticket links) that clearly maps to specific nodes. +- The diagram is complex enough that side-detail genuinely helps readability — dense subgraphs, long chains, branching flows where comments on specific steps would unblock a reader. +- The user is framing this as a shareable artifact (_"for our team review"_, _"so PMs can follow"_) rather than a quick thinking sketch. + +Signals that say **no, single-tool is enough**: + +- Short / self-explanatory request (_"diagram our auth flow"_ with no adjectives). +- User appears to be testing or exploring — small scope, minimal language, no data to attach. +- Small diagram (<8 nodes) where any annotation would be noisier than useful. +- Flowchart request where the only "extension" would be color — Mermaid subgraph styling already handles this (see [flowchart.md §4](./flowchart.md)). + +Bias toward action. The end goal is giving the user a file they can work with and keep iterating on — not producing a perfect artifact. Something is better than nothing; nothing is frustrating. + +## 2. Traffic-shaped priorities + +Not all diagram types benefit equally. Rough priority for deploying the workflow: + +1. **Flowchart** — highest value is _annotation_ (notes, callouts, attached data). Color-coding is already covered natively by Mermaid subgraph styling — **skip color recipes for flowcharts** and route to [flowchart.md](./flowchart.md) if that's all the user wants. +2. **ERD** — highest value is _domain color-coding_ (group tables by auth / billing / content / etc.) and _table-level annotations_. Mermaid's ERD styling is stripped by our preprocessor, so use_figma is the only path. +3. **Sequence / state / gantt** — smaller audiences; be conservative. Use the same recipes if the user explicitly asks, but don't volunteer heavy workflow on these. + +## 3. The pattern + +``` +1. Generate: call generate_diagram → capture fileKey from the returned URL +2. (Optional) Inspect: get_figjam(fileKey) to discover node IDs / positions if you need + to anchor extensions precisely +3. Extend: call use_figma with the same fileKey, applying one or more recipes +4. Report: share the file link + a one-line summary of what you added +``` + +**fileKey reuse is non-negotiable.** Every `use_figma` call after generation must pass the `fileKey` you parsed from the `generate_diagram` response URL (`figma.com/board/{fileKey}/...`). Never call `create_new_file` in this workflow — extensions go into the same file as the diagram. Multiple drafts pollute the user's file list. + +**Inspection is optional.** Skip `get_figjam` when your extensions don't need precise anchoring (e.g., adding a title text block above the diagram, adding a legend off to one side). Call it when you need to know where a specific node ended up (e.g., placing a sticky note adjacent to "Login" step). + +## 4. Recipe: Annotations (label + legend pattern) + +The single most universal extension. Works for every diagram type. Proven especially effective on dense diagrams (architecture, sequence, large flowcharts). + +**The opinionated default — label circles + sticky legend:** + +Place a small numbered circle ("pin") on or near each annotated node, then cluster the corresponding sticky notes as a **legend** off to the side of the diagram. The diagram stays clean; readers can reference "point 3" unambiguously; 10 annotations is as scannable as 3. + +Use [create-label](../../figma-use-figjam/references/create-label.md) for the pin circles and [create-sticky](../../figma-use-figjam/references/create-sticky.md) for the legend entries. That reference has a full worked example of the label-plus-legend pattern (`## Label + Sticky Legend` section) — follow it. + +**When to use:** + +- User asked for notes, callouts, annotations, comments, or "explain X". +- User provided data (conversion rates, latency numbers, quotes, ticket links, rationale) that maps to specific nodes. +- Three or more nodes in the diagram merit annotation — once you're past a couple, the legend pattern is strictly better than free-floating stickies. +- Shareable artifact ("for team review", "for the PRD") — the legend format reads as designed rather than scribbled. + +**How:** + +1. Call `get_figjam(fileKey)` to read back the diagram and find node IDs + bounding boxes for the nodes you're annotating. +2. Create one label circle per annotated node, colored consistently (e.g. all `PRESET_BLUE`), positioned at the node's top-left corner (offset by half the label size so it overlaps the corner slightly). +3. Create the matching stickies in a vertical column to the right of the diagram, prefixed with the number (`1. Drop-off: 42% last quarter`). +4. Follow the create-label reference's three-pass pattern (create labels, position on nodes, cluster legend) — especially the conflict-detection logic for pushing the legend past any existing content. + +**Fallback — plain sticky adjacent (1–2 annotations only):** + +If the user wants to annotate just one or two nodes and a legend would be visual overhead, place a single sticky directly adjacent to the target node (right side preferred, then above, then below). Keep text short. Optionally wire a connector from sticky to node with `create-connector` if position alone doesn't make the association clear. Past two annotations, switch to the label+legend pattern — don't scale stickies-on-nodes up. + +**Don'ts:** + +- Don't annotate every node. If it annotates everything, it annotates nothing. Pick the nodes that carry disproportionate importance. +- Don't rewrite information that's already in the node label. +- Don't place the sticky immediately adjacent to its label circle — if they're glued together, the circle is redundant. Legend goes in a cluster, not one-per-node. +- Don't mix the two forms — commit to labels+legend or commit to adjacent-stickies, not both in the same diagram. + +## 5. Recipe: Color-coding (domain / status tinting) + +**When to use:** + +- **ERD** — color tables by domain (auth / billing / content). Primary use case. +- **Sequence / state** — color participants or states by role (user-facing vs internal, terminal vs active vs error). +- **NOT flowchart** — use Mermaid subgraph styling via [flowchart.md §4](./flowchart.md) instead. If the user specifically wants per-node coloring that Mermaid can't do, fall through to this recipe, but start with the native path. + +**How:** + +1. Call `get_figjam(fileKey)` to find the node IDs you want to recolor. +2. Use `batch-modify` (see [figma-use-figjam/references/batch-modify.md](../../figma-use-figjam/references/batch-modify.md)) to update fills in a single call. Group by color assignment so one batch covers all nodes getting the same tint. +3. Pick from FigJam's built-in palette (documented in [create-shape-with-text.md](../../figma-use-figjam/references/create-shape-with-text.md)) rather than freehand hex values — keeps the diagram visually coherent with the rest of the canvas. + +**Don'ts:** + +- Don't use saturated / high-contrast colors as fills — text inside colored shapes becomes hard to read. Stick to light tints. +- Don't color every node differently. Color groups, not individuals. If every node has its own color, the color isn't carrying meaning. +- Don't also add a sticky-note legend unless the user asked — the coloring should be self-explanatory in context (e.g., grouped tables), or the user can infer from node names. + +## 6. Communication pattern + +Two things matter; the rest is up to the model's normal style and user preferences. + +- **One-liner up front when the plan isn't obvious from the ask.** If the user said _"diagram our auth flow"_, no preamble needed. If they said _"diagram our auth flow, highlight the drop-offs"_, a short _"Generating the diagram, then adding callouts for the drop-offs"_ sets expectations. Don't ask for approval; the user already asked. +- **Share the file link as soon as `generate_diagram` returns — before running extensions.** The base diagram is the first deliverable; users would rather open it and start looking while extension work continues than wait for a "finished" version. A sentence like _"Here's the base diagram: [link]. Adding the callouts now."_ is enough. + +Everything else is up to you and your typical interactions with the user. + +Ambiguous request? Pick a reasonable extension, do it, and narrate what you chose so the user can redirect. Don't ask a clarifying question when a reasonable default exists. + +## 7. When extensions fail partway + +If `use_figma` fails after `generate_diagram` succeeded, the user already has the file link from step 3 of the communication flow. The failure message just needs to tell them the state of the file: + +- **Do not** retry in a loop or churn trying to fix it. +- **Do** report clearly what landed and what didn't. _"The diagram is in the file, but I couldn't add the callout labels — `use_figma` failed with {short error}. You can add them manually or ask me to try again."_ +- Partial progress is still progress. The user can open the file and continue from there. + +## 8. What NOT to do in MVP + +- **Don't reposition nodes.** ELK's layout is what it is for now. If the diagram looks cramped or tangled, the fix is better Mermaid, not manual repositioning via `use_figma`. +- **Don't build the diagram from scratch with `use_figma`.** If `generate_diagram` can produce a reasonable base, use it. `use_figma` is for additive extensions, not replacement. +- **Don't over-extend.** If the user asked for something simple, give them something simple. Every unrequested sticky or color choice is noise. +- **Don't turn the workflow into a checklist.** If the user says _"diagram our API flow"_ with no qualifiers, the right answer is a single `generate_diagram` call — not a scaffold-and-extend ceremony. + +## 9. End goal + +The file you ship is a **starting point**. Users will open it in FigJam and keep iterating — moving things, recoloring, adding their own stickies. The hybrid workflow's job is to give them a better starting point, not a finished deliverable. Don't aim for pixel-perfect; aim for useful-immediately. diff --git a/plugins/figma/skills/figma-generate-library/references/discovery-phase.md b/plugins/figma/skills/figma-generate-library/references/discovery-phase.md index e32a309a..758f958e 100644 --- a/plugins/figma/skills/figma-generate-library/references/discovery-phase.md +++ b/plugins/figma/skills/figma-generate-library/references/discovery-phase.md @@ -147,7 +147,7 @@ CSS `0 4px 6px -1px rgba(0,0,0,0.1)` → Figma: |---|---| | `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"}` | +| `font-weight: 600` | STRING variable (scope `FONT_STYLE`, holds a font-specific style name like `"Regular"` — discover via `listAvailableFontsAsync()`) or Text Style `fontName.style` | | `letter-spacing: -0.02em` | Text Style `letterSpacing: {value: -2, unit: "PERCENT"}` | | `font-family: "Inter"` | STRING variable (scope `FONT_FAMILY`) or Text Style `fontName.family` | diff --git a/plugins/figma/skills/figma-generate-library/references/documentation-creation.md b/plugins/figma/skills/figma-generate-library/references/documentation-creation.md index 2cf426e2..eb311a13 100644 --- a/plugins/figma/skills/figma-generate-library/references/documentation-creation.md +++ b/plugins/figma/skills/figma-generate-library/references/documentation-creation.md @@ -34,19 +34,16 @@ async function createCoverPage(systemName, tagline, version, primaryColorVar) { await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); await figma.loadFontAsync({ family: 'Inter', style: 'Medium' }); - const frame = figma.createFrame(); + const frame = figma.createAutoLayout('VERTICAL'); frame.name = 'Cover'; frame.resize(1440, 900); + frame.layoutSizingHorizontal = 'FIXED'; + frame.layoutSizingVertical = 'FIXED'; 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) { @@ -115,9 +112,8 @@ async function createFoundationsPage() { await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); // Root scroll frame - const root = figma.createFrame(); + const root = figma.createAutoLayout('VERTICAL'); root.name = 'Foundations'; - root.layoutMode = 'VERTICAL'; root.primaryAxisAlignItems = 'MIN'; root.counterAxisAlignItems = 'MIN'; root.itemSpacing = 80; @@ -125,9 +121,8 @@ async function createFoundationsPage() { root.paddingBottom = 120; root.paddingLeft = 80; root.paddingRight = 80; - root.layoutSizingHorizontal = 'FIXED'; - root.layoutSizingVertical = 'HUG'; root.resize(1440, 1); + root.layoutSizingHorizontal = 'FIXED'; root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; page.appendChild(root); @@ -156,15 +151,13 @@ Color swatches must be **bound to actual Figma variables** — never hardcode he async function createColorSwatch(parent, varName, variable) { await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); - const swatchFrame = figma.createFrame(); + const swatchFrame = figma.createAutoLayout('VERTICAL'); 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.layoutSizingHorizontal = 'FIXED'; swatchFrame.fills = []; // Color rectangle — bound to variable @@ -218,14 +211,12 @@ async function createColorSection(root, primitiveVars, semanticVars) { await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); // Section container - const section = figma.createFrame(); + const section = figma.createAutoLayout('VERTICAL'); section.name = 'Section/Colors'; - section.layoutMode = 'VERTICAL'; section.itemSpacing = 24; - section.layoutSizingHorizontal = 'FILL'; - section.layoutSizingVertical = 'HUG'; section.fills = []; root.appendChild(section); + section.layoutSizingHorizontal = 'FILL'; // Section heading const heading = figma.createText(); @@ -252,15 +243,13 @@ async function createColorSection(root, primitiveVars, semanticVars) { primLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }]; section.appendChild(primLabel); - const primRow = figma.createFrame(); + const primRow = figma.createAutoLayout(); primRow.name = 'Primitives/Row'; - primRow.layoutMode = 'HORIZONTAL'; primRow.itemSpacing = 12; - primRow.layoutSizingHorizontal = 'FILL'; - primRow.layoutSizingVertical = 'HUG'; primRow.fills = []; primRow.layoutWrap = 'WRAP'; section.appendChild(primRow); + primRow.layoutSizingHorizontal = 'FILL'; for (const v of primitiveVars) { await createColorSwatch(primRow, v.name, v); @@ -275,15 +264,13 @@ async function createColorSection(root, primitiveVars, semanticVars) { semLabel.fills = [{ type: 'SOLID', color: { r: 0.55, g: 0.55, b: 0.55 } }]; section.appendChild(semLabel); - const semRow = figma.createFrame(); + const semRow = figma.createAutoLayout(); semRow.name = 'Semantic/Row'; - semRow.layoutMode = 'HORIZONTAL'; semRow.itemSpacing = 12; - semRow.layoutSizingHorizontal = 'FILL'; - semRow.layoutSizingVertical = 'HUG'; semRow.fills = []; semRow.layoutWrap = 'WRAP'; section.appendChild(semRow); + semRow.layoutSizingHorizontal = 'FILL'; for (const v of semanticVars) { await createColorSwatch(semRow, v.name, v); @@ -320,16 +307,14 @@ async function createTypeSpecimen(parent, styleName, fontFamily, fontStyle, font await figma.loadFontAsync({ family: 'Inter', style: 'Medium' }); await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); - const row = figma.createFrame(); + const row = figma.createAutoLayout('VERTICAL'); 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); + row.layoutSizingHorizontal = 'FILL'; // Style name label (small, muted) const nameText = figma.createText(); @@ -383,14 +368,12 @@ async function createTypeSpecimen(parent, styleName, fontFamily, fontStyle, font async function createTypographySection(root, typeStyles) { await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); - const section = figma.createFrame(); + const section = figma.createAutoLayout('VERTICAL'); section.name = 'Section/Typography'; - section.layoutMode = 'VERTICAL'; section.itemSpacing = 0; - section.layoutSizingHorizontal = 'FILL'; - section.layoutSizingVertical = 'HUG'; section.fills = []; root.appendChild(section); + section.layoutSizingHorizontal = 'FILL'; const heading = figma.createText(); heading.fontName = { family: 'Inter', style: 'Bold' }; @@ -429,15 +412,13 @@ Spacing bars show each spacing token as a filled rectangle whose width equals th async function createSpacingBar(parent, name, value, variable, codeSyntax) { await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); - const row = figma.createFrame(); + const row = figma.createAutoLayout(); row.name = `Spacing/${name}`; - row.layoutMode = 'HORIZONTAL'; row.counterAxisAlignItems = 'CENTER'; row.itemSpacing = 16; - row.layoutSizingHorizontal = 'FILL'; - row.layoutSizingVertical = 'HUG'; row.fills = []; parent.appendChild(row); + row.layoutSizingHorizontal = 'FILL'; // The bar rectangle — width bound to spacing variable const bar = figma.createRectangle(); @@ -474,14 +455,12 @@ async function createSpacingBar(parent, name, value, variable, codeSyntax) { async function createSpacingSection(root, spacingTokens) { await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); - const section = figma.createFrame(); + const section = figma.createAutoLayout('VERTICAL'); section.name = 'Section/Spacing'; - section.layoutMode = 'VERTICAL'; section.itemSpacing = 12; - section.layoutSizingHorizontal = 'FILL'; - section.layoutSizingVertical = 'HUG'; section.fills = []; root.appendChild(section); + section.layoutSizingHorizontal = 'FILL'; const heading = figma.createText(); heading.fontName = { family: 'Inter', style: 'Bold' }; @@ -519,15 +498,16 @@ async function createShadowCard(parent, name, effects) { await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); await figma.loadFontAsync({ family: 'Inter', style: 'Medium' }); - const card = figma.createFrame(); + const card = figma.createAutoLayout('VERTICAL'); 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.layoutSizingHorizontal = 'FIXED'; + card.layoutSizingVertical = 'FIXED'; card.cornerRadius = 8; card.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }]; card.effects = effects; @@ -572,14 +552,12 @@ async function createShadowCard(parent, name, effects) { async function createShadowSection(root, shadowTokens) { await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); - const section = figma.createFrame(); + const section = figma.createAutoLayout('VERTICAL'); section.name = 'Section/Elevation'; - section.layoutMode = 'VERTICAL'; section.itemSpacing = 24; - section.layoutSizingHorizontal = 'FILL'; - section.layoutSizingVertical = 'HUG'; section.fills = []; root.appendChild(section); + section.layoutSizingHorizontal = 'FILL'; const heading = figma.createText(); heading.fontName = { family: 'Inter', style: 'Bold' }; @@ -589,19 +567,17 @@ async function createShadowSection(root, shadowTokens) { section.appendChild(heading); // Cards row — extra top padding so shadows are visible - const row = figma.createFrame(); + const row = figma.createAutoLayout(); 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; + row.fills = [{ type: 'SOLID', color: { r: 0.97, g: 0.97, b: 0.97 } }]; + row.cornerRadius = 8; section.appendChild(row); + row.layoutSizingHorizontal = 'FILL'; for (const tok of shadowTokens) { await createShadowCard(row, tok.name, tok.effects); @@ -633,16 +609,14 @@ 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(); + const wrapper = figma.createAutoLayout('VERTICAL'); 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); + wrapper.layoutSizingHorizontal = 'FIXED'; parent.appendChild(wrapper); const rect = figma.createRectangle(); @@ -693,14 +667,12 @@ async function createRadiusCard(parent, name, value, variable) { async function createRadiusSection(root, radiusTokens) { await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); - const section = figma.createFrame(); + const section = figma.createAutoLayout('VERTICAL'); section.name = 'Section/Radius'; - section.layoutMode = 'VERTICAL'; section.itemSpacing = 24; - section.layoutSizingHorizontal = 'FILL'; - section.layoutSizingVertical = 'HUG'; section.fills = []; root.appendChild(section); + section.layoutSizingHorizontal = 'FILL'; const heading = figma.createText(); heading.fontName = { family: 'Inter', style: 'Bold' }; @@ -709,19 +681,17 @@ async function createRadiusSection(root, radiusTokens) { heading.fills = [{ type: 'SOLID', color: { r: 0.07, g: 0.07, b: 0.07 } }]; section.appendChild(heading); - const row = figma.createFrame(); + const row = figma.createAutoLayout(); 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); + row.layoutSizingHorizontal = 'FILL'; for (const tok of radiusTokens) { await createRadiusCard(row, tok.name, tok.value, tok.variable); @@ -754,17 +724,15 @@ async function createComponentDocFrame(page, componentName, description, usageNo await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }); await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }); - const doc = figma.createFrame(); + const doc = figma.createAutoLayout('VERTICAL'); 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.layoutSizingHorizontal = 'FIXED'; doc.fills = []; doc.x = 0; doc.y = 0; diff --git a/plugins/figma/skills/figma-generate-library/references/token-creation.md b/plugins/figma/skills/figma-generate-library/references/token-creation.md index 1fcb03a8..70a20983 100644 --- a/plugins/figma/skills/figma-generate-library/references/token-creation.md +++ b/plugins/figma/skills/figma-generate-library/references/token-creation.md @@ -447,7 +447,7 @@ return { created, count: created.length }; | Effect blur radius | `["EFFECT_FLOAT"]` | FLOAT | | Opacity | `["OPACITY"]` | FLOAT | | Font family | `["FONT_FAMILY"]` | STRING | -| Font style (e.g. "Semi Bold") | `["FONT_STYLE"]` | STRING | +| Font style (font-specific name, e.g. `"Regular"` — varies per font) | `["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. diff --git a/plugins/figma/skills/figma-generate-library/scripts/createDocumentationPage.js b/plugins/figma/skills/figma-generate-library/scripts/createDocumentationPage.js index 01aa6a26..20430691 100644 --- a/plugins/figma/skills/figma-generate-library/scripts/createDocumentationPage.js +++ b/plugins/figma/skills/figma-generate-library/scripts/createDocumentationPage.js @@ -62,9 +62,8 @@ async function createDocumentationPage(pageName, config, runId) { const frameIds = [] // Root scroll container — 1440px wide, auto-height - const root = figma.createFrame() + const root = figma.createAutoLayout('VERTICAL') root.name = pageName - root.layoutMode = 'VERTICAL' root.primaryAxisAlignItems = 'MIN' root.counterAxisAlignItems = 'MIN' root.itemSpacing = 80 @@ -72,9 +71,8 @@ async function createDocumentationPage(pageName, config, runId) { root.paddingBottom = 120 root.paddingLeft = 80 root.paddingRight = 80 - root.layoutSizingHorizontal = 'FIXED' - root.layoutSizingVertical = 'HUG' root.resize(1440, 1) + root.layoutSizingHorizontal = 'FIXED' root.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 } }] root.x = 0 root.y = 0 @@ -88,14 +86,12 @@ async function createDocumentationPage(pageName, config, runId) { frameIds.push(root.id) // Page header: title + optional description - const header = figma.createFrame() + const header = figma.createAutoLayout('VERTICAL') header.name = 'Header' - header.layoutMode = 'VERTICAL' header.itemSpacing = 12 - header.layoutSizingHorizontal = 'FILL' - header.layoutSizingVertical = 'HUG' header.fills = [] root.appendChild(header) + header.layoutSizingHorizontal = 'FILL' const titleNode = figma.createText() titleNode.fontName = { family: 'Inter', style: 'Bold' } @@ -118,14 +114,12 @@ async function createDocumentationPage(pageName, config, runId) { // Sections for (const section of config.sections) { - const sectionFrame = figma.createFrame() + const sectionFrame = figma.createAutoLayout('VERTICAL') sectionFrame.name = `Section/${section.name}` - sectionFrame.layoutMode = 'VERTICAL' sectionFrame.itemSpacing = 20 - sectionFrame.layoutSizingHorizontal = 'FILL' - sectionFrame.layoutSizingVertical = 'HUG' sectionFrame.fills = [] root.appendChild(sectionFrame) + sectionFrame.layoutSizingHorizontal = 'FILL' if (runId) { sectionFrame.setPluginData('dsb_run_id', runId) diff --git a/plugins/figma/skills/figma-implement-design/SKILL.md b/plugins/figma/skills/figma-implement-design/SKILL.md index 07b920ec..6317edca 100644 --- a/plugins/figma/skills/figma-implement-design/SKILL.md +++ b/plugins/figma/skills/figma-implement-design/SKILL.md @@ -15,7 +15,7 @@ This skill provides a structured workflow for translating Figma designs into pro - 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 only for Code Connect mappings, switch to [figma-code-connect](../figma-code-connect/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 diff --git a/plugins/figma/skills/figma-use-figjam/LICENSE.txt b/plugins/figma/skills/figma-use-figjam/LICENSE.txt new file mode 100644 index 00000000..5dcf1aa2 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/LICENSE.txt @@ -0,0 +1,2 @@ +Use of these Figma skills and related files ("Materials") is governed by the Figma Developer Terms (available at https://www.figma.com/legal/developer-terms/). By accessing, downloading, or using these Materials — including through automated systems or AI agents — you agree to the Figma Developer Terms. +These Materials are currently offered as a Beta feature. Figma may modify, suspend, or discontinue them at any time without notice. diff --git a/plugins/figma/skills/figma-use-figjam/SKILL.md b/plugins/figma/skills/figma-use-figjam/SKILL.md new file mode 100644 index 00000000..542e9cbc --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/SKILL.md @@ -0,0 +1,28 @@ +--- +name: figma-use-figjam +description: "This skill helps agents use Figma's use_figma MCP tool in the FigJam context. Can be used alongside figma-use which has foundational context for using the use_figma tool." +disable-model-invocation: false +--- + +# use_figma — Figma Plugin API Skill for FigJam + +This skill contains FigJam-specific context for the `use_figma` MCP tool. The [figma-use](../figma-use/SKILL.md) skill provides foundational context for plugin API execution via MCP as well as the full Figma plugin API for more advanced use-cases that are not described here. + +**Always pass `skillNames: "figma-use-figjam"` when calling `use_figma` for FigJam operations.** This is a logging parameter used to track skill usage — it does not affect execution. + +## Reference Docs + +- [plan-board-content](references/plan-board-content.md) - Read this for any board content request — board template, retro, brainstorm, ice breaker, meeting board, scaffold + - Covers planning of generated board content, including sequential outline, sections, intents, and hierarchical text + - Delegates to other references for specific API details +- [create-section](references/create-section.md) — Create and configure FigJam sections (sizing, naming, colors, content visibility, organizing nodes, column layouts) +- [create-sticky](references/create-sticky.md) — Create and configure FigJam sticky notes (colors, sizing, text, author visibility, batch creation) +- [create-connector](references/create-connector.md) — Create and configure FigJam connectors (endpoints, arrows, line types, labels, colors, diagram wiring) +- [create-text](references/create-text.md) — Create and configure FigJam text nodes (font loading, preset fonts and colors, sizing, lists, mind map operations) +- [position-figjam-nodes](references/position-figjam-nodes.md) — Position, size, and reparent nodes on the canvas (including within sections) +- [create-shape-with-text](references/create-shape-with-text.md) — Create and configure FigJam shapes with embedded text (shape types, color presets, sizing to fit text, diagram layouts) +- [create-code-block](references/create-code-block.md) — Create and configure FigJam code block nodes (languages, syntax highlighting, positioning, embedding in sections) +- [create-table](references/create-table.md) — Create and configure FigJam tables (rows, columns, cell text, color presets, resizing) +- [edit-text](references/edit-text.md) — Edit existing text nodes (font loading, styled ranges, find/replace, FigJam Charcoal default color) +- [create-label](references/create-label.md) — Create and configure FigJam label nodes (small numbered/lettered circle callout markers, sequences, positioning) +- [batch-modify](references/batch-modify.md) — Patterns for modifying many existing nodes at once (bulk style changes, repositioning, property updates) diff --git a/plugins/figma/skills/figma-use-figjam/agents/openai.yaml b/plugins/figma/skills/figma-use-figjam/agents/openai.yaml new file mode 100644 index 00000000..53d87f1f --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "use_figma FigJam" + short_description: "Use Figma's use_figma tool in FigJam files" + default_prompt: "Use the FigJam-specific use_figma workflow to create or modify FigJam content." diff --git a/plugins/figma/skills/figma-use-figjam/references/batch-modify.md b/plugins/figma/skills/figma-use-figjam/references/batch-modify.md new file mode 100644 index 00000000..13dccfab --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/batch-modify.md @@ -0,0 +1,300 @@ +# Batch Operations Pattern + +> Part of the [figma-use-figjam skill](../SKILL.md). Patterns for modifying many existing nodes at once. + +**Typical workflow:** + +1. Find nodes using traversal APIs (`findAll`, `findAllWithCriteria`) +2. Apply modifications using the patterns below + +## Performance Tips + +### 1. Use findAllWithCriteria for Type-Based Searches + +`findAllWithCriteria` is significantly faster than `findAll` when filtering by node type only. + +```javascript +// ✅ FAST - Use findAllWithCriteria for type filtering +const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] }) +const shapes = figma.currentPage.findAllWithCriteria({ + types: ['RECTANGLE', 'ELLIPSE', 'POLYGON', 'STAR'], +}) + +// ❌ SLOWER - findAll with type check +const textNodesSlow = figma.currentPage.findAll((n) => n.type === 'TEXT') + +figma.closePlugin() +``` + +### 2. Skip Invisible Instance Children + +For large files with many component instances, this significantly speeds up traversal. + +```javascript +// Enable at the start of your script +figma.skipInvisibleInstanceChildren = true + +// Now findAll/findOne will skip hidden content inside instances +const visibleText = figma.currentPage.findAll((n) => n.type === 'TEXT') + +figma.closePlugin() +``` + +### 3. Limit Search Scope + +Search within a specific node rather than the entire page. + +```javascript +// ✅ FAST - Search within specific frame +const frame = await figma.getNodeByIdAsync('123:456') +if (frame && 'findAll' in frame) { + const textInFrame = frame.findAll((n) => n.type === 'TEXT') +} + +// ❌ SLOWER - Search entire page +const allText = figma.currentPage.findAll((n) => n.type === 'TEXT') + +figma.closePlugin() +``` + +## Batch Modify Pattern + +### Basic Batch Modification + +```javascript +const page = figma.currentPage + +// Find all buttons +const buttons = page.findAll((n) => n.name.toLowerCase().includes('button')) +console.log(`Found ${buttons.length} buttons`) + +// Modify each one +let modified = 0 +for (const btn of buttons) { + if ('fills' in btn) { + btn.fills = [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1 } }] + modified++ + } +} + +console.log(`Modified ${modified} buttons`) +figma.closePlugin() +``` + +### With Progress Logging + +For long operations, log progress so you can track what's happening. + +```javascript +const nodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] }) +console.log(`Processing ${nodes.length} text nodes...`) + +let processed = 0 +for (const node of nodes) { + // Load all fonts (handles mixed fonts via styled segments) + const segments = node.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + node.fontSize = 16 + + processed++ + if (processed % 50 === 0) { + console.log(`Processed ${processed}/${nodes.length}`) + } +} + +console.log(`Done! Processed ${processed} nodes`) +figma.closePlugin() +``` + +## Chunked Processing + +For very large operations, process in chunks to avoid timeouts. + +```javascript +async function processInChunks(nodes, chunkSize, processFn) { + const results = [] + + for (let i = 0; i < nodes.length; i += chunkSize) { + const chunk = nodes.slice(i, i + chunkSize) + console.log( + `Processing chunk ${Math.floor(i / chunkSize) + 1}/${Math.ceil(nodes.length / chunkSize)}`, + ) + + for (const node of chunk) { + const result = await processFn(node) + results.push(result) + } + } + + return results +} + +// Usage +const allText = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] }) + +await processInChunks(allText, 100, async (node) => { + // Load all fonts (handles mixed fonts via styled segments) + const segments = node.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + node.textCase = 'UPPER' + return node.id +}) + +figma.closePlugin() +``` + +## Collecting Results + +### Build Summary Object + +```javascript +const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] }) + +// Collect statistics +const fontUsage = {} +for (const node of textNodes) { + if (node.fontName && node.fontName.family) { + const key = `${node.fontName.family} ${node.fontName.style}` + fontUsage[key] = (fontUsage[key] || 0) + 1 + } +} + +console.log('Font usage:') +for (const [font, count] of Object.entries(fontUsage).sort((a, b) => b[1] - a[1])) { + console.log(` ${font}: ${count}`) +} + +figma.closePlugin() +``` + +### Group by Property + +```javascript +const nodes = figma.currentPage.findAll((n) => 'fills' in n) + +// Group by fill color +const byColor = {} +for (const node of nodes) { + if (Array.isArray(node.fills) && node.fills.length > 0) { + const fill = node.fills[0] + if (fill.type === 'SOLID') { + const key = `rgb(${Math.round(fill.color.r * 255)}, ${Math.round(fill.color.g * 255)}, ${Math.round(fill.color.b * 255)})` + if (!byColor[key]) byColor[key] = [] + byColor[key].push(node.name) + } + } +} + +console.log('Nodes by color:', JSON.stringify(byColor, null, 2)) +figma.closePlugin() +``` + +## Safe Batch Updates + +### Check Before Modify + +```javascript +const nodes = figma.currentPage.findAll((n) => n.name.includes('Button')) + +for (const node of nodes) { + // Log before state + console.log(`${node.name} before:`, 'fills' in node ? JSON.stringify(node.fills) : 'no fills') + + // Check if modification is possible + if (!('fills' in node)) { + console.log(` Skipping ${node.name} - no fills property`) + continue + } + + // Modify + node.fills = [{ type: 'SOLID', color: { r: 0, g: 0.5, b: 1 } }] + + // Log after state + console.log(`${node.name} after:`, JSON.stringify(node.fills)) +} + +figma.closePlugin() +``` + +## Common Patterns: Renaming Layers + +### Bulk Find-and-Replace in Names + +```javascript +const nodes = figma.currentPage.findAll((n) => n.name.includes('Button')) +console.log(`Found ${nodes.length} nodes to rename`) + +for (const node of nodes) { + const oldName = node.name + node.name = node.name.replace('Button', 'Btn') + console.log(` "${oldName}" → "${node.name}"`) +} + +figma.closePlugin() +``` + +### Auto-Numbering Children + +```javascript +const frame = await figma.getNodeByIdAsync('123:456') + +if ('children' in frame) { + for (let i = 0; i < frame.children.length; i++) { + frame.children[i].name = `Item ${i + 1}` + } + console.log(`Numbered ${frame.children.length} children`) +} + +figma.closePlugin() +``` + +### Content-Based Naming (Name from Text Content) + +```javascript +const frames = figma.currentPage.findAll((n) => n.type === 'FRAME' && 'children' in n) + +let renamed = 0 +for (const frame of frames) { + const heading = frame.findOne((n) => n.type === 'TEXT') + if (heading) { + frame.name = heading.characters.slice(0, 40) + renamed++ + } +} + +console.log(`Renamed ${renamed} frames from heading text`) +figma.closePlugin() +``` + +### Strip Auto-Generated Names + +```javascript +const autoNamePattern = /^(Frame|Rectangle|Ellipse|Group|Vector|Line|Polygon|Star)\s+\d+$/ +const nodes = figma.currentPage.findAll((n) => autoNamePattern.test(n.name)) +console.log(`Found ${nodes.length} auto-named nodes`) + +for (const node of nodes) { + node.name = node.type.toLowerCase() +} + +figma.closePlugin() +``` + +### Add Prefix with `/` Separator (Layer Panel Grouping) + +Figma groups layers in the panel by `/` in names (e.g., `icons/arrow`, `icons/check`). + +```javascript +const icons = figma.currentPage.findAll( + (n) => n.type === 'INSTANCE' && n.name.toLowerCase().includes('icon'), +) + +for (const icon of icons) { + if (!icon.name.startsWith('icons/')) { + icon.name = `icons/${icon.name}` + } +} + +console.log(`Prefixed ${icons.length} icon layers`) +figma.closePlugin() +``` diff --git a/plugins/figma/skills/figma-use-figjam/references/create-code-block.md b/plugins/figma/skills/figma-use-figjam/references/create-code-block.md new file mode 100644 index 00000000..c61a01f6 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-code-block.md @@ -0,0 +1,89 @@ +# Create Code Blocks + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating and configuring FigJam code block nodes. + +**Scope:** Code blocks are FigJam-specific nodes created with `figma.createCodeBlock()`. They render code content with syntax highlighting and a monospace font. `CODE_BLOCK` is a first-class node type — not a shape or text node. + +## Creating a Code Block + +```javascript +// Snapshot existing children before creating the node — createCodeBlock() auto-appends to the page +const existingNodes = figma.currentPage.children.slice() + +const cb = figma.createCodeBlock() +cb.code = 'const greeting = "Hello, FigJam!"' +cb.codeLanguage = 'JAVASCRIPT' + +// Position away from (0,0) — find clear space to the right of existing content +const rightEdge = existingNodes.length > 0 ? Math.max(...existingNodes.map((n) => n.x + n.width)) : 0 +cb.x = rightEdge + 100 +cb.y = 100 + +return { id: cb.id, x: cb.x, y: cb.y } +``` + +## Supported Languages (`codeLanguage`) + +Pass one of these exact uppercase string values. Omitting `codeLanguage` defaults to `PLAINTEXT`. + +| Value | Language | +|---|---| +| `TYPESCRIPT` | TypeScript | +| `JAVASCRIPT` | JavaScript | +| `PYTHON` | Python | +| `GO` | Go | +| `RUST` | Rust | +| `RUBY` | Ruby | +| `CSS` | CSS | +| `HTML` | HTML | +| `JSON` | JSON | +| `GRAPHQL` | GraphQL | +| `SQL` | SQL | +| `SWIFT` | Swift | +| `KOTLIN` | Kotlin | +| `CPP` | C++ | +| `BASH` | Bash / Shell | +| `PLAINTEXT` | Plain text (no highlighting) | + +If the user specifies a language not in this list, use `PLAINTEXT`. + +## Setting Code Content + +The `code` property maps to the node's text sublayer — set it after creating the node: + +```javascript +const cb = figma.createCodeBlock() +cb.code = `function add(a, b) { + return a + b +}` +cb.codeLanguage = 'TYPESCRIPT' +return { id: cb.id } +``` + +## Positioning Within a Section + +To place a code block inside a FigJam section, append it to the section instead of the page: + +```javascript +const section = figma.currentPage.findOne((n) => n.type === 'SECTION' && n.name === 'My Section') +if (!section) throw new Error('Section not found') + +const cb = figma.createCodeBlock() +cb.code = 'SELECT * FROM users WHERE active = true' +cb.codeLanguage = 'SQL' + +section.appendChild(cb) + +// Position relative to section origin +cb.x = 40 +cb.y = 40 + +return { id: cb.id } +``` + +## Important Notes + +- `CODE_BLOCK` is **FigJam-only** — this will throw in Figma design files. +- There is no theme/color API for code blocks; FigJam handles the visual styling automatically. +- Always `return` the created node's `id` for reference in follow-up calls (see figma-use rule #15). +- No font loading is required — code blocks handle their own monospace rendering. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-connector.md b/plugins/figma/skills/figma-use-figjam/references/create-connector.md new file mode 100644 index 00000000..fe441106 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-connector.md @@ -0,0 +1,328 @@ +# Create Connectors + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating connectors between nodes — endpoints, arrows, line types, labels, and colors. + +**Scope:** Connectors are FigJam-specific nodes created with `figma.createConnector()`. They connect shapes, stickies, sections, and other nodes to show relationships. For creating shapes to connect, see [create-shape-with-text](create-shape-with-text.md). For stickies, see [create-sticky](create-sticky.md). For sections, see [create-section](create-section.md). + +## Creating a Connector Between Two Nodes + +```javascript +const connector = figma.createConnector() +connector.connectorStart = { endpointNodeId: '123:456', magnet: 'AUTO' } +connector.connectorEnd = { endpointNodeId: '123:789', magnet: 'AUTO' } + +console.log('Created connector:', connector.id) +figma.closePlugin() +``` + +## Connector Endpoints + +Endpoints define where the connector starts and ends. There are three forms: + +### Attached to a node with auto-magnet (most common) + +```javascript +connector.connectorStart = { endpointNodeId: nodeA.id, magnet: 'AUTO' } +connector.connectorEnd = { endpointNodeId: nodeB.id, magnet: 'AUTO' } +``` + +### Attached to a node at a specific side + +Magnet values: `'AUTO'`, `'TOP'`, `'BOTTOM'`, `'LEFT'`, `'RIGHT'`, `'CENTER'`, `'NONE'` + +```javascript +connector.connectorStart = { endpointNodeId: nodeA.id, magnet: 'RIGHT' } +connector.connectorEnd = { endpointNodeId: nodeB.id, magnet: 'LEFT' } +``` + +### Floating (not attached to any node) + +```javascript +connector.connectorStart = { position: { x: 100, y: 200 } } +connector.connectorEnd = { position: { x: 400, y: 200 } } +``` + +### Attached to a node at a specific position (relative, 0–1) + +```javascript +connector.connectorStart = { endpointNodeId: nodeA.id, position: { x: 1, y: 0.5 } } +connector.connectorEnd = { endpointNodeId: nodeB.id, position: { x: 0, y: 0.5 } } +``` + +## Line Types + +```javascript +connector.connectorLineType = 'ELBOWED' // Right-angle bends (default) +connector.connectorLineType = 'STRAIGHT' // Direct line +connector.connectorLineType = 'CURVED' // Smooth curve +``` + +## Stroke Caps (Arrows) + +Control the arrowheads at each end of the connector. + +Available cap styles: `'NONE'`, `'ARROW_LINES'`, `'ARROW_EQUILATERAL'`, `'TRIANGLE_FILLED'`, `'DIAMOND_FILLED'`, `'CIRCLE_FILLED'` + +```javascript +const connector = figma.createConnector() +connector.connectorStart = { endpointNodeId: nodeA.id, magnet: 'AUTO' } +connector.connectorEnd = { endpointNodeId: nodeB.id, magnet: 'AUTO' } + +// Arrow at the end only (most common for directed flows) +connector.connectorStartStrokeCap = 'NONE' +connector.connectorEndStrokeCap = 'ARROW_LINES' + +figma.closePlugin() +``` + +### Common arrow configurations + +```javascript +// One-way arrow (A → B) +connector.connectorStartStrokeCap = 'NONE' +connector.connectorEndStrokeCap = 'ARROW_LINES' + +// Two-way arrow (A ↔ B) +connector.connectorStartStrokeCap = 'ARROW_LINES' +connector.connectorEndStrokeCap = 'ARROW_LINES' + +// No arrows (plain line) +connector.connectorStartStrokeCap = 'NONE' +connector.connectorEndStrokeCap = 'NONE' + +// Filled triangle arrow +connector.connectorEndStrokeCap = 'ARROW_EQUILATERAL' + +// Diamond endpoint +connector.connectorStartStrokeCap = 'DIAMOND_FILLED' + +// Circle endpoint +connector.connectorStartStrokeCap = 'CIRCLE_FILLED' +``` + +## Adding a Text Label + +Connectors have a `text` sublayer for visible labels. You must load fonts before setting text. + +**CRITICAL**: To display text on a connector, set `connector.text.characters` — NOT `connector.name`. Setting `connector.name` only changes the layer name in the layers panel and is NOT visible on the canvas. + +**CRITICAL**: A newly created connector's `text.fontName` is **invalid by default** — calling `figma.loadFontAsync(connector.text.fontName)` will fail. You must explicitly set `connector.text.fontName` to a known font (after loading it), then set `connector.text.characters`. + +```javascript +const font = { family: 'Inter', style: 'Medium' } +await figma.loadFontAsync(font) + +const connector = figma.createConnector() +connector.connectorStart = { endpointNodeId: nodeA.id, magnet: 'AUTO' } +connector.connectorEnd = { endpointNodeId: nodeB.id, magnet: 'AUTO' } + +// Explicitly set the font, then set text +connector.text.fontName = font +connector.text.characters = 'depends on' // This is the visible label + +figma.closePlugin() +``` + +### Modifying label on an existing connector + +For existing connectors that already have text, `text.fontName` is valid and can be loaded directly: + +```javascript +const connector = await figma.getNodeByIdAsync('123:456') +if (connector && connector.type === 'CONNECTOR') { + await figma.loadFontAsync(connector.text.fontName) + connector.text.characters = 'new label' +} +figma.closePlugin() +``` + +## Color Presets + +Connectors use the same FigJam color palette as shapes. The line color is set via `strokes`. The connector's text label has its own background and its color does **not** change when the line color changes — only set the stroke. + +**CRITICAL**: Use `hex/255` notation (e.g. `0x66/255`) for exact palette matching — rounded decimals cause FigJam to treat the color as "custom". + +| Color | Hex | +| ---------- | --------- | +| Black | `#1E1E1E` | +| Dark gray | `#757575` | +| Gray | `#B3B3B3` | +| Light gray | `#D9D9D9` | +| Green | `#66D575` | +| Teal | `#5AD8CC` | +| Blue | `#3DADFF` | +| Violet | `#874FFF` | +| Pink | `#F849C1` | +| Red | `#FF7556` | +| Orange | `#FF9E42` | +| Yellow | `#FFC943` | +| White | `#FFFFFF` | + +### Setting a Connector's Color + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const connector = figma.createConnector() +connector.connectorStart = { endpointNodeId: nodeA.id, magnet: 'AUTO' } +connector.connectorEnd = { endpointNodeId: nodeB.id, magnet: 'AUTO' } +connector.strokes = [{ type: 'SOLID', color: h(0x3d, 0xad, 0xff) }] // Blue #3DADFF + +figma.closePlugin() +``` + +### Changing color on an existing connector + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const connector = await figma.getNodeByIdAsync('123:456') +if (connector && connector.type === 'CONNECTOR') { + connector.strokes = [{ type: 'SOLID', color: h(0xff, 0x75, 0x56) }] // Red #FF7556 +} +figma.closePlugin() +``` + +## Stroke Weight and Dash Pattern + +```javascript +const connector = figma.createConnector() +connector.connectorStart = { endpointNodeId: nodeA.id, magnet: 'AUTO' } +connector.connectorEnd = { endpointNodeId: nodeB.id, magnet: 'AUTO' } + +connector.strokeWeight = 2 + +// Dashed line +connector.dashPattern = [10, 5] + +// Dotted line +connector.dashPattern = [2, 4] + +// Solid line (default) +connector.dashPattern = [] + +figma.closePlugin() +``` + +## Finding Connectors Attached to a Node + +Every node with connectors has an `attachedConnectors` property: + +```javascript +const node = await figma.getNodeByIdAsync('123:456') +if (node && 'attachedConnectors' in node) { + for (const conn of node.attachedConnectors) { + console.log('Connector:', conn.id, 'type:', conn.connectorLineType) + console.log(' start:', JSON.stringify(conn.connectorStart)) + console.log(' end:', JSON.stringify(conn.connectorEnd)) + console.log(' label:', conn.text.characters) + } +} +figma.closePlugin() +``` + +## Batch Creation: Connecting a Chain of Nodes with Labels + +```javascript +const nodeIds = ['1:10', '1:20', '1:30', '1:40'] +const labels = ['Step 1→2', 'Step 2→3', 'Step 3→4'] +const font = { family: 'Inter', style: 'Regular' } +await figma.loadFontAsync(font) + +for (let i = 0; i < nodeIds.length - 1; i++) { + const connector = figma.createConnector() + connector.connectorStart = { endpointNodeId: nodeIds[i], magnet: 'AUTO' } + connector.connectorEnd = { endpointNodeId: nodeIds[i + 1], magnet: 'AUTO' } + connector.connectorStartStrokeCap = 'NONE' + connector.connectorEndStrokeCap = 'ARROW_LINES' + + // Set visible label (not connector.name, which is just the layer name) + connector.text.fontName = font + connector.text.characters = labels[i] +} + +figma.closePlugin() +``` + +## Batch Creation: Flowchart with Shapes and Connectors + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const preset = { + fill: h(0xc2, 0xe5, 0xff), // Light blue + stroke: h(0x3d, 0xad, 0xff), // Blue + text: h(0x1e, 0x1e, 0x1e), // Dark +} + +const steps = ['Start', 'Process', 'Review', 'Done'] +const shapeW = 160 +const shapeH = 80 +const spacing = 80 + +const totalWidth = steps.length * shapeW + (steps.length - 1) * spacing +const startX = 0 + +const nodes = [] +for (let i = 0; i < steps.length; i++) { + const shape = figma.createShapeWithText() + await figma.loadFontAsync(shape.text.fontName) + shape.text.characters = steps[i] + shape.resize(shapeW, shapeH) + shape.fills = [{ type: 'SOLID', color: preset.fill }] + shape.strokes = [{ type: 'SOLID', color: preset.stroke }] + shape.text.fills = [{ type: 'SOLID', color: preset.text }] + shape.x = startX + i * (shapeW + spacing) + nodes.push(shape) +} + +for (let i = 0; i < nodes.length - 1; i++) { + const connector = figma.createConnector() + connector.connectorStart = { endpointNodeId: nodes[i].id, magnet: 'AUTO' } + connector.connectorEnd = { endpointNodeId: nodes[i + 1].id, magnet: 'AUTO' } + connector.connectorStartStrokeCap = 'NONE' + connector.connectorEndStrokeCap = 'ARROW_LINES' + connector.strokes = [{ type: 'SOLID', color: preset.stroke }] +} + +figma.closePlugin() +``` + +## Batch Creation: Star/Hub Pattern (One Node to Many) + +```javascript +const hubId = '1:100' +const spokeIds = ['1:200', '1:201', '1:202', '1:203'] + +for (const spokeId of spokeIds) { + const connector = figma.createConnector() + connector.connectorStart = { endpointNodeId: hubId, magnet: 'AUTO' } + connector.connectorEnd = { endpointNodeId: spokeId, magnet: 'AUTO' } + connector.connectorEndStrokeCap = 'ARROW_LINES' +} + +figma.closePlugin() +``` + +## Cloning Connectors + +```javascript +const original = await figma.getNodeByIdAsync('123:456') +if (original && original.type === 'CONNECTOR') { + const clone = original.clone() + console.log('Cloned connector:', clone.id) +} +figma.closePlugin() +``` + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Visible text = `connector.text.characters`**, NOT `connector.name`. `name` is only the layer name in the panel — it does not appear on the canvas. +- **Connector text needs explicit font setup.** A new connector's `text.fontName` is invalid by default — load a known font, set `connector.text.fontName`, then set `connector.text.characters`. For existing connectors with text, `text.fontName` is valid and can be loaded directly. +- **Use `magnet: 'AUTO'`** for most cases — Figma picks the best attachment point. +- **Only set `strokes` for connector color** — the text label color does not change with the line color. +- **Default caps**: start = `'NONE'`, end = `'ARROW_LINES'` — explicitly set both if you want a different configuration. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. +- **Use `attachedConnectors`** to find existing connectors on a node. +- **Verify changes** by logging before/after values and exporting images when supported. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-label.md b/plugins/figma/skills/figma-use-figjam/references/create-label.md new file mode 100644 index 00000000..4eb2aeac --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-label.md @@ -0,0 +1,276 @@ +# Create Label Nodes + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating small circle callout markers with a number or letter. + +**Scope:** Label nodes are small fixed-size circle shapes containing a single number or letter, used as callout markers, step indicators, or annotation anchors on a FigJam board. They are created with `figma.createShapeWithText()` using `shapeType = 'ELLIPSE'` and a fixed size. For shapes that need to fit longer text content, see [create-shape-with-text](create-shape-with-text.md). + +**When to use labels:** Annotating steps in a process, numbering items on a diagram, marking locations on a map or wireframe, or providing lettered callouts that reference an accompanying legend. + +**When NOT to use labels:** If the content is more than 2 characters (e.g. a word or phrase), use a regular [shape-with-text](create-shape-with-text.md) instead. + +## Creating a Label + +```javascript +// Position the label — determine a location relative to existing content +const labelLocation = { x: 100, y: 100 } + +const label = figma.createShapeWithText() +label.shapeType = 'ELLIPSE' + +await figma.loadFontAsync(label.text.fontName) +label.text.characters = '1' + +// Labels use a fixed size — do NOT use fitShapeToText +label.resize(48, 48) +label.text.fontSize = 20 + +label.x = labelLocation.x +label.y = labelLocation.y + +figma.currentPage.appendChild(label) +return { id: label.id, x: label.x, y: label.y } +``` + +## Size + +Labels use **fixed dimensions** — do not use `fitShapeToText` (that utility is for shapes with variable-length text). + +| Content | Width × Height | Font size | +| ---------------------- | -------------- | --------- | +| Single char (`1`, `A`) | 48 × 48 | 20 | +| Two chars (`10`, `AB`) | 64 × 64 | 20 | + +Both width and height must always be equal (square bounding box) so the ellipse renders as a perfect circle. + +## Color Presets + +Labels use the same coordinated fill/stroke/text color system as other FigJam shapes. Always set all three together. Use `hex/255` notation for exact palette matching — rounded decimals cause FigJam to treat the color as "custom". + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const WHITE = h(0xff, 0xff, 0xff) +const DARK = h(0x1e, 0x1e, 0x1e) + +const LABEL_COLOR_PRESETS = { + black: { fill: h(0x1e, 0x1e, 0x1e), stroke: h(0xb3, 0xb3, 0xb3), text: WHITE }, + darkGray: { fill: h(0x75, 0x75, 0x75), stroke: h(0x5e, 0x5e, 0x5e), text: WHITE }, + green: { fill: h(0x66, 0xd5, 0x75), stroke: h(0x3e, 0x9b, 0x4b), text: WHITE }, + teal: { fill: h(0x5a, 0xd8, 0xcc), stroke: h(0x36, 0x9e, 0x94), text: WHITE }, + blue: { fill: h(0x3d, 0xad, 0xff), stroke: h(0x00, 0x7a, 0xd2), text: WHITE }, + violet: { fill: h(0x87, 0x4f, 0xff), stroke: h(0x54, 0x27, 0xb4), text: WHITE }, + pink: { fill: h(0xf8, 0x49, 0xc1), stroke: h(0xb4, 0x24, 0x87), text: WHITE }, + red: { fill: h(0xff, 0x75, 0x56), stroke: h(0xdc, 0x30, 0x09), text: WHITE }, + orange: { fill: h(0xff, 0x9e, 0x42), stroke: h(0xeb, 0x75, 0x00), text: WHITE }, + gray: { fill: h(0xb3, 0xb3, 0xb3), stroke: h(0x8f, 0x8f, 0x8f), text: DARK }, + lightGray: { fill: h(0xd9, 0xd9, 0xd9), stroke: h(0xb3, 0xb3, 0xb3), text: DARK }, + yellow: { fill: h(0xff, 0xc9, 0x43), stroke: h(0xe8, 0xa3, 0x02), text: DARK }, + white: { fill: h(0xff, 0xff, 0xff), stroke: h(0xb3, 0xb3, 0xb3), text: DARK }, +} + +function applyLabelColor(label, preset) { + label.fills = [{ type: 'SOLID', color: preset.fill }] + label.strokes = [{ type: 'SOLID', color: preset.stroke }] + label.text.fills = [{ type: 'SOLID', color: preset.text }] +} +``` + +### Applying a color + +```javascript +const label = figma.createShapeWithText() +label.shapeType = 'ELLIPSE' +await figma.loadFontAsync(label.text.fontName) +label.text.characters = '1' +label.resize(48, 48) +label.text.fontSize = 20 +applyLabelColor(label, LABEL_COLOR_PRESETS.blue) + +figma.closePlugin() +``` + +## Batch Creation: Numbered Sequence + +The most common use case is a horizontal row of numbered labels. Use a two-pass layout: create all labels first, then position them using their actual dimensions. + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const WHITE = h(0xff, 0xff, 0xff) +const PRESET_BLUE = { fill: h(0x3d, 0xad, 0xff), stroke: h(0x00, 0x7a, 0xd2), text: WHITE } + +const count = 5 +const size = 48 +const spacing = 16 +const labelLocation = { x: 100, y: 100 } + +// Pass 1: create all labels +const labels = [] +for (let i = 1; i <= count; i++) { + const label = figma.createShapeWithText() + label.shapeType = 'ELLIPSE' + await figma.loadFontAsync(label.text.fontName) + label.text.characters = String(i) + label.resize(size, size) + label.text.fontSize = 20 + label.fills = [{ type: 'SOLID', color: PRESET_BLUE.fill }] + label.strokes = [{ type: 'SOLID', color: PRESET_BLUE.stroke }] + label.text.fills = [{ type: 'SOLID', color: PRESET_BLUE.text }] + labels.push(label) +} + +// Pass 2: position in a horizontal row +let curX = labelLocation.x +for (const label of labels) { + label.x = curX + label.y = labelLocation.y + curX += size + spacing +} + +return labels.map((l) => ({ id: l.id })) +``` + +## Batch Creation: Lettered Sequence + +Same two-pass pattern as the numbered sequence, using `String.fromCharCode` to generate A, B, C… + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const WHITE = h(0xff, 0xff, 0xff) +const PRESET_VIOLET = { fill: h(0x87, 0x4f, 0xff), stroke: h(0x54, 0x27, 0xb4), text: WHITE } + +const letters = ['A', 'B', 'C', 'D', 'E'] + +const size = 48 +const spacing = 16 +const labelLocation = { x: 100, y: 100 } + +// Pass 1: create all labels +const labels = [] +for (const letter of letters) { + const label = figma.createShapeWithText() + label.shapeType = 'ELLIPSE' + await figma.loadFontAsync(label.text.fontName) + label.text.characters = letter + label.resize(size, size) + label.text.fontSize = 20 + label.fills = [{ type: 'SOLID', color: PRESET_VIOLET.fill }] + label.strokes = [{ type: 'SOLID', color: PRESET_VIOLET.stroke }] + label.text.fills = [{ type: 'SOLID', color: PRESET_VIOLET.text }] + labels.push(label) +} + +// Pass 2: position in a horizontal row +let curX = labelLocation.x +for (const label of labels) { + label.x = curX + label.y = labelLocation.y + curX += size + spacing +} + +return labels.map((l) => ({ id: l.id })) +``` + +## Positioning Relative to an Existing Node + +Labels are most often placed adjacent to the node they're annotating. Use the target node's bounds to derive `labelLocation`: + +```javascript +// Place a label at the top-right corner of an existing node +const targetNode = figma.getNodeById(targetNodeId) +if (!targetNode) throw new Error('Node not found') + +const label = figma.createShapeWithText() +label.shapeType = 'ELLIPSE' +await figma.loadFontAsync(label.text.fontName) +label.text.characters = '1' +label.resize(48, 48) +label.text.fontSize = 20 + +// Top-left corner, offset so the label overlaps the corner slightly +label.x = targetNode.x - label.width / 2 +label.y = targetNode.y - label.height / 2 + +figma.currentPage.appendChild(label) +return { id: label.id } +``` + +## Label + Sticky Legend + +When annotations need descriptive text (e.g. "1. Introduction", "A. Problem statement"), place label circles on or near the target nodes as markers, then group the corresponding stickies in a cluster nearby — offset 200–300px below (or to the side of) the labeled content. The stickies act as a legend; the labels are the pins. Do NOT place the sticky immediately adjacent to its label circle — if they're glued together there's no need for the circle at all. + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const WHITE = h(0xff, 0xff, 0xff) +const PRESET_BLUE = { fill: h(0x3d, 0xad, 0xff), stroke: h(0x00, 0x7a, 0xd2), text: WHITE } + +const annotations = [ + { number: '1', text: 'Introduction' }, + { number: '2', text: 'Problem statement' }, + { number: '3', text: 'Proposed solution' }, +] + +// targetNodes: the nodes being annotated, one per annotation +// (derive from node IDs passed in the user message) + +// Pass 1: create labels and stickies +const pairs = [] +for (const item of annotations) { + const label = figma.createShapeWithText() + label.shapeType = 'ELLIPSE' + await figma.loadFontAsync(label.text.fontName) + label.text.characters = item.number + label.resize(48, 48) + label.text.fontSize = 20 + label.fills = [{ type: 'SOLID', color: PRESET_BLUE.fill }] + label.strokes = [{ type: 'SOLID', color: PRESET_BLUE.stroke }] + label.text.fills = [{ type: 'SOLID', color: PRESET_BLUE.text }] + + const sticky = figma.createSticky() + await figma.loadFontAsync(sticky.text.fontName) + sticky.text.characters = `${item.number}. ${item.text}` + + pairs.push({ label, sticky }) +} + +// Pass 2: place labels on their target nodes (top-left corner) +for (let i = 0; i < pairs.length; i++) { + const targetNode = targetNodes[i] + pairs[i].label.x = targetNode.x - 24 + pairs[i].label.y = targetNode.y - 24 +} + +// Pass 3: cluster stickies in a vertical column to the right of the labeled content. +// Use the right edge of the target nodes as the anchor, then push further right past +// any existing nodes that overlap vertically with the legend area. +const targetRight = Math.max(...targetNodes.map((n) => n.x + n.width)) +const targetTop = Math.min(...targetNodes.map((n) => n.y)) +const targetBottom = Math.max(...targetNodes.map((n) => n.y + n.height)) + +// Find the rightmost edge of any page node that overlaps vertically with the legend area +const legendGap = 250 +const conflictRight = figma.currentPage.children + .filter((n) => n.y < targetBottom + legendGap && n.y + n.height > targetTop) + .reduce((max, n) => Math.max(max, n.x + n.width), targetRight) + +const legendX = conflictRight + legendGap +const stickySpacing = 32 +let curY = targetTop +for (const { sticky } of pairs) { + sticky.x = legendX + sticky.y = curY + curY += sticky.height + stickySpacing +} + +return pairs.map(({ label, sticky }) => ({ labelId: label.id, stickyId: sticky.id })) +``` + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Load fonts** before setting `label.text.characters`. Always use `await figma.loadFontAsync(label.text.fontName)`, never hardcode the font name. +- **Use fixed size — do NOT use `fitShapeToText`.** Labels are compact by design; their size is fixed at 48×48 (single char) or 64×64 (two chars). +- **Width must equal height** so the ELLIPSE renders as a perfect circle. +- **Set `fontSize` explicitly** after loading the font to ensure the character is legible in the small circle. +- **Set fill, stroke, AND text color together** — setting only fills leaves mismatched stroke/text colors. +- **Use `shapeType = 'ELLIPSE'`** — the default shapeType is also `'ELLIPSE'`, but set it explicitly for clarity. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-section.md b/plugins/figma/skills/figma-use-figjam/references/create-section.md new file mode 100644 index 00000000..0f133224 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-section.md @@ -0,0 +1,181 @@ +# Create Sections + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating, modifying, and organizing sections. + +**Scope:** Sections are FigJam containers created with `figma.createSection()`. They organize related objects on the board. For creating stickies to place inside sections, see [create-sticky](create-sticky.md). For creating text to place inside sections, see [create-text](create-text.md). + +## Creating a Section + +Create sections and resize them carefully according to the guidance in the [figma-plan-figjam-board-content](../../figma-plan-figjam-board-content/SKILL.md) skill's layout reference. + +```javascript +const section = figma.createSection() +section.name = 'My Section' + +// Sections start very small — resize to a usable size +section.resize(400, 300) + +console.log('Created section:', section.id, section.name, section.width, 'x', section.height) +figma.closePlugin() +``` + +## Stickies vs. Text Nodes as section content + +Stickies and text play different roles. Before adding section child content, make sure to read and understand the usage guidance for each in [create-sticky](create-sticky.md) and [create-text](create-text.md) skills. + +## Naming + +Section names should be **short, navigational identifiers** (e.g. "Brainstorm", "Action Items", "Went Well") — they are used for browsing and quick identification in FigJam's UI. The section name is NOT the user-facing header. Create a separate **H2 text node** inside the section for the visible, descriptive header (see [figma-plan-figjam-board-content](../../figma-plan-figjam-board-content/SKILL.md) for the section name vs header distinction). + +```javascript +const section = figma.createSection() +section.name = 'What went well' // Short navigational name + +console.log('Section name:', section.name) +figma.closePlugin() +``` + +To rename an existing section: + +```javascript +const section = await figma.getNodeByIdAsync('123:456') +if (section && section.type === 'SECTION') { + console.log('Before:', section.name) + section.name = 'Updated name' + console.log('After:', section.name) +} +figma.closePlugin() +``` + +## Resizing + +Sections support both `resize(width, height)` and `resizeWithoutConstraints(width, height)`. **Prefer `resize(...)`** — it matches the ergonomics of every other resizable node. Sections don't propagate constraints to their children, so the two methods behave identically on sections. Both width and height must be >= 0.01. + +```javascript +const section = figma.createSection() +section.name = 'Wide section' +section.resize(800, 400) + +console.log('Size:', section.width, 'x', section.height) +figma.closePlugin() +``` + +### Resizing an Existing Section + +Often when creating a section and adding content, the content will exceed the bounds of the section. To solve that, find the maximum extents of the section's children using their section-local coordinates, then resize the section to fit. Consider adding padding of at least 32px on all sides of the content within the section to prevent the content from appearing cramped. + +Do not resize the section to hug its contents if it is meant to be an INTERACTIVE section as described by the [figma-plan-figjam-board-content](../../figma-plan-figjam-board-content/SKILL.md) skill. Also do not resize sections to hug content when they are part of a **grid layout** — sections in a grid must maintain uniform dimensions to preserve the rectangular appearance (see [layout-figjam-board-content](../../figma-plan-figjam-board-content/references/layout-figjam-board-content.md)). + +```javascript +const section = await figma.getNodeByIdAsync('123:456') +if (section && section.type === 'SECTION') { + if (section.children.length < 1) { + // for empty sections, choose a reasonable width and height based on the purpose + section.resize(800, 400) + figma.closePlugin() + return + } + console.log('Before:', section.width, 'x', section.height) + + // Children's x/y are in section-local coordinates, so find the max extents from (0,0) + let maxRight = 0 + let maxBottom = 0 + for (const child of section.children) { + maxRight = Math.max(maxRight, child.x + child.width) + maxBottom = Math.max(maxBottom, child.y + child.height) + } + + const padding = 32 + section.resize(maxRight + padding, maxBottom + padding) + console.log('After:', section.width, 'x', section.height) +} +figma.closePlugin() +``` + +## Color Palette + +FigJam sections share the shape/connector color palette. Set via the `fills` property. Sections typically use lighter colors as background fills. + +When creating multiple sections, **vary the colors** across the palette to visually distinguish them — don't use the same color for every section. Only apply default color variety when the user hasn't specified colors. + +**CRITICAL**: Use `hex/255` notation (e.g. `0xF5/255`) for exact palette matching — rounded decimals cause FigJam to treat the color as "custom" instead of a palette color. + +| Color | Hex | +| ------------ | --------- | +| White | `#FFFFFF` | +| Light gray | `#F9F9F9` | +| Light green | `#EBFFEE` | +| Light teal | `#F1FEFD` | +| Light blue | `#F5FBFF` | +| Light violet | `#F8F5FF` | +| Light pink | `#FFF0FA` | +| Light red | `#FFF5F5` | +| Light orange | `#FFF7F0` | +| Light yellow | `#FFFBF0` | + +### Setting a Section's Color + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const section = figma.createSection() +section.name = 'Blue section' +section.resize(400, 300) +section.fills = [{ type: 'SOLID', color: h(0xf5, 0xfb, 0xff) }] // Light blue #F5FBFF + +figma.closePlugin() +``` + +### Changing the Color of an Existing Section + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const section = await figma.getNodeByIdAsync('123:456') +if (section && section.type === 'SECTION') { + console.log('Before:', JSON.stringify(section.fills)) + section.fills = [{ type: 'SOLID', color: h(0xeb, 0xff, 0xee) }] // Light green #EBFFEE + console.log('After:', JSON.stringify(section.fills)) +} +figma.closePlugin() +``` + +## Hiding Section Contents + +Toggle whether a section's child nodes are visible: + +```javascript +const section = await figma.getNodeByIdAsync('123:456') +if (section && section.type === 'SECTION') { + console.log('Contents hidden before:', section.sectionContentsHidden) + section.sectionContentsHidden = true + console.log('Contents hidden after:', section.sectionContentsHidden) +} +figma.closePlugin() +``` + +## Adding Nodes to a Section + +**CRITICAL**: It's very important that you follow the instructions in [position-figjam-nodes](position-figjam-nodes.md): Adding Nodes to a Section. This is _crucial_ for a high-quality output. + +## Cloning Sections + +```javascript +const original = await figma.getNodeByIdAsync('123:456') +if (original && original.type === 'SECTION') { + const clone = original.clone() + clone.x = original.x + original.width + 32 + clone.name = original.name + ' (copy)' + console.log('Cloned section:', clone.id, clone.name) +} +figma.closePlugin() +``` + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Use `section.resize(width, height)`** to set section size — `width`/`height` are read-only. Sections also accept `resizeWithoutConstraints(...)`, but `resize(...)` is the preferred method. +- **Resize sections to fit their children.** After adding children to a section, make sure that the section encompasses the children. If you need to resize it, refer to the example of resizing an existing section. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. +- **Verify changes** by logging before/after values and exporting images when supported. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-shape-with-text.md b/plugins/figma/skills/figma-use-figjam/references/create-shape-with-text.md new file mode 100644 index 00000000..9c381e07 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-shape-with-text.md @@ -0,0 +1,358 @@ +# Create Shapes with Text + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating shapes with embedded text for diagrams and visual layouts. + +**Scope:** ShapeWithText nodes are FigJam-specific geometric shapes with built-in text, created with `figma.createShapeWithText()`. For tables, see [create-table](create-table.md). For sections, see [create-section](create-section.md). For stickies, see [create-sticky](create-sticky.md). + +**When NOT to use this skill:** For tabular-data (e.g. data tables, spreadsheets, comparison tables, rosters, or any row/column grid of text or data), use the [create-table](create-table.md) skill instead. Do not build a table-like layout from a grid of shapes. + +## Creating a Shape + +```javascript +const shape = figma.createShapeWithText() +// Default shapeType is 'ELLIPSE' + +await figma.loadFontAsync(shape.text.fontName) +shape.text.characters = 'Step 1' + +console.log('Created shape:', shape.id, shape.shapeType, shape.text.characters) +figma.closePlugin() +``` + +## Shape Types + +Set the `shapeType` property **after** creation. It defaults to `'ELLIPSE'`. + +```javascript +const shape = figma.createShapeWithText() +shape.shapeType = 'DIAMOND' +``` + +Available shape types: + +| Category | Shape types | +| ----------------- | ------------------------------------------------------------------------------------------------------------------------ | +| Basic | `SQUARE`, `ELLIPSE`, `ROUNDED_RECTANGLE`, `DIAMOND`, `TRIANGLE_UP`, `TRIANGLE_DOWN` | +| Arrows & Chevrons | `ARROW_LEFT`, `ARROW_RIGHT`, `CHEVRON`, `PENTAGON`, `HEXAGON`, `OCTAGON` | +| Flowchart | `PARALLELOGRAM_RIGHT`, `PARALLELOGRAM_LEFT`, `TRAPEZOID`, `PREDEFINED_PROCESS`, `MANUAL_INPUT`, `SUMMING_JUNCTION`, `OR` | +| Engineering | `ENG_DATABASE` (Cylinder), `ENG_QUEUE` (Horizontal cylinder), `ENG_FILE` (File), `ENG_FOLDER` (Folder) | +| Other | `SHIELD`, `DOCUMENT_SINGLE`, `DOCUMENT_MULTIPLE`, `SPEECH_BUBBLE`, `STAR`, `PLUS`, `INTERNAL_STORAGE` | + +### Creating Different Shape Types + +```javascript +const types = ['SQUARE', 'DIAMOND', 'ELLIPSE', 'ROUNDED_RECTANGLE'] +const shapes = [] +for (const type of types) { + const s = figma.createShapeWithText() + s.shapeType = type + await figma.loadFontAsync(s.text.fontName) + s.text.characters = type + shapes.push(s) +} +figma.closePlugin() +``` + +## Setting Text + +ShapeWithText nodes expose a `text` sublayer (a `TextSublayerNode`). The default font is **"Inter Medium"** (not "Inter Regular"). You must load the shape's own font before changing text. **Never hardcode a font name** — always read it from `shape.text.fontName`. + +**Put all text content directly into `shape.text.characters`.** Do not split text into a short label and a separate description field — all content the user expects to see in the shape must be set as the characters. The `fitShapeToText` utility will automatically size the shape to fit the full text. + +```javascript +const shape = figma.createShapeWithText() +await figma.loadFontAsync(shape.text.fontName) +shape.text.characters = 'Decision?' + +figma.closePlugin() +``` + +To modify text on an existing shape: + +```javascript +const shape = await figma.getNodeByIdAsync('123:456') +if (shape && shape.type === 'SHAPE_WITH_TEXT') { + await figma.loadFontAsync(shape.text.fontName) + console.log('Before:', shape.text.characters) + shape.text.characters = 'Updated label' + console.log('After:', shape.text.characters) +} +figma.closePlugin() +``` + +## Color Presets + +FigJam shapes have **coordinated fill, stroke, and text colors**. When applying a color, you must set all three to match the FigJam palette — otherwise the shape will look wrong (e.g., dark text on a dark fill, or missing stroke). Strongly prefer colors from this list instead of custom colors. + +Each color preset defines: + +- **Fill**: the shape's background color (`shape.fills`) +- **Stroke**: the shape's outline color (`shape.strokes`) +- **Text**: the text color (`shape.text.fills` — set after loading fonts) + +### Color Preset Map + +Use this map in your code to apply coordinated colors. **CRITICAL**: Use `hex/255` notation (e.g. `0x66/255`) for exact palette matching — rounded decimals cause FigJam to treat the color as "custom" instead of a palette color. + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const WHITE = h(0xff, 0xff, 0xff) +const DARK = h(0x1e, 0x1e, 0x1e) + +const SHAPE_COLOR_PRESETS = { + // Dark fills use white text; stroke uses darker variant + black: { fill: h(0x1e, 0x1e, 0x1e), stroke: h(0xb3, 0xb3, 0xb3), text: WHITE }, + darkGray: { fill: h(0x75, 0x75, 0x75), stroke: h(0x5e, 0x5e, 0x5e), text: WHITE }, + green: { fill: h(0x66, 0xd5, 0x75), stroke: h(0x3e, 0x9b, 0x4b), text: WHITE }, + teal: { fill: h(0x5a, 0xd8, 0xcc), stroke: h(0x36, 0x9e, 0x94), text: WHITE }, + blue: { fill: h(0x3d, 0xad, 0xff), stroke: h(0x00, 0x7a, 0xd2), text: WHITE }, + violet: { fill: h(0x87, 0x4f, 0xff), stroke: h(0x54, 0x27, 0xb4), text: WHITE }, + pink: { fill: h(0xf8, 0x49, 0xc1), stroke: h(0xb4, 0x24, 0x87), text: WHITE }, + red: { fill: h(0xff, 0x75, 0x56), stroke: h(0xdc, 0x30, 0x09), text: WHITE }, + orange: { fill: h(0xff, 0x9e, 0x42), stroke: h(0xeb, 0x75, 0x00), text: WHITE }, + + // Light fills use dark text; stroke uses the corresponding dark variant + gray: { fill: h(0xb3, 0xb3, 0xb3), stroke: h(0x8f, 0x8f, 0x8f), text: DARK }, + lightGray: { fill: h(0xd9, 0xd9, 0xd9), stroke: h(0xb3, 0xb3, 0xb3), text: DARK }, + lightGreen: { fill: h(0xcd, 0xf4, 0xd3), stroke: h(0x66, 0xd5, 0x75), text: DARK }, + lightTeal: { fill: h(0xc6, 0xfa, 0xf6), stroke: h(0x5a, 0xd8, 0xcc), text: DARK }, + lightBlue: { fill: h(0xc2, 0xe5, 0xff), stroke: h(0x3d, 0xad, 0xff), text: DARK }, + lightViolet: { fill: h(0xdc, 0xcc, 0xff), stroke: h(0x87, 0x4f, 0xff), text: DARK }, + lightPink: { fill: h(0xff, 0xc2, 0xec), stroke: h(0xf8, 0x49, 0xc1), text: DARK }, + lightRed: { fill: h(0xff, 0xcd, 0xc2), stroke: h(0xff, 0x75, 0x56), text: DARK }, + lightOrange: { fill: h(0xff, 0xe0, 0xc2), stroke: h(0xff, 0x9e, 0x42), text: DARK }, + yellow: { fill: h(0xff, 0xc9, 0x43), stroke: h(0xe8, 0xa3, 0x02), text: DARK }, + lightYellow: { fill: h(0xff, 0xec, 0xbd), stroke: h(0xff, 0xc9, 0x43), text: DARK }, + white: { fill: h(0xff, 0xff, 0xff), stroke: h(0xb3, 0xb3, 0xb3), text: DARK }, +} +``` + +### Hex Reference + +| Color | Fill Hex | Stroke Hex | Text | +| ------------ | --------- | ---------- | ----- | +| Black | `#1E1E1E` | `#B3B3B3` | white | +| Dark gray | `#757575` | `#5E5E5E` | white | +| Gray | `#B3B3B3` | `#8F8F8F` | dark | +| Light gray | `#D9D9D9` | `#B3B3B3` | dark | +| Green | `#66D575` | `#3E9B4B` | white | +| Light green | `#CDF4D3` | `#66D575` | dark | +| Teal | `#5AD8CC` | `#369E94` | white | +| Light teal | `#C6FAF6` | `#5AD8CC` | dark | +| Blue | `#3DADFF` | `#007AD2` | white | +| Light blue | `#C2E5FF` | `#3DADFF` | dark | +| Violet | `#874FFF` | `#5427B4` | white | +| Light violet | `#DCCCFF` | `#874FFF` | dark | +| Pink | `#F849C1` | `#B42487` | white | +| Light pink | `#FFC2EC` | `#F849C1` | dark | +| Red | `#FF7556` | `#DC3009` | white | +| Light red | `#FFCDC2` | `#FF7556` | dark | +| Orange | `#FF9E42` | `#EB7500` | white | +| Light orange | `#FFE0C2` | `#FF9E42` | dark | +| Yellow | `#FFC943` | `#E8A302` | dark | +| Light yellow | `#FFECBD` | `#FFC943` | dark | +| White | `#FFFFFF` | `#B3B3B3` | dark | + +_white = `#FFFFFF`, dark = `#1E1E1E`_ + +### Applying a Color Preset + +Always set fill, stroke, and text color together: + +```javascript +function applyColorPreset(shape, preset) { + shape.fills = [{ type: 'SOLID', color: preset.fill }] + shape.strokes = [{ type: 'SOLID', color: preset.stroke }] + shape.text.fills = [{ type: 'SOLID', color: preset.text }] +} + +const shape = figma.createShapeWithText() +await figma.loadFontAsync(shape.text.fontName) +shape.text.characters = 'Start' +applyColorPreset(shape, SHAPE_COLOR_PRESETS.lightGreen) + +figma.closePlugin() +``` + +### Changing Color on an Existing Shape + +```javascript +const shape = await figma.getNodeByIdAsync('123:456') +if (shape && shape.type === 'SHAPE_WITH_TEXT') { + await figma.loadFontAsync(shape.text.fontName) + const preset = SHAPE_COLOR_PRESETS.lightBlue + shape.fills = [{ type: 'SOLID', color: preset.fill }] + shape.strokes = [{ type: 'SOLID', color: preset.stroke }] + shape.text.fills = [{ type: 'SOLID', color: preset.text }] +} +figma.closePlugin() +``` + +## Resizing + +Use `resize(width, height)` to change the size. Both values must be >= 0.01. `width` and `height` properties are **read-only**. `rescale(scale)` resizes proportionally from the top-left corner. + +## Sizing Shapes to Fit Text (REQUIRED) + +**CRITICAL: Never hardcode shape dimensions.** Default sizes are too small for most text and will clip. You **must** dynamically size every shape to fit its text content using a measurement TextNode. + +### How it works + +Create a temporary TextNode with `textAutoResize: 'HEIGHT'`, use it to measure how tall text will be at a given width, and scale shapes up until the text fits. Remove the measurer when done. + +### Utility code — include this in any code that creates shapes with text + +```javascript +const NON_RECT_TYPES = new Set([ + 'DIAMOND', + 'TRIANGLE_UP', + 'TRIANGLE_DOWN', + 'ELLIPSE', + 'HEXAGON', + 'OCTAGON', + 'STAR', + 'PENTAGON', +]) +const BASE_W = 200 +const BASE_H = 120 +const MAX_SCALE = 3 +const PADDING = 32 + +const measurer = figma.createText() +const SWT_FONT = { family: 'Inter', style: 'Medium' } +await figma.loadFontAsync(SWT_FONT) +measurer.fontName = SWT_FONT +measurer.textAutoResize = 'HEIGHT' + +function textAreaForShape(shapeType, w, h) { + if (NON_RECT_TYPES.has(shapeType)) { + return { w: w / 2 - PADDING, h: h / 2 - PADDING } + } + return { w: w - PADDING * 2, h: h - PADDING * 2 } +} + +function fitShapeToText(label, shapeType) { + let w = BASE_W + let h = BASE_H + if (NON_RECT_TYPES.has(shapeType)) { + w = Math.round(BASE_W * 1.6) + h = Math.round(BASE_H * 1.6) + } + const origW = w, + origH = h + let scale = 1 + while (scale < MAX_SCALE) { + const area = textAreaForShape(shapeType, w, h) + measurer.resize(Math.max(area.w, 1), measurer.height) + measurer.characters = label + if (measurer.height <= area.h) break + scale += 0.1 + w = Math.round(origW * scale) + h = Math.round(origH * scale) + } + return { w, h } +} +``` + +Then for each shape, call `fitShapeToText(label, shapeType)` to get the right dimensions before calling `shape.resize(w, h)`. **Always call `measurer.remove()` after all shapes are created.** + +## Rotation + +**Only rotate shapes when the user explicitly asks for it.** Do not add rotation for visual flair — FigJam shapes should default to 0° rotation. + +The `rotation` property sets degrees from -180 to 180, rotating around the top-left corner: + +```javascript +const shape = await figma.getNodeByIdAsync('123:456') +if (shape && shape.type === 'SHAPE_WITH_TEXT') { + shape.rotation = 45 +} +figma.closePlugin() +``` + +## Opacity and Blend Mode + +```javascript +const shape = figma.createShapeWithText() +await figma.loadFontAsync(shape.text.fontName) +shape.text.characters = 'Semi-transparent' + +shape.opacity = 0.5 +console.log('Opacity:', shape.opacity) + +figma.closePlugin() +``` + +## Batch Creation: Row of Different Shapes + +Uses the `fitShapeToText` utility from above to size each shape to its label: + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const DARK = h(0x1e, 0x1e, 0x1e) +const PRESETS = { + lightGreen: { fill: h(0xcd, 0xf4, 0xd3), stroke: h(0x66, 0xd5, 0x75), text: DARK }, + lightBlue: { fill: h(0xc2, 0xe5, 0xff), stroke: h(0x3d, 0xad, 0xff), text: DARK }, + lightYellow: { fill: h(0xff, 0xec, 0xbd), stroke: h(0xff, 0xc9, 0x43), text: DARK }, + lightRed: { fill: h(0xff, 0xcd, 0xc2), stroke: h(0xff, 0x75, 0x56), text: DARK }, +} + +const items = [ + { label: 'Start', type: 'ROUNDED_RECTANGLE', color: PRESETS.lightGreen }, + { label: 'Process', type: 'SQUARE', color: PRESETS.lightBlue }, + { label: 'Decision', type: 'DIAMOND', color: PRESETS.lightYellow }, + { label: 'End', type: 'ELLIPSE', color: PRESETS.lightRed }, +] +const spacing = 40 + +// ... include fitShapeToText utility code from above ... + +const sizes = items.map((item) => fitShapeToText(item.label, item.type)) +const totalWidth = sizes.reduce((sum, s) => sum + s.w, 0) + (items.length - 1) * spacing +let curX = 0 + +for (let i = 0; i < items.length; i++) { + const size = sizes[i] + const shape = figma.createShapeWithText() + shape.shapeType = items[i].type + await figma.loadFontAsync(shape.text.fontName) + shape.text.characters = items[i].label + shape.resize(size.w, size.h) + const preset = items[i].color + shape.fills = [{ type: 'SOLID', color: preset.fill }] + shape.strokes = [{ type: 'SOLID', color: preset.stroke }] + shape.text.fills = [{ type: 'SOLID', color: preset.text }] + shape.x = curX + curX += size.w + spacing +} +measurer.remove() + +figma.closePlugin() +``` + +## Cloning Shapes + +```javascript +const original = await figma.getNodeByIdAsync('123:456') +if (original && original.type === 'SHAPE_WITH_TEXT') { + const clone = original.clone() + clone.x = original.x + original.width + 40 + console.log('Cloned shape:', clone.id, clone.shapeType) +} +figma.closePlugin() +``` + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Load fonts** before setting `shape.text.characters`. ShapeWithText uses **"Inter Medium"** by default — always use `await figma.loadFontAsync(shape.text.fontName)`, never hardcode `{ family: 'Inter', style: 'Regular' }`. +- **Connector text needs explicit font setup.** Unlike shapes, a ConnectorNode's `text.fontName` is invalid by default. To label a connector, first set `connector.text.fontName = { family: 'Inter', style: 'Medium' }` (font must already be loaded), then set `connector.text.characters`. Never call `figma.loadFontAsync(connector.text.fontName)` — it will fail. +- **Put ALL text content in `shape.text.characters`** — do not split into a short label and a separate description/metadata field. The shape should display the full text the user expects to see, and `fitShapeToText` will size it accordingly. +- **Never hardcode shape sizes. Always use `fitShapeToText`** to dynamically size shapes based on their text content. Create a measurer TextNode with `textAutoResize: 'HEIGHT'`, use it to measure text, scale shapes until text fits, then call `measurer.remove()`. This prevents text clipping. +- **Always set fill, stroke, AND text color together** using the color presets — setting only fills will leave mismatched stroke/text colors. +- **Set `shapeType` after creation:** `shape.shapeType = 'DIAMOND'` — use different types when the user asks for varied shapes. +- **Do not rotate** shapes unless the user explicitly asks for rotation. +- **Use `resize()`** not `resizeWithoutConstraints()` — shapes support `resize()` and `rescale()`. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. +- **Verify changes** by logging before/after values and exporting images when supported. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-sticky.md b/plugins/figma/skills/figma-use-figjam/references/create-sticky.md new file mode 100644 index 00000000..4e30f8e5 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-sticky.md @@ -0,0 +1,287 @@ +# Create Sticky Notes + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating, modifying, and styling sticky notes. + +**Scope:** Sticky notes are FigJam-specific nodes created with `figma.createSticky()`. For advanced text formatting on stickies, see [edit-text](edit-text.md). + +## When to use a Sticky + +Use sticky notes for individual ideas, responses, or pieces of input — keep each sticky to one idea. + +Do not use stickies for prompts, instructions, guiding questions, labels, or pre-written analysis — even if the content is short. If the content is there to guide or inform, use a text node instead. + +For an interactive board, you can also think of a sticky as something an active participant or collaborator would have placed, whereas text is often a part of the board's structure. + +## Creating a Sticky + +```javascript +const sticky = figma.createSticky() + +// Load the font before setting text content +await figma.loadFontAsync(sticky.text.fontName) +sticky.text.characters = 'Hello from FigJam!' + +console.log('Created sticky:', sticky.id, sticky.text.characters) +figma.closePlugin() +``` + +## Setting Text + +Stickies expose a `text` sublayer (a `TextSublayerNode`). You must load fonts before changing text content: + +```javascript +const sticky = figma.createSticky() + +// Load the font used by the sticky's text sublayer +await figma.loadFontAsync(sticky.text.fontName) +sticky.text.characters = 'Updated text' + +figma.closePlugin() +``` + +To modify text on an existing sticky: + +```javascript +const sticky = await figma.getNodeByIdAsync('123:456') +if (sticky && sticky.type === 'STICKY') { + await figma.loadFontAsync(sticky.text.fontName) + sticky.text.characters = 'New content' +} +figma.closePlugin() +``` + +## Color Palette + +FigJam sticky notes use a fixed palette of 10 colors. Set via the `fills` property. + +**CRITICAL**: Use `hex/255` notation (e.g. `0xA8/255`) for exact palette matching — rounded decimals cause FigJam to treat the color as "custom" instead of a palette color. + +| Color | Hex | +| ------ | --------- | +| White | `#FFFFFF` | +| Gray | `#E6E6E6` | +| Green | `#B3EFBD` | +| Teal | `#B3F4EF` | +| Blue | `#A8DAFF` | +| Violet | `#D3BDFF` | +| Pink | `#FFA8DB` | +| Red | `#FFB8A8` | +| Orange | `#FFD3A8` | +| Yellow | `#FFE299` | + +### Setting a Sticky's Color + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const sticky = figma.createSticky() +await figma.loadFontAsync(sticky.text.fontName) +sticky.text.characters = 'Blue sticky' +sticky.fills = [{ type: 'SOLID', color: h(0xa8, 0xda, 0xff) }] // Blue #A8DAFF + +figma.closePlugin() +``` + +### Changing the Color of an Existing Sticky + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const sticky = await figma.getNodeByIdAsync('123:456') +if (sticky && sticky.type === 'STICKY') { + console.log('Before:', JSON.stringify(sticky.fills)) + sticky.fills = [{ type: 'SOLID', color: h(0xff, 0xe2, 0x99) }] // Yellow #FFE299 + console.log('After:', JSON.stringify(sticky.fills)) +} +figma.closePlugin() +``` + +## Sizing + +Stickies have two shapes controlled by `isWideWidth`: + +- **Square** (default): `isWideWidth = false` — **240 × 240 px** +- **Wide rectangular**: `isWideWidth = true` — **416 × 240 px** + +The `width` and `height` properties are **read-only**. Stickies do not support `resize()` — use `isWideWidth` to toggle between square and wide shapes. + +**Auto-grow:** Stickies automatically grow taller when text overflows the default height. The width stays fixed (240 or 416), but height can exceed 240. When positioning multiple stickies, always read the actual `sticky.height` after setting text — don't assume 240. + +Default to square stickies; only use wide stickies if the text is approximately 100 words or more. + +```javascript +const sticky = figma.createSticky() +await figma.loadFontAsync(sticky.text.fontName) +sticky.text.characters = 'Wide sticky' +sticky.isWideWidth = true + +console.log('Size:', sticky.width, 'x', sticky.height) +// Square: 240 x 240 (or taller if text overflows) +// Wide: 416 x 240 (or taller if text overflows) +figma.closePlugin() +``` + +### Toggling Size on an Existing Sticky + +```javascript +const sticky = await figma.getNodeByIdAsync('123:456') +if (sticky && sticky.type === 'STICKY') { + console.log('Before:', sticky.width, 'x', sticky.height, 'wide:', sticky.isWideWidth) + sticky.isWideWidth = !sticky.isWideWidth + console.log('After:', sticky.width, 'x', sticky.height, 'wide:', sticky.isWideWidth) +} +figma.closePlugin() +``` + +## Layout & Spacing (REQUIRED for batch creation) + +**Use a grid, not a vertical stack.** When placing multiple stickies inside a section, arrange them in a **grid** (cols × rows) with 64 px spacing — do not stack them in a single column. See "Grid of Stickies" below. + +**CRITICAL — Two-pass layout:** When creating multiple stickies, you MUST use a two-pass approach. Measuring one sticky and assuming all are the same size **will cause overlapping**. + +**Pass 1 — Create all stickies:** Create every sticky, set its text and color. Do NOT position yet. + +**Pass 2 — Position using actual dimensions:** Read each sticky's real `.width` and `.height`, compute per-row max heights, then assign x/y coordinates. + +**Row-based positioning for grids:** When laying out stickies in a grid, position each row independently. Within a row, place stickies left-to-right using each sticky's actual `.width` plus uniform spacing. Rows should align vertically (use per-row max height for the y offset), but columns do NOT need to align across rows. This keeps uniform gaps between stickies even when widths vary (e.g., mixing square and wide stickies). + +**Recommended spacing:** 20px minimum between stickies at all times. Use 30–40px for more breathing room. Default to 64px when laying out stickies in a grid pattern. + +## Author Properties + +Prefer to keep author visible unless explicitly prompted otherwise. + +```javascript +const sticky = figma.createSticky() +await figma.loadFontAsync(sticky.text.fontName) +sticky.text.characters = 'Team feedback' + +// The author is automatically set to the current user on creation +console.log('Author:', sticky.authorName) + +// Hide or show the author label +sticky.authorVisible = false + +figma.closePlugin() +``` + +## Batch Creation + +### Row of Stickies + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const labels = ['Idea 1', 'Idea 2', 'Idea 3', 'Idea 4'] +const colors = [ + h(0xb3, 0xef, 0xbd), // Green #B3EFBD + h(0xa8, 0xda, 0xff), // Blue #A8DAFF + h(0xff, 0xa8, 0xdb), // Pink #FFA8DB + h(0xff, 0xe2, 0x99), // Yellow #FFE299 +] +const spacing = 64 + +// Pass 1: Create all stickies and set content +const stickies = [] +for (let i = 0; i < labels.length; i++) { + const sticky = figma.createSticky() + await figma.loadFontAsync(sticky.text.fontName) + sticky.text.characters = labels[i] + sticky.fills = [{ type: 'SOLID', color: colors[i % colors.length] }] + stickies.push(sticky) +} + +// Pass 2: Position using each sticky's actual width and height +const totalWidth = stickies.reduce((sum, s) => sum + s.width, 0) + (stickies.length - 1) * spacing +const maxH = Math.max(...stickies.map((s) => s.height)) +let curX = 0 +for (const sticky of stickies) { + sticky.x = curX + curX += sticky.width + spacing +} + +figma.closePlugin() +``` + +### Grid of Stickies + +Rows align vertically, but columns don't need to line up — each row flows left-to-right with uniform spacing. + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const items = ['Task A', 'Task B', 'Task C', 'Task D', 'Task E', 'Task F'] +const cols = 3 +const spacing = 64 + +// Pass 1: Create all stickies and set content +const stickies = [] +for (let i = 0; i < items.length; i++) { + const sticky = figma.createSticky() + await figma.loadFontAsync(sticky.text.fontName) + sticky.text.characters = items[i] + sticky.fills = [{ type: 'SOLID', color: h(0xff, 0xe2, 0x99) }] // Yellow #FFE299 + stickies.push(sticky) +} + +// Pass 2: Group into rows, compute per-row dimensions +const numRows = Math.ceil(stickies.length / cols) +const rowGroups = [] +for (let r = 0; r < numRows; r++) { + rowGroups.push(stickies.slice(r * cols, r * cols + cols)) +} +const rowHeights = rowGroups.map((row) => Math.max(...row.map((s) => s.height))) +// Position each row independently +let curY = 0 +for (let r = 0; r < rowGroups.length; r++) { + let curX = 0 + for (const sticky of rowGroups[r]) { + sticky.x = curX + sticky.y = curY + curX += sticky.width + spacing + } + curY += rowHeights[r] + spacing +} + +figma.closePlugin() +``` + +## Cloning Stickies + +```javascript +const original = await figma.getNodeByIdAsync('123:456') +if (original && original.type === 'STICKY') { + const clone = original.clone() + clone.x = original.x + original.width + 64 + console.log('Cloned sticky:', clone.id) +} +figma.closePlugin() +``` + +### Replacing a node with a sticky + +Copy the source node's position, add the sticky to the same parent, then remove the original: + +```javascript +const source = await figma.getNodeByIdAsync(nodeId) +const sticky = figma.createSticky() +await figma.loadFontAsync(sticky.text.fontName) +sticky.text.characters = source.text.characters + +// Reparent into the same container so x/y are in the same coordinate space +source.parent.appendChild(sticky) +sticky.x = source.x +sticky.y = source.y + +source.remove() +``` + +### Creating stickies near an existing node + +Please see [position-figjam-nodes](position-figjam-nodes.md) - "Positioning Nodes Relative to Existing Nodes" + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Load fonts** before setting `sticky.text.characters`. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. +- **Verify changes** by logging before/after values and exporting images when supported. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-table.md b/plugins/figma/skills/figma-use-figjam/references/create-table.md new file mode 100644 index 00000000..f8e68ff0 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-table.md @@ -0,0 +1,345 @@ +# Create Tables + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating and styling tables with rows, columns, and cell content. + +**Scope:** Tables are FigJam-specific nodes created with `figma.createTable()`. They structure content in rows and columns. For stickies and sections, see [create-sticky](create-sticky.md) and [create-section](create-section.md). For shapes with text in them, see [create-shape-with-text](create-shape-with-text.md). + +**When to use this skill:** Prefer FigJam tables whenever the user asks for a table, spreadsheet, comparison grid, roster, or any row/column layout of text data. Examples: "create a table", "add a spreadsheet", "make a grid with names and roles", "comparison table", "team roster", "data table". Do **not** build a table-like layout out of shapes with text or other node types. + +**When NOT to use this skill:** Prefer creating other node types or relying on node positioning on the canvas in order to organize non-text content. + +**Note:** The Table API is only available in FigJam. + +## Creating a Table + +Default to applying a dark color to the header row(s) but leave the other cells to have the default fill (without making any edits), unless the user provides guiddance on styling. + +**CRITICAL**: If the user provides real data to include (e.g. in the form of CSV, image, etc.), include **all** of it in the resulting table. Never intermix real data with placeholder data. Otherwise if no data is provied, create tables without any placeholder content in headers, rows, columns, or cells. + +**CRITICAL**: Never delete any source data from the canvas when asked to convert to a table. + +```javascript +// Default: 2 rows, 2 columns, parented under figma.currentPage +const table = figma.createTable() + +// Or specify dimensions: createTable(numRows?, numColumns?) +const table3x4 = figma.createTable(3, 4) + +console.log('Created table:', table.id, table.numRows, 'x', table.numColumns) +figma.closePlugin() +``` + +## Setting Cell Text + +Each cell is a `TableCellNode` with a `text` sublayer (`TextSublayerNode`). You must load the font before setting `characters`. Use `table.cellAt(rowIndex, columnIndex)` to get a cell (indices are zero-based). + +```javascript +const table = figma.createTable(2, 3) + +// Load the font before setting characters +await figma.loadFontAsync(table.cellAt(0, 0).text.fontName) + +// Set characters for each cell (example: header row A B C, data row 1 2 3) +table.cellAt(0, 0).text.characters = 'A' +table.cellAt(0, 1).text.characters = 'B' +table.cellAt(0, 2).text.characters = 'C' +table.cellAt(1, 0).text.characters = '1' +table.cellAt(1, 1).text.characters = '2' +table.cellAt(1, 2).text.characters = '3' + +table.x = 0 +table.y = 0 + +figma.closePlugin() +``` + +### Modifying Text in an Existing Table + +```javascript +const table = await figma.getNodeByIdAsync('123:456') +if (table && table.type === 'TABLE') { + const cell = table.cellAt(0, 0) + await figma.loadFontAsync(cell.text.fontName) + cell.text.characters = 'Updated' +} +figma.closePlugin() +``` + +## TableNode Properties + +- **type**: `'TABLE'` (readonly) +- **numRows**, **numColumns**: number (readonly) — number of rows and columns +- **cellAt(rowIndex, columnIndex)**: returns the `TableCellNode` at that position +- **width**, **height**: number (readonly) — use `resizeRow` / `resizeColumn` to change size + +### Adding and Removing Rows/Columns + +```javascript +const table = figma.createTable(2, 2) + +// Insert a row before index 1 (so new row is at index 1) +table.insertRow(1) + +// Insert a column before index 0 +table.insertColumn(0) + +// Remove row at index 2, column at index 0 +table.removeRow(2) +table.removeColumn(0) + +figma.closePlugin() +``` + +### Moving Rows and Columns + +```javascript +const table = await figma.getNodeByIdAsync('123:456') +if (table && table.type === 'TABLE') { + // moveRow(fromIndex, toIndex) — move row from fromIndex to toIndex + table.moveRow(2, 0) + + // moveColumn(fromIndex, toIndex) + table.moveColumn(1, 0) +} +figma.closePlugin() +``` + +### Resizing Rows and Columns + +Rows and columns cannot be resized smaller than their minimum size. Use `resizeRow(rowIndex, height)` and `resizeColumn(columnIndex, width)`. + +```javascript +const table = figma.createTable(3, 3) + +// Resize first row to 60px height, first column to 120px width +table.resizeRow(0, 60) +table.resizeColumn(0, 120) + +console.log('Table size:', table.width, 'x', table.height) +figma.closePlugin() +``` + +## TableCellNode Properties + +- **type**: `'TABLE_CELL'` (readonly) +- **text**: TextSublayerNode (readonly) — the cell’s text; load font then set `text.characters` +- **rowIndex**, **columnIndex**: number (readonly) — cell position in the table +- **width**, **height**: number (readonly) — determined by table layout +- **fills**: set cell background (e.g. header row styling) + +### Setting Cell Fills + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const table = figma.createTable(2, 3) +await figma.loadFontAsync(table.cellAt(0, 0).text.fontName) + +// Header row: light blue background +for (let c = 0; c < 3; c++) { + const cell = table.cellAt(0, c) + cell.fills = [{ type: 'SOLID', color: h(0xc2, 0xe5, 0xff) }] // Light blue #C2E5FF + cell.text.characters = ['A', 'B', 'C'][c] +} +// Data row +table.cellAt(1, 0).text.characters = '1' +table.cellAt(1, 1).text.characters = '2' +table.cellAt(1, 2).text.characters = '3' + +figma.closePlugin() +``` + +## Table-Level Fills + +The table itself has a `fills` property for the overall table background. Use `setFillsAsync` for pattern fills; for solid fills you can set `table.fills` directly. + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const table = figma.createTable(2, 2) +table.fills = [{ type: 'SOLID', color: h(0xff, 0xec, 0xbd) }] // Light yellow #FFECBD + +figma.closePlugin() +``` + +## Color Options + +FigJam tables use the same color palette as sections and shapes. You can style: + +- **Table fill** — `table.fills` (overall table background) +- **Cell fill** — `table.cellAt(row, col).fills` (per-cell background, e.g. header row) +- **Cell text color** — `cell.text.fills` (set after loading fonts) + +Tables do **not** have strokes. When applying colors, set **fill and text together** so contrast is correct: dark fills use white text; light fills use dark text. Strongly prefer colors from this list so the table matches the FigJam editor palette. + +### Color Preset Map + +Use this map for table fills and cell fills. For **cell text**, use the `text` value (white on dark fills, dark on light fills). **CRITICAL**: Use `hex/255` notation (e.g. `0x66/255`) for exact palette matching — rounded decimals cause FigJam to treat the color as "custom" instead of a palette color. + +If the user asks for a color by a similar but different name, identify the closest option available from this map, keeping in mind the cell context (e.g. header row, body cell, etc). For example, choose `violet` if asked for a purple header or `lightGreen` if asked for green rows. + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const WHITE = h(0xff, 0xff, 0xff) +const DARK = h(0x1e, 0x1e, 0x1e) + +const TABLE_COLOR_PRESETS = { + // Dark fills (e.g. header row) — use white text + black: { fill: h(0x1e, 0x1e, 0x1e), text: WHITE }, + darkGray: { fill: h(0x75, 0x75, 0x75), text: WHITE }, + green: { fill: h(0x66, 0xd5, 0x75), text: WHITE }, + teal: { fill: h(0x5a, 0xd8, 0xcc), text: WHITE }, + blue: { fill: h(0x3d, 0xad, 0xff), text: WHITE }, + violet: { fill: h(0x87, 0x4f, 0xff), text: WHITE }, + pink: { fill: h(0xf8, 0x49, 0xc1), text: WHITE }, + red: { fill: h(0xf2, 0x48, 0x22), text: WHITE }, + orange: { fill: h(0xff, 0x9e, 0x42), text: WHITE }, + + // Light fills (e.g. table background, body cells) — use dark text + gray: { fill: h(0xb3, 0xb3, 0xb3), text: DARK }, + lightGray: { fill: h(0xd9, 0xd9, 0xd9), text: DARK }, + lightGreen: { fill: h(0xcd, 0xf4, 0xd3), text: DARK }, + lightTeal: { fill: h(0xc6, 0xfa, 0xf6), text: DARK }, + lightBlue: { fill: h(0xc2, 0xe5, 0xff), text: DARK }, + lightViolet: { fill: h(0xdc, 0xcc, 0xff), text: DARK }, + lightPink: { fill: h(0xff, 0xc2, 0xec), text: DARK }, + lightRed: { fill: h(0xff, 0xc7, 0xc2), text: DARK }, + lightOrange: { fill: h(0xff, 0xe0, 0xc2), text: DARK }, + yellow: { fill: h(0xff, 0xc9, 0x43), text: DARK }, + lightYellow: { fill: h(0xff, 0xec, 0xbd), text: DARK }, + white: { fill: h(0xff, 0xff, 0xff), text: DARK }, +} +``` + +### Hex Reference + +| Color | Fill Hex | Text | +| ------------ | --------- | ----- | +| Black | `#1E1E1E` | white | +| Dark gray | `#757575` | white | +| Gray | `#B3B3B3` | dark | +| Light gray | `#D9D9D9` | dark | +| Green | `#66D575` | white | +| Light green | `#CDF4D3` | dark | +| Teal | `#5AD8CC` | white | +| Light teal | `#C6FAF6` | dark | +| Blue | `#3DADFF` | white | +| Light blue | `#C2E5FF` | dark | +| Violet | `#874FFF` | white | +| Light violet | `#DCCCFF` | dark | +| Pink | `#F849C1` | white | +| Light pink | `#FFC2EC` | dark | +| Red | `#F24822` | white | +| Light red | `#FFC7C2` | dark | +| Orange | `#FF9E42` | white | +| Light orange | `#FFE0C2` | dark | +| Yellow | `#FFC943` | dark | +| Light yellow | `#FFECBD` | dark | +| White | `#FFFFFF` | dark | + +_white = `#FFFFFF`, dark = `#1E1E1E`_ + +### Applying Table and Cell Colors + +Set table background, then cell fills and text colors. Load the cell font before setting `text.fills` or `text.characters`: + +**CRITICAL**: Never clear or remove the fill from a table or cell node. Instead, interpret this as an ask to reset to the default fill color (i.e. white). + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) +const DARK = h(0x1e, 0x1e, 0x1e) +const preset = { + fill: h(0xc2, 0xe5, 0xff), // Light blue + text: DARK, +} + +const table = figma.createTable(2, 3) +await figma.loadFontAsync(table.cellAt(0, 0).text.fontName) + +// Table-level background +table.fills = [{ type: 'SOLID', color: preset.fill }] + +// Header row: dark fill, white text +const headerPreset = { fill: h(0x3d, 0xad, 0xff), text: h(0xff, 0xff, 0xff) } +for (let c = 0; c < 3; c++) { + const cell = table.cellAt(0, c) + cell.fills = [{ type: 'SOLID', color: headerPreset.fill }] + cell.text.fills = [{ type: 'SOLID', color: headerPreset.text }] + cell.text.characters = ['Name', 'Role', 'Team'][c] +} + +// Body row: light fill (or inherit table fill), dark text +for (let c = 0; c < 3; c++) { + const cell = table.cellAt(1, c) + cell.text.fills = [{ type: 'SOLID', color: preset.text }] + cell.text.characters = ['Alice', 'Designer', 'Product'][c] +} + +figma.closePlugin() +``` + +### Changing Color on an Existing Table + +```javascript +const table = await figma.getNodeByIdAsync('123:456') +if (table && table.type === 'TABLE') { + await figma.loadFontAsync(table.cellAt(0, 0).text.fontName) + const preset = TABLE_COLOR_PRESETS.lightGreen + table.fills = [{ type: 'SOLID', color: preset.fill }] + // Optionally update header or specific cells + table.cellAt(0, 0).fills = [{ type: 'SOLID', color: preset.fill }] + table.cellAt(0, 0).text.fills = [{ type: 'SOLID', color: preset.text }] +} +figma.closePlugin() +``` + +## Building a Table from Data + +```javascript +const rows = [ + ['Name', 'Role', 'Team'], + ['Alice', 'Designer', 'Product'], + ['Bob', 'Engineer', 'Platform'], +] +const numRows = rows.length +const numCols = rows[0].length + +const table = figma.createTable(numRows, numCols) +await figma.loadFontAsync(table.cellAt(0, 0).text.fontName) + +for (let r = 0; r < numRows; r++) { + for (let c = 0; c < numCols; c++) { + table.cellAt(r, c).text.characters = rows[r][c] + } +} + +table.name = 'Team roster' + +figma.closePlugin() +``` + +## Cloning Tables + +```javascript +const original = await figma.getNodeByIdAsync('123:456') +if (original && original.type === 'TABLE') { + const clone = original.clone() + clone.x = original.x + original.width + 20 + console.log('Cloned table:', clone.id, clone.numRows, 'x', clone.numColumns) +} +figma.closePlugin() +``` + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Initial table content:** Prefer empty tables unless the user provides data; then include all of it (no placeholders) and do not delete source data when converting. Use a dark header row and default fill elsewhere. +- **Load fonts** before setting `cell.text.characters` or `cell.text.fills` for any cell (use the first cell’s `text.fontName` or each cell’s as needed). +- **Set fill and text color together** when styling cells — use the color presets so light fills get dark text and dark fills get white text. +- **Use `hex/255` notation** for palette colors (e.g. `h(0xC2, 0xE5, 0xFF)`) so FigJam treats them as palette colors, not custom. +- **Table API is FigJam-only** — `figma.createTable()` is not available in Figma Design or other editor types. +- **Indices are zero-based**: `cellAt(0, 0)` is the top-left cell. +- **Table dimensions**: `width` and `height` are readonly; use `resizeRow` and `resizeColumn` to change size. Rows/columns cannot be resized below their minimum. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. +- **Verify changes** by logging before/after values when helpful. diff --git a/plugins/figma/skills/figma-use-figjam/references/create-text.md b/plugins/figma/skills/figma-use-figjam/references/create-text.md new file mode 100644 index 00000000..0a07b7b3 --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/create-text.md @@ -0,0 +1,466 @@ +# Create Text Nodes + +> Part of the [figma-use-figjam skill](../SKILL.md). Creating and styling standalone text nodes and mind map operations. + +Use this skill when creating, modifying, or styling standalone text in FigJam (text created with the **Text** tool, not text inside stickies, shapes, or connectors). Also use this skill for mind map operations — adding, inserting, or extending connected text nodes. + +**Scope:** Text nodes are created with `figma.createText()` and have type `'TEXT'`. For editing existing text content or mixed styles, see [edit-text](edit-text.md). + +## When to use a Text Node + +Use text nodes for titles, headers, labels, prompts, instructions, and any content that provides structure or context. They can also be used for longer descriptions or explanations. + +## Creating a Text Node + +```javascript +const text = figma.createText() + +// Load the font before setting content (required for characters, fontSize, etc.) +await figma.loadFontAsync(text.fontName) +text.characters = 'Brainstorming instructions' + +console.log('Created text:', text.id, text.characters) +figma.closePlugin() +``` + +## Text Wrapping and Width Constraints + +By default, text nodes auto-resize in both width and height (`textAutoResize = 'WIDTH_AND_HEIGHT'`), meaning they never wrap — text extends in one line until it ends. + +To make text wrap within a specific width (e.g., instructional text inside sections): + +1. Set `textAutoResize = 'HEIGHT'` — text will grow vertically but respect the width constraint +2. Use `resize(width, height)` to set the desired width + + ```javascript + const text = figma.createText() + await figma.loadFontAsync(text.fontName) + text.characters = 'Long instructional text that should wrap...' + + // Constrain to 336px wide, allow height to grow + text.textAutoResize = 'HEIGHT' + text.resize(336, text.height) + ``` + +**When creating text inside sections**: Calculate the max width as `section.width - (padding * 2)`. For example, with 32px padding on each side: + +```javascript +const maxWidth = section.width - 64 // 32px left + 32px right +text.textAutoResize = 'HEIGHT' +text.resize(maxWidth, text.height) +``` + +**Important**: Call `resize()` AFTER setting `characters` and `textAutoResize`, so the height adjusts correctly based on the wrapped content. + +**When to wrap vs not**: Use text wrapping for body text and instructions inside sections. Leave headers and short labels at the default `WIDTH_AND_HEIGHT` so they size naturally — wrapping a short H1 title into a narrow column looks worse than letting it extend. + +## Loading Fonts + +**Critical:** Changing text content or any property that affects layout (e.g. `characters`, `fontSize`, `fontName`, `textCase`, `lineHeight`) requires the font to be loaded first. Call `figma.loadFontAsync(fontName)` before such operations. + +- **Single font:** Use the node’s `fontName` (or the new font when changing font). +- **Mixed styles:** Text can have different fonts per range. Load every font used in the node: + +```javascript +// Load all fonts in a text node (handles mixed fonts) +const segments = textNode.getStyledTextSegments(['fontName']) +await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) +``` + +Alternatively, for a given range: + +```javascript +const fontNames = textNode.getRangeAllFontNames(0, textNode.characters.length) +await Promise.all(fontNames.map(figma.loadFontAsync)) +``` + +You do **not** need to load a font to change only **fills** (text color), **strokes**, or similar paint-related properties. + +## FigJam Preset Fonts + +In FigJam, the font family control exposes four presets plus any custom fonts already in the selection. Prefer these preset fonts so created text matches what users see in the UI: + +| Preset (UI label) | Font family | Default style | Use for | +| ----------------- | -------------- | ------------- | ---------------------- | +| Simple | `Inter` | `Medium` | Default, readable body | +| Bookish | `Merriweather` | `Regular` | Serif, formal | +| Technical | `Roboto Mono` | `Medium` | Monospace, code | +| Scribbled | `Figma Hand` | `Regular` | Script, handwritten | + +Set `fontName` to match the FigJam UI: `{ family: 'Inter', style: 'Medium' }`, `{ family: 'Merriweather', style: 'Regular' }`, `{ family: 'Roboto Mono', style: 'Medium' }`, or `{ family: 'Figma Hand', style: 'Regular' }` (or the appropriate style for the font). Load the font before setting `characters` or `fontSize`. + +## Missing Fonts + +Check `textNode.hasMissingFont` before loading. If `true`, the font is not available in the document (e.g. not installed for the user). Avoid setting content or layout properties that require that font, or handle the case explicitly. + +```javascript +const text = await figma.getNodeByIdAsync('123:456') +if (text && text.type === 'TEXT') { + if (text.hasMissingFont) { + console.warn('Text uses a missing font; cannot safely edit content.') + } else { + const segments = text.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + text.characters = 'Updated text' + } +} +figma.closePlugin() +``` + +## Color Palette + +**CRITICAL**: When creating text for board templates, ALWAYS use the default **Charcoal (#1E1E1E)** color. Do not use grey (#757575, #B3B3B3) or light grey (#D9D9D9) for body text, headers, or descriptions — these make content look unfinished and hard to read. + +In FigJam, text created with the **Text** tool uses a specific color palette. Prefer these colors so text matches FigJam’s default palette. + +**CRITICAL:** Use `hex/255` notation (e.g. `0x1E/255`) for exact palette matching — rounded decimals can make FigJam treat the color as custom. + +| Color | Hex | +| ------------ | --------------------- | +| White | `#FFFFFF` | +| Black | `#1E1E1E` | +| Dark gray | `#757575` | +| Gray | `#B3B3B3` | +| Light gray | `#D9D9D9` | +| Green | `#66D575` | +| Light green | `#CDF4D3` | +| Teal | `#5AD8CC` | +| Light teal | `#C6FAF6` | +| Blue | `#3DADFF` | +| Light blue | `#C2E5FF` | +| Violet | `#9747FF` | +| Light violet | `#E4CCFF` | +| Pink | `#F849C1` | +| Light pink | `#FFC2EC` | +| Red | `#FF7556` | +| Light red | `#FFCDC2` | +| Orange | `#FF9E42` | +| Light orange | `#FFE0C2` | +| Yellow | `#FFC943` | +| Light yellow | `#FFECBD` | +| Custom | Any hex or eyedropper | + +The default color for new text in FigJam is **Charcoal** (`#1E1E1E`). Use this for new text nodes unless the user specifies otherwise. + +**Do not use color to create text hierarchy** — rely on font size (H1→64, H2→40, H3→24, body→16). All text MUST use Charcoal (#1E1E1E) unless the user specifically requests otherwise. + +### Color Helper and Preset Map + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const FIGJAM_TEXT_COLORS = { + white: h(0xff, 0xff, 0xff), + black: h(0x1e, 0x1e, 0x1e), // Charcoal — default for new text + darkGray: h(0x75, 0x75, 0x75), + gray: h(0xb3, 0xb3, 0xb3), + lightGray: h(0xd9, 0xd9, 0xd9), + green: h(0x66, 0xd5, 0x75), + lightGreen: h(0xcd, 0xf4, 0xd3), + teal: h(0x5a, 0xd8, 0xcc), + lightTeal: h(0xc6, 0xfa, 0xf6), + blue: h(0x3d, 0xad, 0xff), + lightBlue: h(0xc2, 0xe5, 0xff), + violet: h(0x97, 0x47, 0xff), + lightViolet: h(0xe4, 0xcc, 0xff), + pink: h(0xf8, 0x49, 0xc1), + lightPink: h(0xff, 0xc2, 0xec), + red: h(0xff, 0x75, 0x56), + lightRed: h(0xff, 0xcd, 0xc2), + orange: h(0xff, 0x9e, 0x42), + lightOrange: h(0xff, 0xe0, 0xc2), + yellow: h(0xff, 0xc9, 0x43), + lightYellow: h(0xff, 0xec, 0xbd), +} +``` + +### Setting Text Color + +Set the text fill via the node’s `fills` property (after loading the font if you also change content): + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const text = figma.createText() +await figma.loadFontAsync(text.fontName) +text.characters = 'Blue label' +text.fills = [{ type: 'SOLID', color: h(0x3d, 0xad, 0xff) }] // Blue #3DADFF + +figma.closePlugin() +``` + +### Changing Color on an Existing Text Node + +```javascript +const h = (r, g, b) => ({ r: r / 255, g: g / 255, b: b / 255 }) + +const text = await figma.getNodeByIdAsync('123:456') +if (text && text.type === 'TEXT') { + // Fills can be set without loading the font + text.fills = [{ type: 'SOLID', color: h(0x97, 0x47, 0xff) }] // Violet #9747FF + console.log('Updated text color') +} +figma.closePlugin() +``` + +## Setting Text on an Existing Node + +```javascript +const text = await figma.getNodeByIdAsync('123:456') +if (text && text.type === 'TEXT') { + if (text.hasMissingFont) { + console.warn('Missing font; skipping content update.') + } else { + const segments = text.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + text.characters = 'New content' + } +} +figma.closePlugin() +``` + +## FigJam Preset Font Sizes + +The FigJam font size dropdown uses these preset values (in px). Prefer them so created text matches the UI options: + +| Preset (UI label) | Size (px) | +| ----------------- | --------- | +| Small | 16 | +| Medium | 24 | +| Large | 40 | +| Extra large | 64 | +| Huge | 96 | + +Helper for use in code: + +```javascript +const FIGJAM_FONT_SIZES = { + small: 16, + medium: 24, + large: 40, + extraLarge: 64, + huge: 96, +} +``` + +Users can also pick custom sizes (e.g. 1–2000); the presets are the standard choices. + +### Setting Size and Alignment + +Load the font before changing layout-related properties: + +```javascript +const text = figma.createText() +await figma.loadFontAsync(text.fontName) +text.characters = 'Heading' +text.fontSize = FIGJAM_FONT_SIZES.medium // 24 — matches FigJam "Medium" +text.textAlignHorizontal = 'CENTER' +text.textAlignVertical = 'CENTER' + +figma.closePlugin() +``` + +## Bulleted and Numbered Lists + +When creating content with numbered or bulleted lines, generate it line-by-line as a list by using `setRangeListOptions` and `setRangeIndentation` to properly render bullet points and numbers with indentation. + +When creating lists with bullets or numbers, **do not** put literal bullet or number characters in the text (e.g. `"• Item 1\n• Item 2"` or `"1. First\n2. Second"`). Also **do not** build indentation in manually to items by including spaces (e.g. ` indented sub point`). + +1. Set `characters` to the **content only** — one line per item, **no** leading `"• "`, `"1. "`, `A.`, `i.` or white space to manually create an indent. +2. Every line must have a list item type set, either 'ORDERED' for numbered/lettered lists, and 'UNORDERED' for bulleted lists. For each line that should be a list item, call **`setRangeListOptions(start, end, value)`** with the character range of that line (include the newline at the end of the line). +3. Every line must have an indentation level set. This is an integer **0–5**; use **1** for top-level list items. Use **`setRangeIndentation(start, end, level)`** to set this value for each line. + +`setRangeListSpacing(start, end, value)` can optionally be used to add spacing between list items. +`getRangeListOptions(start, end)` or `getRangeIndentation(start, end)` can be used to inspect list options and indentation. + +### Example: Numbered list + +```javascript +const text = figma.createText() +await figma.loadFontAsync(text.fontName) + +// Content only — no number characters. Each entry: [line content, indentation level 0–5] +const items = [ + ['First main point', 1], + ['Sub-point under first', 2], + ['Sub-sub-point', 3], + ['Second main point', 1], + ['Sub-point under second', 2], +] + +const lines = items.map(([content]) => content) +text.characters = lines.join('\n') + +let offset = 0 +for (let i = 0; i < items.length; i++) { + const [content, indentLevel] = items[i] + const start = offset + // Only add +1 for newline if NOT the last line + const end = offset + content.length + (i < lines.length - 1 ? 1 : 0) + text.setRangeListOptions(start, end, { type: 'ORDERED' }) + text.setRangeIndentation(start, end, indentLevel) + offset = end +} + +figma.closePlugin() +``` + +### Example: Bulleted list + +```javascript +const text = figma.createText() +await figma.loadFontAsync(text.fontName) + +// Each entry: [line content, indentation level 0–5] +const items = [ + ['Top-level item', 1], + ['Nested under first', 2], + ['Deeper nested', 3], + ['Sibling at level 2', 2], + ['Second top-level item', 1], + ['Its nested child', 2], +] + +const lines = items.map(([content]) => content) +text.characters = lines.join('\n') + +let offset = 0 +for (let i = 0; i < items.length; i++) { + const [content, indentLevel] = items[i] + const start = offset + // Only add +1 for newline if NOT the last line + const end = offset + content.length + (i < lines.length - 1 ? 1 : 0) + text.setRangeListOptions(start, end, { type: 'UNORDERED' }) + text.setRangeIndentation(start, end, indentLevel) + offset = end +} + +figma.closePlugin() +``` + +## Cloning Text Nodes + +```javascript +const original = await figma.getNodeByIdAsync('123:456') +if (original && original.type === 'TEXT') { + const clone = original.clone() + clone.x = original.x + original.width + 20 + console.log('Cloned text:', clone.id) +} +figma.closePlugin() +``` + +### Modifying existing structures (mind maps, connected text) + +Mind maps and similar structures use text nodes connected by connectors. When adding or inserting nodes, you must **shift existing nodes to make room** — otherwise nodes will overlap. + +**Shift direction depends on the layout:** + +- **Left-to-right flows:** shift downstream nodes along the **x-axis** +- **Tree / mind map branches:** shift sibling nodes along the **y-axis** — branches spread vertically, so new children need vertical space + +#### Adding child nodes to a mind map branch + +When adding multiple child nodes to a branch point, space each child vertically and shift any existing siblings below them downward: + +```javascript +const branchNode = await figma.getNodeByIdAsync(branchNodeId) +const parent = branchNode.parent + +const newTopics = ['Topic A', 'Topic B', 'Topic C'] +const Y_SPACING = 40 + +// Measure total height the new nodes will need +const newTexts = [] +for (const topic of newTopics) { + const t = figma.createText() + await figma.loadFontAsync(t.fontName) + t.characters = topic + newTexts.push(t) +} +const totalNewHeight = + newTexts.reduce((sum, t) => sum + t.height, 0) + (newTexts.length - 1) * Y_SPACING + +// Shift existing sibling nodes below the insertion point downward +for (const sibling of parent.children) { + if (sibling.type === 'TEXT' && sibling.y > branchNode.y) { + sibling.y += totalNewHeight + Y_SPACING + } +} + +// Place new nodes vertically, connected to the branch point +let curY = branchNode.y + branchNode.height + Y_SPACING +for (const t of newTexts) { + parent.appendChild(t) + t.x = branchNode.x - t.width - 80 + t.y = curY + + const conn = figma.createConnector() + conn.connectorStart = { endpointNodeId: t.id, magnet: 'AUTO' } + conn.connectorEnd = { endpointNodeId: branchNode.id, magnet: 'AUTO' } + conn.connectorStartStrokeCap = 'NONE' + conn.connectorEndStrokeCap = 'ARROW_LINES' + parent.appendChild(conn) + + curY += t.height + Y_SPACING +} +``` + +#### Inserting a text node into a linear chain + +For left-to-right connected text (not tree-shaped), shift downstream nodes horizontally: + +```javascript +const leftNode = await figma.getNodeByIdAsync(leftNodeId) +const rightNode = await figma.getNodeByIdAsync(rightNodeId) +const oldConnector = await figma.getNodeByIdAsync(connectorId) +const parent = leftNode.parent + +const newText = figma.createText() +await figma.loadFontAsync(newText.fontName) +newText.characters = 'New Topic' + +// Shift nodes to the right to make room +const SPACING = 80 +const shiftAmount = newText.width + SPACING +for (const sibling of parent.children) { + if (sibling.type === 'TEXT' && sibling.x >= rightNode.x) { + sibling.x += shiftAmount + } +} + +// Place the new node in the created gap +parent.appendChild(newText) +newText.x = leftNode.x + leftNode.width + SPACING / 2 +newText.y = leftNode.y + +// Rewire connectors +oldConnector.remove() +const conn1 = figma.createConnector() +conn1.connectorStart = { endpointNodeId: leftNode.id, magnet: 'AUTO' } +conn1.connectorEnd = { endpointNodeId: newText.id, magnet: 'AUTO' } +conn1.connectorStartStrokeCap = 'NONE' +conn1.connectorEndStrokeCap = 'ARROW_LINES' +parent.appendChild(conn1) + +const conn2 = figma.createConnector() +conn2.connectorStart = { endpointNodeId: newText.id, magnet: 'AUTO' } +conn2.connectorEnd = { endpointNodeId: rightNode.id, magnet: 'AUTO' } +conn2.connectorStartStrokeCap = 'NONE' +conn2.connectorEndStrokeCap = 'ARROW_LINES' +parent.appendChild(conn2) +``` + +If the parent is a section, resize it afterward to encompass the new content (see [create-section](create-section.md) — "Resizing an Existing Section"). + +## Key Points + +- **Always wrap code in an async IIFE:** `(async () => { ... })();` +- **Always call `figma.closePlugin()`** at the end of every code path. +- **Load fonts** before setting `characters`, `fontSize`, `fontName`, or any other property that affects text layout; not required for `fills` (color) only. +- **Check `hasMissingFont`** when editing existing text; do not assume fonts are available. +- **Use node IDs** from the user message, not `figma.currentPage.selection`. +- **Use the FigJam palette** with `hex/255` for text color. +- **Prefer FigJam font presets** (Inter, Merriweather, Roboto Mono, Figma Hand — UI labels: Simple, Bookish, Technical, Scribbled) and **preset font sizes** (16, 24, 40, 64, 96) so created text aligns with the font and size dropdowns in the UI. +- **For bulleted/numbered lists:** use `setRangeListOptions` and `setRangeIndentation` on line ranges; do not embed bullet or number characters in the text if it will be formatted as an ordered or unordered list. +- **Verify changes** by logging before/after values and exporting images when supported. diff --git a/plugins/figma/skills/figma-use-figjam/references/edit-text.md b/plugins/figma/skills/figma-use-figjam/references/edit-text.md new file mode 100644 index 00000000..5a664e2d --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/edit-text.md @@ -0,0 +1,362 @@ +# Text Operations + +> Part of the [figma-use-figjam skill](../SKILL.md). Editing existing text content, styles, and font segments. + +## Critical: Load Fonts First + +**Always load fonts before modifying text.** This is required or operations will fail. + +```javascript +// Load a single font +await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }) + +// Load a font used by a single-font text node +await figma.loadFontAsync(textNode.fontName) + +// Load all fonts in a text node (handles mixed fonts via styled segments) +const segments = textNode.getStyledTextSegments(['fontName']) +await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) +``` + +## Complete Working Example + +```javascript +const nodeId = '123:456' +const node = await figma.getNodeByIdAsync(nodeId) + +if (node && node.type === 'TEXT') { + // Load all fonts used in this text node (handles mixed fonts via styled segments) + const segments = node.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + + console.log('Before:', node.characters) + node.characters = 'New text content' + console.log('After:', node.characters) + + // Verify with image + const img = await node.exportAsync({ + format: 'PNG', + constraint: { type: node.width > node.height ? 'WIDTH' : 'HEIGHT', value: 128 }, + }) + figma.io.write(`${node.name.replace(/[^a-z0-9]/gi, '_')}_result.png`, img) +} +figma.closePlugin() +``` + +## Basic Properties + +```javascript +// Text content +textNode.characters = 'Hello, World!' + +// Font +textNode.fontName = { family: 'Inter', style: 'Bold' } + +// Size +textNode.fontSize = 16 + +// Color (via fills) — use Charcoal (#1E1E1E) as default +textNode.fills = [{ type: 'SOLID', color: { r: 0x1e / 255, g: 0x1e / 255, b: 0x1e / 255 } }] +``` + +### Text Color in FigJam + +**CRITICAL**: When editing text in FigJam board content (templates, brainstorms, retros, or any generated content), always use **Charcoal (#1E1E1E)** as the text color unless the user has specifically requested different colors. Use `hex/255` notation for exact palette matching: + +```javascript +// Charcoal — default for all FigJam text +textNode.fills = [{ type: 'SOLID', color: { r: 0x1e / 255, g: 0x1e / 255, b: 0x1e / 255 } }] +``` + +Do not use grey (#757575, #B3B3B3) or light grey (#D9D9D9) for body text, headers, or descriptions — these make content look unfinished and hard to read. + +## Text Alignment + +```javascript +// Horizontal alignment +textNode.textAlignHorizontal = 'LEFT' // LEFT, CENTER, RIGHT, JUSTIFIED + +// Vertical alignment +textNode.textAlignVertical = 'TOP' // TOP, CENTER, BOTTOM +``` + +## Line Height and Spacing + +```javascript +// Line height +textNode.lineHeight = { value: 150, unit: 'PERCENT' } +textNode.lineHeight = { value: 24, unit: 'PIXELS' } +textNode.lineHeight = { unit: 'AUTO' } + +// Letter spacing +textNode.letterSpacing = { value: 0, unit: 'PERCENT' } +textNode.letterSpacing = { value: 1, unit: 'PIXELS' } + +// Paragraph spacing +textNode.paragraphSpacing = 16 + +// Paragraph indentation +textNode.paragraphIndent = 24 +``` + +## Text Decoration + +Underlines and strikethroughs support styling (wavy, dotted), offset, and color. + +```javascript +textNode.textDecoration = 'UNDERLINE' // NONE, UNDERLINE, STRIKETHROUGH +textNode.textDecorationStyle = 'WAVY' // SOLID, DOTTED, WAVY +textNode.textDecorationOffset = { unit: 'PIXELS', value: 2 } +textNode.textDecorationColor = { value: { type: 'SOLID', color: { r: 1, g: 0, b: 0 } } } // Custom color +textNode.textDecorationColor = { value: 'AUTO' } // Inherit from text color +``` + +## Text Case + +```javascript +// Case transformation +textNode.textCase = 'ORIGINAL' // ORIGINAL, UPPER, LOWER, TITLE, SMALL_CAPS, SMALL_CAPS_FORCED +``` + +## Text Sizing Behavior + +```javascript +// Auto-resize mode +textNode.textAutoResize = 'WIDTH_AND_HEIGHT' // Auto-size both +textNode.textAutoResize = 'HEIGHT' // Fixed width, auto height +textNode.textAutoResize = 'NONE' // Fixed size +// Truncation pattern (preferred over deprecated textAutoResize = 'TRUNCATE') +textNode.textAutoResize = 'NONE' // Keep fixed bounds +textNode.textTruncation = 'ENDING' // Truncate overflow with ellipsis +textNode.maxLines = 2 // Optional: cap visible lines +``` + +## Styled Ranges (Mixed Styles) + +For text with different styles in different parts: + +```javascript +await figma.loadFontAsync({ family: 'Inter', style: 'Bold' }) +await figma.loadFontAsync({ family: 'Inter', style: 'Regular' }) + +textNode.characters = 'Hello World' + +// Make "Hello" bold (characters 0-5) +textNode.setRangeFontName(0, 5, { family: 'Inter', style: 'Bold' }) + +// Make "World" red (characters 6-11) +textNode.setRangeFills(6, 11, [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }]) + +// Change size of "World" +textNode.setRangeFontSize(6, 11, 24) + +// Get properties for a range +const fontAtStart = textNode.getRangeFontName(0, 1) +const sizeAtEnd = textNode.getRangeFontSize(10, 11) +``` + +## Available Range Methods + +- `setRangeFontName(start, end, fontName)` +- `setRangeFontSize(start, end, size)` +- `setRangeFills(start, end, fills)` +- `setRangeTextDecoration(start, end, decoration)` +- `setRangeTextCase(start, end, textCase)` +- `setRangeLetterSpacing(start, end, spacing)` +- `setRangeLineHeight(start, end, lineHeight)` +- `setRangeHyperlink(start, end, hyperlink)` +- `setRangeListOptions(start, end, listOptions)` +- `setRangeIndentation(start, end, indentation)` + +## Hyperlinks + +```javascript +// Set hyperlink +textNode.setRangeHyperlink(0, 5, { type: 'URL', value: 'https://figma.com' }) + +// Node link +textNode.setRangeHyperlink(0, 5, { type: 'NODE', value: '123:456' }) + +// Remove hyperlink +textNode.setRangeHyperlink(0, 5, null) +``` + +## Lists + +```javascript +// Bulleted list +textNode.setRangeListOptions(0, textNode.characters.length, { type: 'UNORDERED' }) + +// Numbered list +textNode.setRangeListOptions(0, textNode.characters.length, { type: 'ORDERED' }) + +// Remove list +textNode.setRangeListOptions(0, textNode.characters.length, { type: 'NONE' }) +``` + +## Getting Text Segments + +```javascript +// Get all styled segments +const segments = textNode.getStyledTextSegments(['fontName', 'fontSize', 'fills', 'textDecoration']) + +for (const segment of segments) { + console.log(`"${segment.characters}" - ${segment.fontName.family} ${segment.fontSize}px`) +} +``` + +## Inserting and Deleting Characters + +```javascript +// Load all fonts (handles mixed fonts via styled segments) +const segments = textNode.getStyledTextSegments(['fontName']) +await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + +// Insert text at a position +textNode.insertCharacters(0, 'Hello ') // Insert at start +textNode.insertCharacters(textNode.characters.length, '!') // Insert at end +textNode.insertCharacters(6, 'beautiful ') // Insert in middle + +// Delete characters (start, end) +textNode.deleteCharacters(0, 6) // Delete first 6 characters +textNode.deleteCharacters(5, 10) // Delete characters 5-9 + +figma.closePlugin() +``` + +## Splitting Text into Multiple Nodes + +To split a text node into separate nodes (one per line/paragraph): + +```javascript +const nodeId = '123:456' +const node = await figma.getNodeByIdAsync(nodeId) + +if (node && node.type === 'TEXT') { + // Load all fonts (handles mixed fonts via styled segments) + const segments = node.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + + const lines = node.characters.split(/\r?\n/) + const parent = node.parent + const index = parent.children.indexOf(node) + + // Calculate line height for positioning + const lineHeight = + typeof node.lineHeight === 'object' && node.lineHeight.unit === 'PIXELS' + ? node.lineHeight.value + : node.fontSize * 1.2 + + const createdNodes = [] + for (const line of lines) { + if (!line.trim()) continue + + // Clone preserves ALL properties (lineHeight, letterSpacing, etc.) + const newNode = node.clone() + newNode.characters = line.trim() + + // Position vertically using lineHeight (for non-auto-layout parents) + if (parent.layoutMode === 'NONE' || !parent.layoutMode) { + newNode.y = node.y + createdNodes.length * lineHeight + } + + parent.insertChild(index + createdNodes.length, newNode) + createdNodes.push(newNode) + } + + node.remove() + console.log(`Split into ${createdNodes.length} nodes`) +} + +figma.closePlugin() +``` + +**Key points:** + +1. Use `clone()` to preserve all text properties (lineHeight, letterSpacing, textCase, etc.) +2. Position using `lineHeight` directly - use PIXELS value if set, otherwise `fontSize * 1.2` +3. For verification exports, export the parent frame - don't use temporary grouping + +## Find and Replace Across Page + +To search and replace text content across many nodes: + +```javascript +;(async () => { + const searchText = 'Sign Up' + const replaceText = 'Register' + const caseSensitive = false + + const textNodes = figma.currentPage.findAllWithCriteria({ types: ['TEXT'] }) + console.log(`Searching ${textNodes.length} text nodes for "${searchText}"...`) + + const regex = new RegExp( + searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), + caseSensitive ? 'g' : 'gi', + ) + + let totalReplacements = 0 + let nodesModified = 0 + + for (const node of textNodes) { + if (!regex.test(node.characters)) continue + regex.lastIndex = 0 + + // Load all fonts used in this node (handles mixed fonts via styled segments) + const segments = node.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + + const matches = node.characters.match(regex)?.length || 0 + console.log(` ${node.name}: "${node.characters}" → ${matches} match(es)`) + + node.characters = node.characters.replace(regex, replaceText) + totalReplacements += matches + nodesModified++ + } + + console.log(`Replaced ${totalReplacements} occurrence(s) across ${nodesModified} node(s)`) + figma.closePlugin() +})() +``` + +### Scoped Find and Replace (Within a Frame) + +```javascript +const frame = await figma.getNodeByIdAsync('123:456') +if (frame && 'findAllWithCriteria' in frame) { + const textNodes = frame.findAllWithCriteria({ types: ['TEXT'] }) + // ... same replacement logic as above +} +``` + +### Preserving Styled Ranges + +When text has mixed styling (bold + regular), replacing `characters` wholesale resets all styling to the first character's style. To preserve ranges, use `deleteCharacters` + `insertCharacters`: + +```javascript +async function replacePreservingStyles(node, search, replace) { + // Load all fonts (handles mixed fonts via styled segments) + const segments = node.getStyledTextSegments(['fontName']) + await Promise.all(segments.map((s) => figma.loadFontAsync(s.fontName))) + + let idx = node.characters.indexOf(search) + while (idx !== -1) { + node.deleteCharacters(idx, idx + search.length) + node.insertCharacters(idx, replace) + idx = node.characters.indexOf(search, idx + replace.length) + } +} +``` + +## OpenType Features + +```javascript +// Get current features for a range +const features = textNode.getRangeOpenTypeFeatures(0, 1) + +// Inspect node-level OpenType features +const nodeFeatures = textNode.openTypeFeatures +if (nodeFeatures !== figma.mixed) { + console.log('LIGA:', nodeFeatures.LIGA, 'CALT:', nodeFeatures.CALT) +} +``` diff --git a/plugins/figma/skills/figma-use-figjam/references/plan-board-content.md b/plugins/figma/skills/figma-use-figjam/references/plan-board-content.md new file mode 100644 index 00000000..d6e9d36f --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/plan-board-content.md @@ -0,0 +1,330 @@ +# Plan Content for FigJam Boards + +**When NOT to use this skill:** Do NOT read this skill for analysis, summarization, or investigation of existing board content (e.g. "summarize this board", "what themes are here?", "analyze the feedback"). This skill is exclusively for planning NEW content to be created on the board. + +Do NOT use this skill for flowcharts, architecture diagrams, sequence diagrams, state diagrams, or entity relationship diagrams (ERDs). For those, use the `figma-plugin:generate-diagram` skill and the `generate_diagram` tool. + +Use this skill when determining **what content to include** for generated FigJam board content. Given a user’s request (e.g. "make a brainstorm template", "retro board", "ice breaker", "scaffold"), produce a **sequential outline** that downstream skills can use to create sections, text, stickies, and layout. + +**Must be loaded alongside the `figma-use-figjam` skill**, which provides the FigJam Plugin API references (create-section, create-sticky, create-text, position-figjam-nodes) needed to render the planned content. + +## Part 1: Design Principles + +### Reading direction and grouping + +Left-to-right, top-to-bottom. Context on the left, evidence in the middle, proposal/asks on the right. Supporting detail and appendix below. + +**Tight clustering** (60-92px) = same thought. **Loose spacing** (200-400px) = different topics. **Zone breaks** (1000px+) = different part of the board. + +### Type scale + +Use **Inter** exclusively. Subtitles should be 40-50% the size of their parent heading. + +| Role | Size | Weight | +| ---------------- | ------- | -------------- | +| Board title | 60-96px | Bold | +| Board subtitle | 36-40px | Regular | +| Section heading | 48px | Bold | +| Section subtitle | 24-28px | Regular | +| Card title | 28-32px | Semi Bold | +| Body text | 20-24px | Regular | +| Metadata | 16px | Regular/Medium | + +### Color semantics + +**White cards inside colorful containers.** Section backgrounds alternate warm/cool for rhythm: peach, lavender, soft blue, light gray. + +| Signal | Color | Use | +| --------------------------- | --------------------------------- | -------------------------------------- | +| Attention (“look here”) | Gold `{r:0.85, g:0.65, b:0.1}` | Neutral urgency. Not negative. | +| Problem / regression | Orange `{r:0.72, g:0.38, b:0.08}` | Something trending wrong | +| Critical / blocked | Red `{r:0.75, g:0.18, b:0.18}` | Something actually broken | +| Healthy / shipped | Green `{r:0.12, g:0.5, b:0.3}` | Small indicators only, not backgrounds | +| Informational / in-progress | Blue `{r:0.22, g:0.4, b:0.75}` | Neutral | +| Decision needed | Pink `{r:0.7, g:0.2, b:0.45}` | Action required | +| Exploration | Purple `{r:0.45, g:0.3, b:0.65}` | Ideation | + +**Key rule:** Gold = “look here.” Red = “this is bad.” Don’t use red for attention. + +### Proportion and alignment + +- Size sections to fit content, not the other way around +- Center elements in rows on the same y-axis +- Center content in portrait/vertical cards +- Position badges relative to text, not section edges +- At least 12-16px breathing room between title and body + +### Every board needs an entry point + +Board title (60-96px) at top-left, clearly visible at overview zoom. For templates, add a meta/instructions section. For meetings, a colored agenda sticky. + +--- + +## Part 2: Construction Rules + +### Board structure + +**Always wrap the entire board in one top-level white section.** This makes the board a single movable unit. + +```js +const board = figma.createSection(); +board.name = ‘’; +board.resizeWithoutConstraints(estimatedW, estimatedH); +board.fills = [{ type: ‘SOLID’, color: {r:1, g:1, b:1} }]; +// All content goes inside: board.appendChild(...) +``` + +**Size from content outward.** Choose card width based on the text inside it (body text reads well at 400-1000px depending on density). Derive section width from card count and card width. Derive board width from sections. Never divide a container’s width to get card width. Never stretch a card to fill its parent. Sections and cards don’t need to be the same width as each other unless they share a content pattern. + +**For participatory zones, size to the expected activity, not the current content.** Workshop sections, feedback areas, brainstorm columns, and retro lanes exist for other people to fill. Size them to fit the expected number of contributors. Pre-seed with a few example stickies to signal the interaction pattern. + +**Clear all section names** unless the section has no title text inside it. + +### Spacing grid + +All spacing in multiples of 4px. + +```js +const spacing = { + sectionPadding: { top: 68, right: 60, bottom: 100, left: 80 }, + elementGapH: 60, // between cards/columns + elementGapV: 64, // between stacked elements + siblingGapH: 92, // between sibling sections + siblingGapV: 120, // between section rows + contentPadding: 24, // inside cards +} +``` + +**Lay out inside the inset, not from one edge.** Compute usable area first (container size minus padding on all sides), then fit items within it. + +### Color palette + +```js +const black = { r: 0.07, g: 0.07, b: 0.07 } +const gray = { r: 0.35, g: 0.35, b: 0.35 } +const red = { r: 0.75, g: 0.18, b: 0.18 } +const orange = { r: 0.72, g: 0.38, b: 0.08 } +const green = { r: 0.12, g: 0.5, b: 0.3 } +const blue = { r: 0.22, g: 0.4, b: 0.75 } +const purple = { r: 0.45, g: 0.3, b: 0.65 } +const attention = { r: 0.85, g: 0.65, b: 0.1 } // gold +const attentionLight = { r: 1, g: 0.85, b: 0.2 } // bright gold (starburst fills) +const attentionBg = { r: 1, g: 0.96, b: 0.85 } // soft gold (card tint) + +// Section backgrounds +const white = { r: 1, g: 1, b: 1 } // #FFFFFF +const lightGray = { r: 0.976, g: 0.976, b: 0.976 } // #F9F9F9 +const lightGreen = { r: 0.922, g: 1, b: 0.933 } // #EBFFEE +const lightTeal = { r: 0.945, g: 0.996, b: 0.992 } // #F1FEFD +const lightBlue = { r: 0.961, g: 0.984, b: 1 } // #F5FBFF +const lightViolet = { r: 0.973, g: 0.961, b: 1 } // #F8F5FF +const lightPink = { r: 1, g: 0.941, b: 0.98 } // #FFF0FA +const lightRed = { r: 1, g: 0.961, b: 0.961 } // #FFF5F5 +const lightOrange = { r: 1, g: 0.969, b: 0.941 } // #FFF7F0 +const lightYellow = { r: 1, g: 0.984, b: 0.941 } // #FFFBF0 +``` + +### API surface + +**Native primitives:** `createText`, `createFrame`, `createRectangle`, `createEllipse`, `createLine`, `createStar`, `createPolygon`, `createVector`, `figma.union()` / `figma.subtract()` + +**FigJam-specific:** `createSticky`, `createShapeWithText`, `createConnector`, `createSection`, `createTable`, `createCodeBlock`, `createNodeFromSvg` + +**Not available:** `createComponent`, `createComponentSet` + +### Choosing the right node type + +| Need | Use | Why | +| ------------------------- | -------------------------------------------- | ------------------------------------------------------------------------- | +| Flowchart node with label | `createShapeWithText` | Built-in text centering, connector endpoints | +| Card | `createSection` | Native FigJam grouping with background fill, nests inside parent sections | +| Badge / pill | `createFrame` + auto-layout + text | Precise padding, radius, auto-centered text | +| Emphasis marker with text | `createFrame` container + shape + text | Frame guarantees centering | +| Emphasis marker (no text) | `createPolygon`/`createStar`/`createEllipse` | Triangle, starburst, dot | +| Top-level zone | `createSection` | FigJam native grouping, zoom-to behavior | +| Divider | `createRectangle` at 1-2px height | Simple | + +### Sections as cards + +Cards are nested sections. They give you native FigJam grouping (zoom-to behavior, movable as a unit) with a background fill, and they nest cleanly inside parent sections. + +```js +const card = figma.createSection(); +card.name = ‘’; +card.resizeWithoutConstraints(width, height); +card.fills = [{ type: ‘SOLID’, color: white }]; +// Sections don’t support auto-layout — position children with absolute x/y +// inside the card, accounting for your own padding. +``` + +Sections nest. A board is a section that contains zone sections, which contain card sections. Use frames only for badges, pills, or other small containers that need auto-layout to center text. + +### Text + +```js +await figma.loadFontAsync({ family: ‘Inter’, style: ‘Bold’ }); +await figma.loadFontAsync({ family: ‘Inter’, style: ‘Regular’ }); +await figma.loadFontAsync({ family: ‘Inter’, style: ‘Semi Bold’ }); +await figma.loadFontAsync({ family: ‘Inter’, style: ‘Medium’ }); + +const t = figma.createText(); +t.fontName = { family: ‘Inter’, style: ‘Bold’ }; +t.fontSize = 32; +t.fills = [{ type: ‘SOLID’, color: black }]; +t.characters = ‘Title’; +// For body text, constrain width: +t.resize(440, 10); +t.textAutoResize = ‘HEIGHT’; +``` + +Body text max width: 440-520px. Rich text via `setRangeFontName` and `setRangeHyperlink`. + +### Emphasis markers + +Use sparingly. One or two per section max. They work by breaking the visual pattern at overview zoom. + +**Card-level:** + +- Gold border + warm tint = “pay attention” (neutral) +- Red border + red tint = “off-track” (negative status only) +- Warning triangle (`createPolygon({ pointCount: 3 })`) pinned to top-right corner +- Notification dot (`createEllipse`) with count inside + +**Section-level:** + +- Starburst (`createStar({ pointCount: 8, innerRadius: 0.65 })`) with gold fill and text (“NEW”, “UPDATED”) +- Bullseye (concentric rings with decreasing opacity) + +**Centering text over shapes:** Always use a frame container. Never position text with manual x/y math. + +```js +const container = figma.createFrame(); +container.resize(56, 56); container.fills = []; container.clipsContent = false; +const shape = figma.createStar(); shape.resize(56, 56); +container.appendChild(shape); shape.x = 0; shape.y = 0; +const text = figma.createText(); text.characters = ‘NEW’; +container.appendChild(text); +text.x = (56 - text.width) / 2; text.y = (56 - text.height) / 2; +``` + +**Flowchart emphasis:** Green Yes / Red No pills. Octagon for hard blockers. Diamond (rotated rect) for decisions. + +### Tables + +Style header rows with Bold weight and tinted fill. Size table width to match section width minus padding. Don’t leave tables floating in whitespace. + +### Stickies + +For discussion, not editorial content. Color semantics: blue=discussion, yellow=question, green=positive, pink=concern, red=blocker, teal=decision, violet=ideation. + +**Always lay out stickies in a grid.** Rows and columns, consistent 64px gap, aligned to the top-left of the usable inset. Stickies are 240x240 (square) or 416x240 (wide, for longer text). These sizes are fixed; stickies cannot be resized. Never stagger, overlap, or let stickies touch section edges. + +--- + +## Part 3: Workflow + +### 0. Understand the ask + +What’s the purpose? Who’s the audience? Once or recurring? Match to an archetype if possible. + +### 1. Plan the narrative + +Outline beats/sections in plain text before writing code. + +### 2. Build incrementally + +**First call:** Create the white wrapper section at a rough estimated size (you’ll resize it in the reflow pass). + +Then for each sub-section: + +1. Create cards and content first — size cards to fit their text +2. Create the container section sized to wrap those cards (card count × card width + gaps + padding) +3. Validate with `get_screenshot`. Fix before moving on. + +### 3. Reflow pass + +- `textAutoResize = ‘HEIGHT’` on all text +- Resize cards to fit content +- Equalize card heights within rows where cards share a content pattern +- Resize each section to hug its children (measure rightmost/bottommost content edge + padding) +- Resize the board wrapper to hug all sections + +### 4. Audit pass + +- No overflow (child exceeds parent bounds) +- No overlap (consecutive text nodes collide) +- Section names cleared +- Spacing grid compliance +- Type scale compliance +- Color consistency + +--- + +## Part 4: Archetypes + +Use these when the prompt matches a known board type. + +### Vision Board + +3-5 narrative sections left-to-right. Optional working canvas below. Feedback capture at bottom. Mix text with stickies and screenshots. + +### Exec Review / Decision Board + +Linear left-to-right story. 5-8 sections alternating warm/cool tints. Pink/magenta for decisions. Appendix below. + +### Area Review (Template) + +Grid of identical team panels. Each team gets the same sub-section structure (discussion, KRs, project updates, references). + +### Pillar Check-in / Monthly Cadence + +Time x teams grid. Columns = months, rows = teams. Board grows rightward. + +### Competitive Research + +Freeform spatial map. Screenshot-dominant. Column sub-sections. Data tables for evidence. + +### Workshop / Brainstorm + +Two zones: pre-filled analysis + live brainstorm stickies. Meta section with instructions at top-left. Participatory zones should be sized for the expected activity (how many people, how many stickies per person), not the pre-filled content. Pre-seed each zone with a few example stickies to set the tone and show participants what’s expected. + +### Status Card Grid + +Uniform cards with consistent layout. Progress bars and RAG status badges. Category labels on left edge. + +### Context | Options | Decision + +Three zones side-by-side: Context (lavender, constraints), Options (2-3 white cards with upside/downside, rectangle dividers), Decision (pink, recommendation). + +### Vertical Metric Cards + +Portrait cards (400-500px wide) with centered content. Tinted backgrounds per card. + +### Top-to-Bottom Flow + +Vertical flowchart. Green Yes / Red No pills on connector paths. Distinct fills per node type. `createShapeWithText` for all nodes. + +--- + +## Anti-patterns + +- Don’t use stickies for editorial content. Text for narrative, stickies for discussion. +- Don’t use shapes as decoration. Every shape communicates: flowchart node, data viz, or emphasis. +- Don’t over-emphasize. One or two markers per section max. +- Don’t use red for attention. Gold draws the eye without implying failure. +- Don’t use em dashes. Periods, commas, or restructure. +- Don’t build the entire board in one `use_figma` call. Work incrementally. +- Don’t guess text height. Always `textAutoResize = ‘HEIGHT’` and reflow. +- Don’t use body text below 20px or metadata below 16px. +- Don’t skip the wrapper section. Every board is one movable unit. +- Don’t skip the entry point. Every board needs a visible title. +- Don’t leave section names on. Clear them unless there’s no title text inside. +- Don’t make text-only boards. Mix text with visual evidence (screenshots, diagrams, stickies). +- Don’t left-align vertical cards. Center hero numbers and text. +- Don’t use green for large backgrounds. Reserve for small status indicators. +- Don’t let text overlap. Reflow after setting content. This is a critical bug. +- Don’t manually position text over shapes. Use a frame container for centering. +- Don’t stretch cards to fill their parent. Width should serve readability. +- Don’t size participatory zones to their pre-filled content. Size for expected activity. +- Don’t scatter or stagger stickies. Always a grid with consistent gap. +- Don’t split a bulleted list across multiple text nodes — one node per bullet. Put the whole list in a single multi-line text node (`\n`-separated) so line spacing, alignment, and reflow are handled by the text engine. Separate nodes drift, mis-space, and force you to position each one manually. diff --git a/plugins/figma/skills/figma-use-figjam/references/position-figjam-nodes.md b/plugins/figma/skills/figma-use-figjam/references/position-figjam-nodes.md new file mode 100644 index 00000000..28ebc4ac --- /dev/null +++ b/plugins/figma/skills/figma-use-figjam/references/position-figjam-nodes.md @@ -0,0 +1,90 @@ +# FigJam Node Positioning Tutorial + +> Part of the [figma-use-figjam skill](../SKILL.md). Positioning, sizing, and reparenting nodes on the canvas. + +Use this skill when working with positioning, sizing, and reparenting nodes. + +## Basics of how nodes are positioned + +Nodes may be positioned by setting their `x` and `y` properties. Nodes are positioned with respect to their parent. + +```javascript +// Position (relative to parent) +node.x = 100 +node.y = 200 +``` + +## Positioning Nodes Relative to Existing Nodes + +It's important to remember that `node.x` and `node.y` are relative to the node's parent, not the page. For example: a node inside a section has coordinates relative to that section's origin. + +When creating or placing a node relative to an existing node: + +1. **Find the parent**: Locate the parent of the existing node. +2. **Add new node to parent**: Call `parent.appendChild()` to add the new node to the parent. +3. **Position within parent**: Position the new node within the parent. The x / y coordinates of the new node will be with respect to the top-left corner of the parent. +4. **Ensure parent sections encompass their children**: If the parent is a section: + a. Resize the section to encompass the new node + b. (see [create-section](create-section.md) — "Resizing an Existing Section" for more info) + +Example helper function: + +```javascript +// existingNodeId: string :: the node id you are positioning relative to +// nodeToPosition: the node you are trying to position relative to the existing node +async function placeNodeRelativeToOtherNode(existingNodeId: string, nodeToPosition) { + + // 1: find the parent of existing node + const existingNode = await figma.getNodeByIdAsync(existingNodeId); + const parent = existingNode.parent + + // 2: add the node to the parent + parent.appendChild(nodeToPosition) + + // 3: position the node w.r.t the top-left corner of the parent + // here, we chose to place it to the right of the existing node + nodeToPosition.x = existingNode.x + existingNode.width + 40; + nodeToPosition.y = existingNode.y; + + // 4: if the parent is a section: resize the section if needed + if (parent.type === 'SECTION') { + // ... resize if needed, (see [create-section](create-section.md) — "Resizing an Existing Section" for more info) + } +} +``` + +## Adding Nodes to a Section + +Use `appendChild` to move existing nodes into a section. + +**CRITICAL**: when you call `appendChild`, the node's x/y coordinates become relative to the **section's local coordinate space**, where (0,0) is the top-left corner of the section — NOT absolute board coordinates. Always call `appendChild` **first**, then set the node's position using section-local coordinates. + +**IMPORTANT:** After adding nodes to a section, you MUST check that the section encompasses its children. Refer to the `Resizing an Existing Section` code snippet for reference. This is _crucial_ for a high-quality output. + +Steps: + +1. **Add new node to parent**: Call `parent.appendChild()` to add the new node to the parent. +2. **Position within parent**: Position the new node within the parent. The x / y coordinates of the new node will be with respect to the TL of the parent. +3. **Clean up any layout consequences**: If the parent is a section: + a. Resize the section to encompass the new node + b. (see [create-section](create-section.md) — "Resizing an Existing Section" for more info) + +Example helper function: + +```javascript +function addNodeToSection(node, section) { + // Ensure this is a section + if (section.type !== 'SECTION') { + console.log('The node provided is not a section') + return + } + + // Append FIRST, then position using section-local coordinates + section.appendChild(node) + node.x = 32 + node.y = 32 + console.log(`Moved ${node.name} into ${section.name}`) + + // ... resize section if needed, (see [create-section](create-section.md) — "Resizing an Existing Section" for more info) +} +``` diff --git a/plugins/figma/skills/figma-use/SKILL.md b/plugins/figma/skills/figma-use/SKILL.md index 19992333..9705a158 100644 --- a/plugins/figma/skills/figma-use/SKILL.md +++ b/plugins/figma/skills/figma-use/SKILL.md @@ -26,7 +26,7 @@ IMPORTANT: Whenever you work with design systems, start with [working-with-desig 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})`. Use `await figma.listAvailableFontsAsync()` to discover all available fonts and their exact style strings — if a `loadFontAsync` call fails, call `listAvailableFontsAsync()` to find the correct style name or pick a fallback. +8. **Font loading is required before ANY operation on nodes that contain unloaded fonts** — not just text-setting operations. This includes `appendChild`, `insertChild`, `setBoundVariable`, `setExplicitVariableModeForCollection`, `setValueForMode`, and even `findAll` callbacks. If the document has existing text nodes, preload all their fonts at the start of the script. Use `await figma.listAvailableFontsAsync()` to discover available fonts and styles, then `await figma.loadFontAsync({family, style})` to load each one. See [Gotchas](references/gotchas.md) for the full preload pattern. 9. **Pages load incrementally** — use `await figma.setCurrentPageAsync(page)` to switch pages and load their content. The sync setter `figma.currentPage = page` does **NOT** work and will throw (see Page Rules below) 10. `setBoundVariableForPaint` returns a **NEW** paint — must capture and reassign 11. `createVariable` accepts collection **object or ID string** (object preferred) @@ -84,17 +84,177 @@ Available in design mode: Rectangle, Frame, Component, Text, Ellipse, Star, Line **Blocked** in design mode: Sticky, Connector, ShapeWithText, CodeBlock, Slide, SlideRow, Webpage. -## 5. Incremental Workflow (How to Avoid Bugs) +## 5. Efficient APIs — Prefer These Over Verbose Alternatives + +These APIs reduce boilerplate, eliminate ordering errors, and compress token output. **Always prefer them over the verbose alternatives.** + +### `node.query(selector)` — CSS-like node search + +Find nodes within a subtree using CSS-like selectors. Replaces verbose `findAll` + filter loops. + +```js +// BEFORE — verbose traversal +const texts = frame.findAll(n => n.type === 'TEXT' && n.name === 'Title') + +// AFTER — one-liner with query +const texts = frame.query('TEXT[name=Title]') +``` + +**Selector syntax:** +- Type: `FRAME`, `TEXT`, `RECTANGLE`, `ELLIPSE`, `COMPONENT`, `INSTANCE`, `SECTION` (case-insensitive) +- Attribute exact: `[name=Card]`, `[visible=true]`, `[opacity=0.5]` +- Attribute substring: `[name*=art]` (contains), `[name^=Header]` (starts-with), `[name$=Nav]` (ends-with) +- Dot-path traversal: `[fills.0.type=SOLID]`, `[fills.*.type=SOLID]` (wildcard index) +- Instance matching: `[mainComponent=nodeId]`, `[mainComponent.name=Button]` +- Combinators: `FRAME > TEXT` (direct child), `FRAME TEXT` (any descendant), `A + B` (adjacent sibling), `A ~ B` (general sibling) +- Pseudo-classes: `:first-child`, `:last-child`, `:nth-child(2)`, `:not(TYPE)`, `:is(FRAME, RECTANGLE)`, `:where(TEXT, ELLIPSE)` +- Node ID: `#nodeId` or bare GUID +- Comma: `TEXT, RECTANGLE` (union) +- Wildcard: `*` (any type) + +**QueryResult methods:** +| Method | Description | +|---|---| +| `.length` | Number of matched nodes | +| `.first()` | First matched node (or `null`) | +| `.last()` | Last matched node (or `null`) | +| `.toArray()` | Convert to regular array | +| `.each(fn)` | Iterate with callback, returns `this` for chaining | +| `.map(fn)` | Map to new array | +| `.filter(fn)` | Filter to new QueryResult | +| `.values(keys)` | Extract property values: `.values(['name', 'x', 'y'])` → `[{name, x, y}, ...]` | +| `.set(props)` | Set properties on all matched nodes (see `node.set()` below) | +| `.query(selector)` | Sub-query within matched nodes | +| `for...of` | Iterable — works in `for` loops | + +**Scope:** `node.query()` searches within that node's subtree. To search the whole page: `figma.currentPage.query('...')`. There is no global `figma.query()`. + +**Examples:** +```js +// Recolor all text inside cards +figma.currentPage.query('FRAME[name^=Card] TEXT').set({ + fills: [{type: 'SOLID', color: {r: 0.2, g: 0.2, b: 0.8}}] +}) + +// Get names and positions of all frames +return figma.currentPage.query('FRAME').values(['name', 'x', 'y']) + +// Find the first component named "Button" +const btn = figma.currentPage.query('COMPONENT[name=Button]').first() + +// Find all instances of a specific component +figma.currentPage.query(`INSTANCE[mainComponent=${compId}]`) + +// Find nodes with solid fills using dot-path traversal +figma.currentPage.query('[fills.0.type=SOLID]') +``` + +### `node.set(props)` — batch property updates + +Set multiple properties in one call. Returns `this` for chaining. + +```js +// BEFORE — one line per property +frame.opacity = 0.5 +frame.cornerRadius = 8 +frame.name = "Card" + +// AFTER — single call +frame.set({ opacity: 0.5, cornerRadius: 8, name: "Card" }) +``` + +**Priority key ordering:** `layoutMode` is always applied before other properties (like `width`/`height`) regardless of object key order. This prevents the common bug where `resize()` behaves differently depending on whether `layoutMode` is set. + +**Width/height handling:** `width` and `height` are routed through `node.resize()` automatically — setting `{ width: 200 }` calls `resize(200, currentHeight)`. + +**Chaining with query:** +```js +// Find all rectangles named "Divider" and update them +figma.currentPage.query('RECTANGLE[name=Divider]').set({ + fills: [{type: 'SOLID', color: {r: 0.9, g: 0.9, b: 0.9}}], + cornerRadius: 2 +}) +``` + +### `figma.createAutoLayout(direction?, props?)` — auto-layout frames + +Creates a frame with auto-layout already enabled and both axes hugging content. **Prefer this over `figma.createFrame()` for any container that needs auto-layout.** + +```js +// BEFORE — manual setup, easy to get ordering wrong +const frame = figma.createFrame() +frame.layoutMode = 'VERTICAL' +frame.primaryAxisSizingMode = 'AUTO' +frame.counterAxisSizingMode = 'AUTO' +frame.layoutSizingHorizontal = 'HUG' +frame.layoutSizingVertical = 'HUG' + +// AFTER — one call, layout ready +const frame = figma.createAutoLayout('VERTICAL') +``` + +Children can immediately use `layoutSizingHorizontal/Vertical = 'FILL'` after being appended — no need to set sizing modes manually. + +Accepts an optional props object as the first or second argument: +```js +figma.createAutoLayout({ name: 'Card', itemSpacing: 12 }) // HORIZONTAL + props +figma.createAutoLayout('VERTICAL', { name: 'Column', itemSpacing: 8 }) // VERTICAL + props +``` + +### `node.placeholder` — shimmer overlay for AI-in-progress feedback + +Sets a visual shimmer overlay on a node indicating work is in progress. **Always remove the shimmer when done** — leftover shimmers confuse users and indicate incomplete work. + +```js +// Mark as in-progress +frame.placeholder = true + +// ... build out the content ... + +// MUST remove when done — never leave shimmers on finished nodes +frame.placeholder = false +``` + +When building complex layouts, set `placeholder = true` on sections before populating them, then set `placeholder = false` on each section as it's completed. + +### `await node.screenshot(opts?)` — inline screenshots + +Capture a node as a PNG and return it inline in the response. Eliminates the need for a separate `get_screenshot` call. + +```js +// Take a screenshot of a frame (returned inline in the tool response) +await frame.screenshot() + +// Custom scale (default auto-scales: 0.5x or capped so max dimension ≤ 1024px) +await frame.screenshot({ scale: 2 }) + +// Include overlapping content from sibling nodes +await frame.screenshot({ contentsOnly: false }) +``` + +**When to use:** After creating or modifying nodes, call `screenshot()` to visually verify the result within the same script. No need for a separate `get_screenshot` call. + +**Auto-naming:** The image caption includes node metadata — `"Card (300x150 at 0,60).png"` — giving spatial context without parsing the image. + +**Default scaling:** Uses 0.5x scale, but automatically caps so the largest output dimension never exceeds 1024px. Explicit `{ scale: N }` bypasses the cap. + +## 6. Incremental Workflow (How to Avoid Bugs) The most common cause of bugs is trying to do too much in a single `use_figma` call. **Work in small steps and validate after each one.** +### Key rules + +- **At most 10 logical operations per `use_figma` call.** A "logical operation" is creating a node, setting its properties, and parenting it. If you need to create 20 nodes, split across 2-3 calls. +- **Build top-down, starting with placeholders.** Create the outer structure first with `placeholder = true` on each section, then incrementally replace placeholders with real content in subsequent calls. + ### The pattern 1. **Inspect first.** Before creating anything, run a read-only `use_figma` to discover what already exists in the file — pages, components, variables, naming conventions. Match what's there. -2. **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. +2. **Build the skeleton.** Create the top-level structure with placeholder sections. Set `placeholder = true` on each section so the user sees progress. +3. **Fill in sections incrementally.** In each subsequent call, populate one section and set its `placeholder = false` when done. Take a `screenshot()` to verify. +4. **Return IDs from every call.** Always `return` created node IDs, variable IDs, collection IDs as objects (e.g. `return { createdNodeIds: [...] }`). You'll need these as inputs to subsequent calls. +5. **Validate after each step.** Use `get_metadata` to verify structure (counts, names, hierarchy, positions). Use `await node.screenshot()` inline or `get_screenshot` after major milestones to catch visual issues. +6. **Fix before moving on.** If validation reveals a problem, fix it before proceeding to the next step. Don't build on a broken foundation. ### Suggested step order for complex tasks @@ -118,7 +278,7 @@ Step 5: Final verification | 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 +## 7. Error Recovery & Self-Correction **`use_figma` is atomic — failed scripts do not execute.** If a script errors, no changes are made to the file. The file remains in the same state as before the call. This means there are no partial nodes, no orphaned elements from the failed script, and retrying after a fix is safe. @@ -151,7 +311,7 @@ Step 5: Final verification > For the full validation workflow, see [Validation & Error Recovery](references/validation-and-recovery.md). -## 7. Pre-Flight Checklist +## 8. Pre-Flight Checklist Before submitting ANY `use_figma` call, verify: @@ -161,10 +321,13 @@ Before submitting ANY `use_figma` call, verify: - [ ] NO usage of `figma.notify()` anywhere - [ ] NO usage of `console.log()` as output (use `return` instead) - [ ] All colors use 0–1 range (not 0–255) +- [ ] Paint `color` objects use `{r, g, b}` only — no `a` field (opacity goes at the paint level: `{ type: 'SOLID', color: {...}, opacity: 0.5 }`) - [ ] Fills/strokes are reassigned as new arrays (not mutated in place) - [ ] Page switches use `await figma.setCurrentPageAsync(page)` (sync setter `figma.currentPage = page` does NOT work) - [ ] `layoutSizingVertical/Horizontal = 'FILL'` is set AFTER `parent.appendChild(child)` -- [ ] `loadFontAsync()` called BEFORE any text property changes (use `listAvailableFontsAsync()` to verify font availability if unsure) +- [ ] `loadFontAsync()` called before any text property changes (use `listAvailableFontsAsync()` to verify font availability if unsure) +- [ ] Style names have already been verified via `listAvailableFontsAsync()` — NOT guessed from memory (`"SemiBold"` vs `"Semi Bold"` is a common footgun) +- [ ] For `FONT_FAMILY`-scoped variables: every value across every relevant mode is loaded before `setBoundVariable("fontFamily", …)`, `setValueForMode`, or `setExplicitVariableModeForCollection` - [ ] `lineHeight`/`letterSpacing` use `{unit, value}` format (not bare numbers) - [ ] `resize()` is called BEFORE setting sizing modes (resize resets them to FIXED) - [ ] For multi-step workflows: IDs from previous calls are passed as string literals (not variables) @@ -172,7 +335,7 @@ Before submitting ANY `use_figma` call, verify: - [ ] 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 +## 9. Discover Conventions Before Creating **Always inspect the Figma file before creating anything.** Different files use different naming conventions, variable structures, and component patterns. Your code should match what's already there, not impose new conventions. @@ -211,7 +374,7 @@ const results = collections.map(c => ({ return results; ``` -## 9. Reference Docs +## 10. Reference Docs Load these as needed based on what your task involves: @@ -229,6 +392,6 @@ Load these as needed based on what your task involves: | [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 +## 11. Snippet examples You will see snippets throughout documentation here. These snippets contain useful plugin API code that can be repurposed. Use them as is, or as starter code as you go. If there are key concepts that are best documented as generic snippets, call them out and write to disk so you can reuse in the future. diff --git a/plugins/figma/skills/figma-use/references/api-reference.md b/plugins/figma/skills/figma-use/references/api-reference.md index 07941081..b2f28c67 100644 --- a/plugins/figma/skills/figma-use/references/api-reference.md +++ b/plugins/figma/skills/figma-use/references/api-reference.md @@ -22,6 +22,8 @@ ```js figma.createRectangle() figma.createFrame() +figma.createAutoLayout() // Frame with auto layout enabled, both axes hug — prefer over createFrame() for layout containers +figma.createAutoLayout("VERTICAL") // Same but vertical direction figma.createComponent() // Creates a ComponentNode figma.createText() figma.createEllipse() @@ -315,3 +317,4 @@ node.parent // Parent node | `figma.loadAllPagesAsync()` | Not implemented | | `figma.variables.extendLibraryCollectionByKeyAsync()` | Not implemented | | `figma.teamLibrary.*` | Not implemented (requires LiveGraph) | +| `figma.getLocalComponents*()` | **Does not exist** — unlike styles, there is no `getLocalComponents()` or `getLocalComponentSetsAsync()` (or any `getLocalComponent*` variant). Use `findAll(n => n.type === 'COMPONENT')` / `findAll(n => n.type === 'COMPONENT_SET')` to locate components in the current file. | diff --git a/plugins/figma/skills/figma-use/references/common-patterns.md b/plugins/figma/skills/figma-use/references/common-patterns.md index 32abacf4..4cdcdc27 100644 --- a/plugins/figma/skills/figma-use/references/common-patterns.md +++ b/plugins/figma/skills/figma-use/references/common-patterns.md @@ -87,9 +87,8 @@ for (const child of page.children) { maxX = Math.max(maxX, child.x + child.width) } -const frame = figma.createFrame() +const frame = figma.createAutoLayout('VERTICAL') frame.name = "Card" -frame.layoutMode = 'VERTICAL' frame.primaryAxisAlignItems = 'MIN' frame.counterAxisAlignItems = 'MIN' frame.paddingLeft = 16 @@ -97,8 +96,6 @@ 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 @@ -420,7 +417,7 @@ cs.resizeWithoutConstraints(maxX + 40, maxY + 40); const section = figma.createSection(); section.name = "MyComponent Section"; section.appendChild(cs); -section.resizeWithoutConstraints(cs.width + 200, cs.height + 200); +section.resize(cs.width + 200, cs.height + 200); return { csId: cs.id, count: components.length }; ``` diff --git a/plugins/figma/skills/figma-use/references/gotchas.md b/plugins/figma/skills/figma-use/references/gotchas.md index cff458b6..f9a9da21 100644 --- a/plugins/figma/skills/figma-use/references/gotchas.md +++ b/plugins/figma/skills/figma-use/references/gotchas.md @@ -9,8 +9,12 @@ - Page context and plugin lifecycle pitfalls - Auto Layout and sizing order pitfalls (including HUG/FILL interactions) - Variant layout and geometry pitfalls +- Font loading and text/typography pitfalls - Variable scopes and mode pitfalls - Node cleanup and empty-fill pitfalls +- Type-specific method calls without node type guards +- Non-existent property writes and "object is not extensible" +- width/height are read-only — use resize() - detachInstance() and node ID invalidation @@ -42,8 +46,7 @@ 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 card = figma.createAutoLayout('VERTICAL') const label = figma.createText() card.appendChild(label) // positioned by auto-layout, no x/y needed ``` @@ -257,24 +260,6 @@ const colorCollection = figma.variables.createVariableCollection("Colors") component.setExplicitVariableModeForCollection(colorCollection, targetModeId) ``` -## `TextStyle.setBoundVariable` is not available in `use_figma` - -`setBoundVariable` exists on `TextStyle` in the typed API but is **not available** in `use_figma`. Calling it will throw `"not a function"`. - -```js -// WRONG — throws "not a function" in use_figma -const ts = figma.createTextStyle() -ts.setBoundVariable("fontSize", fontSizeVar) - -// CORRECT — 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 `use_figma` 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 @@ -297,30 +282,7 @@ This applies to both `TextStyle` and `TextNode` properties. The same rule applie ## Font style names are file-dependent — use `listAvailableFontsAsync` to discover them -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** — never guess style names. - -Use `figma.listAvailableFontsAsync()` to discover all available fonts and their exact style strings before loading: - -```js -// WRONG — guessing style names -await figma.loadFontAsync({ family: "Inter", style: "SemiBold" }) // may throw - -// CORRECT — discover available styles, then load -const allFonts = await figma.listAvailableFontsAsync() -const interFonts = allFonts.filter(f => f.fontName.family === "Inter") -// interFonts[i].fontName → { family: "Inter", style: "Semi Bold" } (exact string) - -const desired = interFonts.find(f => f.fontName.style === "Semi Bold") -if (desired) { - await figma.loadFontAsync(desired.fontName) -} else { - // Fall back to a known-safe style - const fallback = interFonts.find(f => f.fontName.style === "Regular") - if (fallback) await figma.loadFontAsync(fallback.fontName) -} -``` - -When building a type ramp script, always call `listAvailableFontsAsync()` first to verify font styles against the target file before hardcoding them. If a `loadFontAsync` call fails, use `listAvailableFontsAsync()` to inspect what fonts are actually available and pick the closest match. +Font style names vary per provider and per Figma file. Always call `figma.listAvailableFontsAsync()` to discover exact style strings before loading — never guess or probe with try/catch. See [text-style-patterns.md](text-style-patterns.md#discovering-available-font-styles) for the discovery + load pattern. ## combineAsVariants does NOT auto-layout in `use_figma` @@ -349,18 +311,30 @@ for (const child of cs.children) { cs.resizeWithoutConstraints(maxX + 40, maxY + 40) ``` -## COLOR variable values use {r, g, b, a} (with alpha) +## Paint `color` must not include `a` — use `opacity` at the paint level instead + +Paint `color` only accepts `{r, g, b}`. Adding `a` to it throws `"Unrecognized key(s) in object: 'a' at [0].color"`. This is a common mistake coming from CSS `rgba()` muscle memory. + +Alpha/opacity belongs at the **paint level** as `opacity`, not inside `color`. ```js -// Paint colors use {r, g, b} (no alpha — opacity is a separate paint property) +// WRONG — 'a' is not valid inside color; throws validation error +node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 0.1 } }] + +// CORRECT — opacity goes at the paint level +node.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1 }, opacity: 0.1 }] + +// CORRECT — fully opaque (no opacity needed) node.fills = [{ type: 'SOLID', color: { r: 1, g: 0, b: 0 } }] +``` -// But COLOR variable values use {r, g, b, a} — alpha maps to paint opacity +**COLOR variable values are the exception** — they do use `{r, g, b, a}`: + +```js +// Variable values use {r, g, b, a} — this is correct for variables only const colorVar = figma.variables.createVariable("bg", collection, "COLOR") colorVar.setValueForMode(modeId, { r: 1, g: 0, b: 0, a: 1 }) // opaque red colorVar.setValueForMode(modeId, { r: 0, g: 0, b: 0, a: 0 }) // fully transparent - -// ⚠️ 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 @@ -377,22 +351,29 @@ parent.appendChild(child) // parent must have layoutMode set child.layoutSizingVertical = 'FILL' // Works! ``` +**Tip:** use `figma.createAutoLayout()` (or `figma.createAutoLayout('VERTICAL')`) instead of `figma.createFrame()` when you want a parent that supports `FILL` children. It returns a frame with `layoutMode` already set and both axes hugging content, so you don't have to remember the property dance. + +```js +const parent = figma.createAutoLayout() // layoutMode = 'HORIZONTAL', sizing = AUTO +const child = figma.createFrame() +parent.appendChild(child) +child.layoutSizingHorizontal = 'FILL' // Works immediately +``` + ## HUG parents collapse FILL children A `HUG` parent cannot give `FILL` children meaningful size. If children have `layoutSizingHorizontal = "FILL"` but the parent is `"HUG"`, the children collapse to minimum size. The parent must be `"FILL"` or `"FIXED"` for FILL children to expand. This is a common cause of truncated text in select fields, inputs, and action rows. ```js // WRONG — parent hugs, so FILL children get zero extra space -const parent = figma.createFrame() -parent.layoutMode = 'HORIZONTAL' +const parent = figma.createAutoLayout() parent.layoutSizingHorizontal = 'HUG' const child = figma.createFrame() parent.appendChild(child) child.layoutSizingHorizontal = 'FILL' // collapses to min size! // CORRECT — parent must be FIXED or FILL for FILL children to expand -const parent = figma.createFrame() -parent.layoutMode = 'HORIZONTAL' +const parent = figma.createAutoLayout() parent.resize(400, 50) parent.layoutSizingHorizontal = 'FIXED' // or 'FILL' if inside another auto-layout const child = figma.createFrame() @@ -408,9 +389,7 @@ child.layoutSizingHorizontal = 'FILL' // expands to fill remaining 400px const parent = figma.createComponent() parent.layoutMode = 'VERTICAL' parent.primaryAxisSizingMode = 'AUTO' // hug contents -const content = figma.createFrame() -content.layoutMode = 'VERTICAL' -content.primaryAxisSizingMode = 'AUTO' +const content = figma.createAutoLayout('VERTICAL') parent.appendChild(content) content.layoutGrow = 1 // BUG: content compresses, children hidden! @@ -422,6 +401,29 @@ parent.resizeWithoutConstraints(300, 500) content.layoutGrow = 1 // NOW it correctly fills remaining space ``` +## `width` and `height` are read-only — use `resize()` + +`node.width` and `node.height` are read-only. Assigning to them throws `"TypeError: no setter for property"`. Use `resize()` or `resizeWithoutConstraints()` instead. + +Note: `x` and `y` are **not** read-only and can be set directly. + +```js +// WRONG — throws "no setter for property" +node.width = 300 +node.height = 64 + +// CORRECT — use resize() to change dimensions +node.resize(300, 64) // change both +node.resize(300, node.height) // change width only +node.resize(node.width, 64) // change height only + +// CORRECT — x and y are writable directly +node.x = 100 +node.y = 200 +``` + +For sections and component sets, use `resizeWithoutConstraints()` instead of `resize()` (see the sections gotcha above). + ## `resize()` resets `primaryAxisSizingMode` and `counterAxisSizingMode` to FIXED `resize(w, h)` silently resets **both** sizing modes to `FIXED`. If you call it after setting `HUG`, the frame locks to the exact pixel value you passed — even a throwaway like `1`. @@ -499,7 +501,7 @@ section.appendChild(someNode) // node may be outside section bounds const section = figma.createSection() section.name = "My Section" section.appendChild(someNode) -section.resizeWithoutConstraints( +section.resize( Math.max(someNode.width + 100, 800), Math.max(someNode.height + 100, 600) ) @@ -614,6 +616,49 @@ When constructing a `var(--name)` string from a Figma variable name, replace BOT // Preferred — use the source CSS name directly v.setVariableCodeSyntax('WEB', `var(${token.cssVar})`) // e.g. '--color-bg-brand-secondary-hover' +## Calling type-specific methods without checking node type + +Some methods only exist on specific node types. Calling them on the wrong type throws "TypeError: not a function". Always guard with a type check before calling type-specific methods. + +```js +// WRONG — node might not be a TextNode +const node = await figma.getNodeByIdAsync('952:1253'); +const segments = node.getStyledTextSegments(['hyperlink']); // TypeError if node isn't TEXT + +// CORRECT — check type first +const node = await figma.getNodeByIdAsync('952:1253'); +if (!node || node.type !== 'TEXT') return { error: `Expected TextNode, got ${node?.type ?? 'null'}` }; +const segments = node.getStyledTextSegments(['hyperlink']); +``` + +Common type-specific methods and the types that have them: + +| Method | Node type required | +|--------|-------------------| +| `getStyledTextSegments()` | `TEXT` | +| `setRangeFontName()`, `setRangeFontSize()` | `TEXT` | +| `createInstance()` | `COMPONENT` | +| `addComponentProperty()` | `COMPONENT`, `COMPONENT_SET` | +| `createVariant()` | `COMPONENT_SET` | + +## Setting a non-existent property throws "object is not extensible" + +Figma plugin API node objects are non-extensible — you cannot add new properties to them. Setting a property name that doesn't exist on a node type throws `"Cannot add property X, object is not extensible"` (surfaced as `"object is not extensible"`). This only fires on **write**, and only for properties not defined on that node type. + +```js +// WRONG — 'strokeDashes' does not exist on VectorNode; throws "object is not extensible" +const v = figma.createVector() +v.strokeDashes = [4, 8] // Error! + +// CORRECT — the actual property is dashPattern +v.dashPattern = [4, 8] + +// WRONG — any invented property name throws the same error +node.customColor = '#ff0000' // Error — not a real API property +``` + +**How to avoid this**: Before setting any property, verify it exists on the node type by grepping [plugin-api-standalone.d.ts](plugin-api-standalone.d.ts). Property names that sound plausible but aren't in the typings will always throw. + ## `detachInstance()` invalidates ancestor node IDs When `detachInstance()` is called on a nested instance inside a library component instance, the parent instance may also get implicitly detached (converted from INSTANCE to FRAME with a **new ID**). Any previously cached ID for the parent becomes invalid. diff --git a/plugins/figma/skills/figma-use/references/maintainers.yml b/plugins/figma/skills/figma-use/references/maintainers.yml deleted file mode 100644 index c2af2921..00000000 --- a/plugins/figma/skills/figma-use/references/maintainers.yml +++ /dev/null @@ -1,12 +0,0 @@ -api-reference.md: mcp_server -common-patterns.md: mcp_server -component-patterns.md: mcp_server -effect-style-patterns.md: mcp_server -gotchas.md: mcp_server -plugin-api-patterns.md: mcp_server -plugin-api-standalone.d.ts: mcp_server -plugin-api-standalone.index.md: mcp_server -text-style-patterns.md: mcp_server -validation-and-recovery.md: mcp_server -variable-patterns.md: mcp_server -working-with-design-systems: mcp_server diff --git a/plugins/figma/skills/figma-use/references/plugin-api-patterns.md b/plugins/figma/skills/figma-use/references/plugin-api-patterns.md index 5d0e3677..5e0aa2fd 100644 --- a/plugins/figma/skills/figma-use/references/plugin-api-patterns.md +++ b/plugins/figma/skills/figma-use/references/plugin-api-patterns.md @@ -178,17 +178,28 @@ node.fills = [ ### Setting Up Auto Layout +**Prefer `figma.createAutoLayout()`** — it returns a frame with `layoutMode` already set and both axes hugging content, so children can immediately use `layoutSizingHorizontal/Vertical = "FILL"`. + +```javascript +const frame = figma.createAutoLayout(); // HORIZONTAL by default +const column = figma.createAutoLayout("VERTICAL"); + +// Customize from there as usual: +frame.itemSpacing = 16; +frame.paddingTop = 24; +frame.paddingBottom = 24; +frame.paddingLeft = 24; +frame.paddingRight = 24; +``` + +If you need a non-auto-layout frame, use `figma.createFrame()` and set the properties manually: + ```javascript const frame = figma.createFrame(); frame.layoutMode = "VERTICAL"; // or "HORIZONTAL" frame.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 @@ -338,7 +349,7 @@ group.name = "Grouped Elements"; ```javascript const section = figma.createSection(); section.name = "My Section"; -section.resizeWithoutConstraints(800, 600); +section.resize(800, 600); // `resize` and `resizeWithoutConstraints` are equivalent on sections section.x = 0; section.y = 0; // IMPORTANT: Sections don't auto-resize — always resize after adding content diff --git a/plugins/figma/skills/figma-use/references/plugin-api-standalone.d.ts b/plugins/figma/skills/figma-use/references/plugin-api-standalone.d.ts index 98984682..fc8aea7c 100644 --- a/plugins/figma/skills/figma-use/references/plugin-api-standalone.d.ts +++ b/plugins/figma/skills/figma-use/references/plugin-api-standalone.d.ts @@ -1065,6 +1065,32 @@ interface PluginAPI { * ``` */ createFrame(): FrameNode + /** + * Note: This API is only available via `use_figma` in the MCP server + * + * Creates a new frame with auto layout already enabled. Both axes default to hug content + * (`primaryAxisSizingMode = "AUTO"`, `counterAxisSizingMode = "AUTO"`), so children can + * immediately use `layoutSizingHorizontal/Vertical = "FILL"` after being appended. + * + * @remarks + * + * Prefer this over `createFrame()` whenever you need an auto-layout parent. Since `layoutMode` is + * already set, children can use `FILL` sizing immediately after being appended. + * + * The default direction is `"HORIZONTAL"`. Pass `"VERTICAL"` for a column layout. + * + * ```ts title="Create an auto-layout frame" + * const row = figma.createAutoLayout() + * const column = figma.createAutoLayout("VERTICAL") + * + * row.itemSpacing = 16 + * row.paddingTop = 24 + * row.paddingBottom = 24 + * row.paddingLeft = 24 + * row.paddingRight = 24 + * ``` + */ + createAutoLayout(direction?: 'HORIZONTAL' | 'VERTICAL'): FrameNode /** * Note: This API is only available in Figma Design * @@ -10777,6 +10803,16 @@ interface SectionNode * */ resizeWithoutConstraints(width: number, height: number): void + /** + * Resizes the section node. Sections do not propagate constraints to their + * children, so this behaves equivalently to {@link SectionNode.resizeWithoutConstraints} + * and is provided to match the resize ergonomics of other resizable nodes. + * + * @param width - New width of the node. Must be >= 0.01 + * @param height - New height of the node. Must be >= 0.01 + * + */ + resize(width: number, height: number): void } /** * @see https://developers.figma.com/docs/plugins/api/SlideNode @@ -11289,5 +11325,104 @@ interface RadialRepeatModifier extends RepeatModifier { repeatType: 'RADIAL' } +// ============================================================ +// Additional APIs (available via use_figma) +// ============================================================ + +/** + * Result returned by node.query(). Iterable with for...of. + */ +interface QueryResult { + /** Number of matched nodes */ + readonly length: number + /** First matched node, or null */ + first(): SceneNode | null + /** Last matched node, or null */ + last(): SceneNode | null + /** Convert to regular array */ + toArray(): SceneNode[] + /** Iterate with callback. Returns this for chaining. */ + each(callback: (node: SceneNode, index: number) => void): QueryResult + /** Map to new array */ + map(callback: (node: SceneNode, index: number) => T): T[] + /** Filter to new QueryResult */ + filter(callback: (node: SceneNode, index: number) => boolean): QueryResult + /** Extract property values from all matched nodes */ + values(keys: string[]): Record[] + /** Set properties on all matched nodes */ + set(props: Record): QueryResult + /** Sub-query within matched nodes */ + query(selector: string): QueryResult + [Symbol.iterator](): Iterator +} + +/** + * Options for node.screenshot() + */ +interface ScreenshotOptions { + /** Export scale. Default: 0.5 (auto-capped so max output dimension ≤ 1024px). */ + scale?: number + /** When false, includes overlapping content from sibling nodes. Default: true. */ + contentsOnly?: boolean +} + +// Additional node methods +interface BaseNodeMixin { + /** + * Set multiple properties at once. Returns this for chaining. + * Priority keys (e.g. layoutMode) are applied first regardless of object key order. + * width/height are routed through resize() automatically. + * + * @example + * node.set({ opacity: 0.5, cornerRadius: 8, name: "Card" }) + */ + set(props: Record): this + + /** + * CSS-like selector query within this node's subtree. + * Selector syntax: type (FRAME, TEXT), [attr=val], >, :first-child, :nth-child(n), comma unions. + * + * @example + * frame.query('TEXT[name=Title]') + * figma.currentPage.query('FRAME[name^=Card] > TEXT') + */ + query(selector: string): QueryResult + + /** + * Test if this node matches a CSS-like selector. + */ + matches(selector: string): boolean + + /** + * Capture a PNG screenshot of this node and return it inline in the response. + * + * @example + * await frame.screenshot() // default scale + * await frame.screenshot({ scale: 2 }) // hi-res + * await frame.screenshot({ contentsOnly: false }) // include overlapping content + */ + screenshot(options?: ScreenshotOptions): Promise + + /** + * Show/hide a shimmer overlay on this node indicating work in progress. + */ + placeholder: boolean +} + +// Additional figma.* APIs +interface PluginAPI { + /** File I/O namespace for writing images and data. */ + readonly io: { + /** + * Write a file (.png, .json, .csv). For images, data should be a Uint8Array from exportAsync(). + * + * @example + * const bytes = await node.exportAsync({ format: 'PNG' }) + * figma.io.write('screenshot.png', bytes) + */ + write(path: string, data: Uint8Array | string): void + } +} + // prettier-ignore export { ArgFreeEventType, PluginAPI, VersionHistoryResult, VariablesAPI, LibraryVariableCollection, LibraryVariable, AnnotationsAPI, BuzzAPI, BuzzTextField, BuzzMediaField, BuzzAssetType, TeamLibraryAPI, PaymentStatus, PaymentsAPI, ClientStorageAPI, NotificationOptions, NotifyDequeueReason, NotificationHandler, ShowUIOptions, UIPostMessageOptions, OnMessageProperties, MessageEventHandler, UIAPI, UtilAPI, ColorPalette, ColorPalettes, ConstantsAPI, CodegenEvent, CodegenPreferences, CodegenPreferencesEvent, CodegenResult, CodegenAPI, DevResource, DevResourceWithNodeId, LinkPreviewEvent, PlainTextElement, LinkPreviewResult, AuthEvent, DevResourceOpenEvent, AuthResult, VSCodeAPI, DevResourcesAPI, TimerAPI, ViewportAPI, TextReviewAPI, ParameterValues, SuggestionResults, ParameterInputEvent, ParametersAPI, RunParametersEvent, OpenDevResourcesEvent, RunEvent, SlidesViewChangeEvent, CanvasViewChangeEvent, DropEvent, DropItem, DropFile, DocumentChangeEvent, StyleChangeEvent, StyleChange, BaseDocumentChange, BaseNodeChange, RemovedNode, CreateChange, DeleteChange, PropertyChange, BaseStyleChange, StyleCreateChange, StyleDeleteChange, StylePropertyChange, DocumentChange, NodeChangeProperty, NodeChangeEvent, NodeChange, StyleChangeProperty, TextReviewEvent, TextReviewRange, Transform, Vector, Rect, RGB, RGBA, FontName, TextCase, TextDecoration, TextDecorationStyle, FontStyle, TextDecorationOffset, TextDecorationThickness, TextDecorationColor, OpenTypeFeature, ArcData, DropShadowEffect, InnerShadowEffect, BlurEffectBase, BlurEffectNormal, BlurEffectProgressive, BlurEffect, NoiseEffectBase, NoiseEffectMonotone, NoiseEffectDuotone, NoiseEffectMultitone, NoiseEffect, TextureEffect, GlassEffect, Effect, ConstraintType, Constraints, ColorStop, ImageFilters, SolidPaint, GradientPaint, ImagePaint, VideoPaint, PatternPaint, Paint, Guide, RowsColsLayoutGrid, GridLayoutGrid, LayoutGrid, ExportSettingsConstraints, ExportSettingsImage, ExportSettingsSVGBase, ExportSettingsSVG, ExportSettingsSVGString, ExportSettingsPDF, ExportSettingsREST, ExportSettings, WindingRule, VectorVertex, VectorSegment, VectorRegion, VectorNetwork, VectorPath, VectorPaths, LetterSpacing, LineHeight, LeadingTrim, HyperlinkTarget, TextListOptions, BlendMode, MaskType, Font, TextStyleOverrideType, StyledTextSegment, TextPathStartData, Reaction, VariableDataType, ExpressionFunction, Expression, VariableValueWithExpression, VariableData, ConditionalBlock, DevStatus, Action, SimpleTransition, DirectionalTransition, Transition, Trigger, Navigation, Easing, EasingFunctionBezier, EasingFunctionSpring, OverflowDirection, OverlayPositionType, OverlayBackground, OverlayBackgroundInteraction, PublishStatus, ConnectorEndpointPosition, ConnectorEndpointPositionAndEndpointNodeId, ConnectorEndpointEndpointNodeIdAndMagnet, ConnectorEndpoint, ConnectorStrokeCap, BaseNodeMixin, PluginDataMixin, DevResourcesMixin, DevStatusMixin, SceneNodeMixin, VariableBindableNodeField, VariableBindableTextField, VariableBindablePaintField, VariableBindablePaintStyleField, VariableBindableColorStopField, VariableBindableEffectField, VariableBindableEffectStyleField, VariableBindableLayoutGridField, VariableBindableGridStyleField, VariableBindableComponentPropertyField, VariableBindableComponentPropertyDefinitionField, StickableMixin, ChildrenMixin, ConstraintMixin, DimensionAndPositionMixin, LayoutMixin, AspectRatioLockMixin, BlendMixin, ContainerMixin, DeprecatedBackgroundMixin, StrokeCap, StrokeJoin, HandleMirroring, AutoLayoutMixin, GridTrackSize, GridLayoutMixin, AutoLayoutChildrenMixin, GridChildrenMixin, InferredAutoLayoutResult, DetachedInfo, MinimalStrokesMixin, IndividualStrokesMixin, MinimalFillsMixin, VariableWidthPoint, PresetVariableWidthStrokeProperties, CustomVariableWidthStrokeProperties, VariableWidthStrokeProperties, ComplexStrokeProperties, ScatterBrushProperties, StretchBrushProperties, BrushStrokeProperties, DynamicStrokeProperties, GeometryMixin, ComplexStrokesMixin, CornerMixin, RectangleCornerMixin, ExportMixin, FramePrototypingMixin, VectorLikeMixin, ReactionMixin, DocumentationLink, PublishableMixin, DefaultShapeMixin, BaseFrameMixin, DefaultFrameMixin, OpaqueNodeMixin, MinimalBlendMixin, Annotation, AnnotationProperty, AnnotationPropertyType, AnnotationsMixin, Measurement, MeasurementSide, MeasurementOffset, MeasurementsMixin, VariantMixin, ComponentPropertiesMixin, BaseNonResizableTextMixin, NonResizableTextMixin, NonResizableTextPathMixin, TextSublayerNode, DocumentNode, ExplicitVariableModesMixin, PageNode, FrameNode, GroupNode, TransformGroupNode, SliceNode, RectangleNode, LineNode, EllipseNode, PolygonNode, StarNode, VectorNode, TextNode, TextPathNode, ComponentPropertyType, InstanceSwapPreferredValue, ComponentPropertyOptions, ComponentPropertyDefinitions, ComponentSetNode, ComponentNode, ComponentProperties, InstanceNode, BooleanOperationNode, StickyNode, StampNode, TableNode, TableCellNode, HighlightNode, WashiTapeNode, ShapeWithTextNode, CodeBlockNode, LabelSublayerNode, ConnectorNode, VariableResolvedDataType, VariableAlias, VariableValue, VariableScope, CodeSyntaxPlatform, Variable, VariableCollection, ExtendedVariableCollection, AnnotationCategoryColor, AnnotationCategory, WidgetNode, EmbedData, EmbedNode, LinkUnfurlData, LinkUnfurlNode, MediaData, MediaNode, SectionNode, SlideNode, SlideRowNode, SlideGridNode, InteractiveSlideElementNode, SlideTransition, BaseNode, SceneNode, NodeType, StyleType, InheritedStyleField, StyleConsumers, BaseStyleMixin, PaintStyle, TextStyle, EffectStyle, GridStyle, BaseStyle, Image, Video, BaseUser, User, ActiveUser, FindAllCriteria, TransformModifier, RepeatModifier, LinearRepeatModifier, RadialRepeatModifier } diff --git a/plugins/figma/skills/figma-use/references/plugin-api-standalone.index.md b/plugins/figma/skills/figma-use/references/plugin-api-standalone.index.md index 4b3b6eb7..87aaafb3 100644 --- a/plugins/figma/skills/figma-use/references/plugin-api-standalone.index.md +++ b/plugins/figma/skills/figma-use/references/plugin-api-standalone.index.md @@ -36,6 +36,7 @@ | Method | Returns | | ----------------------------------- | --------------------------- | | `createFrame()` | `FrameNode` | +| `createAutoLayout(direction?)` | `FrameNode` | | `createComponent()` | `ComponentNode` | | `createComponentFromNode(node)` | `ComponentNode` | | `createRectangle()` | `RectangleNode` | @@ -435,3 +436,30 @@ ExportSettingsConstraints User ActiveUser BaseUser Image Video VersionHistoryResult FindAllCriteria ``` + +--- + +## Additional APIs (available via use_figma) + +### Node Methods + +| Method / Property | Returns / Type | Description | +| ----------------------------- | ----------------- | ----------- | +| `node.query(selector)` | `QueryResult` | CSS-like selector search within subtree | +| `node.matches(selector)` | `boolean` | Test if node matches a selector | +| `node.set(props)` | `this` | Set multiple properties at once, chainable | +| `await node.screenshot(opts?)` | `Promise` | Capture PNG inline in tool response | +| `node.placeholder` | `boolean` | Show/hide shimmer overlay | + +### figma.io Namespace + +| Method | Returns | Description | +| ----------------------------- | ----------------- | ----------- | +| `figma.io.write(path, data)` | `void` | Write image/data to be returned in tool response | + +### Types + +| Type | Description | +| ------------------- | ----------- | +| `QueryResult` | Iterable result from `node.query()` with `.first()`, `.last()`, `.each()`, `.map()`, `.filter()`, `.values()`, `.set()`, `.query()` | +| `ScreenshotOptions` | `{ scale?: number, contentsOnly?: boolean }` | diff --git a/plugins/figma/skills/figma-use/references/text-style-patterns.md b/plugins/figma/skills/figma-use/references/text-style-patterns.md index 39daa108..575dca37 100644 --- a/plugins/figma/skills/figma-use/references/text-style-patterns.md +++ b/plugins/figma/skills/figma-use/references/text-style-patterns.md @@ -73,7 +73,7 @@ function createTextStyleFull(name, fontName, fontSize, lineHeight, letterSpacing ## Discovering Available Font Styles -Font style names vary per provider and per file (`"SemiBold"` vs `"Semi Bold"`). Use `figma.listAvailableFontsAsync()` to discover exact style strings — never guess or probe with try/catch: +Font style names vary per provider and per file. Use `figma.listAvailableFontsAsync()` to discover exact style strings — never guess or probe with try/catch: ```javascript /** @@ -88,47 +88,12 @@ async function getAvailableFontStyles(family) { .filter(f => f.fontName.family === family) .map(f => f.fontName.style); } - -/** - * Loads a font, falling back to an alternative style if the requested one is unavailable. - * - * @param {string} family - Font family name - * @param {string} preferredStyle - Desired style, e.g. "Semi Bold" - * @param {string} [fallbackStyle="Regular"] - Fallback if preferred is unavailable - * @returns {Promise} - The FontName that was actually loaded - */ -async function loadFontWithFallback(family, preferredStyle, fallbackStyle = "Regular") { - const allFonts = await figma.listAvailableFontsAsync(); - const familyFonts = allFonts.filter(f => f.fontName.family === family); - - const match = familyFonts.find(f => f.fontName.style === preferredStyle); - if (match) { - await figma.loadFontAsync(match.fontName); - return match.fontName; - } - - const fallback = familyFonts.find(f => f.fontName.style === fallbackStyle); - if (fallback) { - await figma.loadFontAsync(fallback.fontName); - return fallback.fontName; - } - - // Last resort: load the first available style in the family - if (familyFonts.length > 0) { - await figma.loadFontAsync(familyFonts[0].fontName); - return familyFonts[0].fontName; - } - - throw new Error(`Font family "${family}" not available in this file`); -} ``` ## Creating a Type Ramp (Multi-Step) Handles font loading, deduplication, and idempotency. Each entry: `[name, fontFamily, fontStyle, fontSize_px, lineHeight, cssVar]`. -**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. diff --git a/plugins/figma/skills/figma-use/references/working-with-design-systems/maintainers.yml b/plugins/figma/skills/figma-use/references/working-with-design-systems/maintainers.yml deleted file mode 100644 index fac5823a..00000000 --- a/plugins/figma/skills/figma-use/references/working-with-design-systems/maintainers.yml +++ /dev/null @@ -1,9 +0,0 @@ -wwds-components--creating.md: mcp_server -wwds-components--using.md: mcp_server -wwds-components.md: mcp_server -wwds-effect-styles.md: mcp_server -wwds-text-styles.md: mcp_server -wwds-variables--creating.md: mcp_server -wwds-variables--using.md: mcp_server -wwds-variables.md: mcp_server -wwds.md: mcp_server diff --git a/plugins/figma/skills/figma-use/references/working-with-design-systems/wwds-text-styles.md b/plugins/figma/skills/figma-use/references/working-with-design-systems/wwds-text-styles.md index 5c165bc8..454d993c 100644 --- a/plugins/figma/skills/figma-use/references/working-with-design-systems/wwds-text-styles.md +++ b/plugins/figma/skills/figma-use/references/working-with-design-systems/wwds-text-styles.md @@ -50,26 +50,16 @@ The following fields can be bound to variables via `style.setBoundVariable(field To unbind: `style.setBoundVariable(field, null)` -**Important: `setBoundVariable` is NOT available on `TextStyle` in `use_figma`.** - -It is only available in interactive plugin context (UI plugins, Figma editor). When running through `use_figma`, calling `ts.setBoundVariable(...)` will throw `"not a function"`. Set raw values directly instead: +**Important: where possible, use `setBoundVariable` instead of raw values** ```js -// In use_figma — variable binding not available on TextStyle const ts = figma.createTextStyle(); -ts.fontSize = 24; // set directly; cannot bind to a variable +ts.fontSize = 24; // set directly; not bound to a variable -// In an interactive plugin — variable binding works const ts = figma.createTextStyle(); -ts.setBoundVariable("fontSize", fontSizeVariable); +ts.setBoundVariable("fontSize", fontSizeVariable); // preferred if the variable exists. ``` -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 - ### 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. @@ -77,8 +67,7 @@ Once you have a `TextStyle`, apply it to a `TextNode` by assigning its `id` to t ## 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 call `await figma.listAvailableFontsAsync()` to discover exact style strings before loading — never guess or probe with try/catch. -- **`setBoundVariable` not available in `use_figma`**: `TextStyle.setBoundVariable()` throws `"not a function"` in `use_figma`. Set raw values instead and bind interactively if needed. +- **Font style names are file-dependent**: Font style names vary by font provider and Figma file. Always call `await figma.listAvailableFontsAsync()` to discover exact style strings before loading — never guess or probe with try/catch. - **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.