|
| 1 | +--- |
| 2 | +name: figma-component-from-design-system |
| 3 | +description: >- |
| 4 | + Build Axis design-system-compliant Figma components from this codebase using the |
| 5 | + Figma MCP server. USE WHEN the user wants to create, mock up, or generate a UI |
| 6 | + component, screen, or pattern in the "Axis Global components" Figma file from an |
| 7 | + existing storybook composite, or asks to mirror a code component into Figma using |
| 8 | + real Axis Fluent 2 Web Components + bound design tokens (not raw hex/rectangles). |
| 9 | + Covers the code-first discovery loop, Plugin API gotchas, verified token/component |
| 10 | + keys, the boolean/text/instance-swap parameterization recipe, and the doc-template |
| 11 | + workflow for documenting a component from its storybook. |
| 12 | +--- |
| 13 | + |
| 14 | +# Building Figma components from Axis Fluent 2 (code → Figma) |
| 15 | + |
| 16 | +Target file: **"Axis Global components"**, fileKey `0kVLp2qecBWQiQXEQidCeJ`. |
| 17 | + |
| 18 | +Goal: recreate a codebase composite |
| 19 | +(`examples/src/storybook/ui-patterns/components/composites/*.tsx`) as a real Figma |
| 20 | +**main component** built from the org design system — real components + bound tokens, |
| 21 | +never raw hex or detached rectangles. |
| 22 | + |
| 23 | +## Workflow |
| 24 | + |
| 25 | +1. **Code-first.** Grep the component name under |
| 26 | + `examples/**/composites/*.tsx` and its `*.stories.tsx`. Mirror its tokens, |
| 27 | + spacing, layout, and variants from the source of truth. |
| 28 | +2. **`get_libraries(fileKey)`** to confirm the two libraries that matter: |
| 29 | + - **Axis Fluent 2 Web Components** libKey |
| 30 | + `lk-79792b042bb8ea6d9f072a72d0703aa5e6de468d3df881224f7d466047a61c492a70887e7bfc05c8dba114cf560b4264ca8ef2148b83c9c24d87695b50030c5f` |
| 31 | + - **Microsoft Fluent System Icons** libKey |
| 32 | + `lk-d18bb83ecc482ff3c41ebaaa01ef819aa8f0df14460446f03b83ff61703aca33f74ddc59d704f9f8d65b176a4298feb6d29af3dc4c4c0232396b3557d482f952` |
| 33 | +3. **`search_design_system`** scoped with `includeLibraryKeys` to fetch |
| 34 | + component / variable / style keys. Search is fuzzy — search exact names and |
| 35 | + ignore iOS / SF / Material look-alikes from other libraries. |
| 36 | +4. **Build with `use_figma`** (Plugin API JS). Pass `skillNames: "resource:figma-use"`. |
| 37 | +5. **Verify with `get_screenshot`** using `enableBase64Response: true` — local `*.png` |
| 38 | + download is blocked by Axis content-exclusion. `get_design_context` (with |
| 39 | + `disableCodeConnect: true`) also returns an inline screenshot and is the most |
| 40 | + reliable way to eyeball a node after each edit. |
| 41 | + |
| 42 | +## ALWAYS document a new component with the Doc template |
| 43 | + |
| 44 | +Whenever you create (or finish) a component, also produce a documentation page for it |
| 45 | +by cloning the shared **Doc template** — never hand-roll a doc layout. This keeps every |
| 46 | +component's docs visually consistent. |
| 47 | + |
| 48 | +- The template is its own page **"Doc template"** (node `9:2957`); its single child |
| 49 | + frame is **"Tree"** (`9:4236`). This file is **dynamic-page**: a node on a page other |
| 50 | + than `figma.currentPage` is NOT reachable until you call `await page.loadAsync()` on |
| 51 | + its page first. `figma.getNodeById` returns `null` otherwise. |
| 52 | +- Workflow: `const dp = figma.getNodeById("9:2957"); await dp.loadAsync();` → |
| 53 | + `const tpl = dp.children.find(n => n.name === "Tree");` → `const clone = tpl.clone();` |
| 54 | + → append to the target page (`figma.getNodeById("0:1").appendChild(clone)`), rename, |
| 55 | + set `clone.x/clone.y` to park it beside existing content. |
| 56 | +- Template anatomy (names are stable; clone gets fresh ids — re-read with |
| 57 | + `get_metadata` on the clone): |
| 58 | + - `Header > Logo` — leave as-is. |
| 59 | + - `Component name > Title > Frame 13`: child `Title` (text = component name), |
| 60 | + sibling `Text` (the storybook component description), `Links` containing two |
| 61 | + `.link-tags` → `Primary` text nodes (set to e.g. "View documentation" / |
| 62 | + "Storybook"). |
| 63 | + - `image 2` — a placeholder rounded-rectangle hero. Replace it: `remove()` it and |
| 64 | + `insertChild(1, showcase)` a centered auto-layout frame holding a real component |
| 65 | + instance. The `Title` frame is **auto-layout**, so appended children flow in order |
| 66 | + — use `insertChild`/`layoutAlign="STRETCH"`, do NOT rely on absolute `x/y`. |
| 67 | + - `Frame 6`: `Anatomy / Badge > Copy me` badge text (rename to "Anatomy"), and a |
| 68 | + `Group` of `Isolated` example cards each with a `Text` caption. |
| 69 | + - `HR` divider line, then `Variants` containing `Title` + subsections |
| 70 | + `Frame 9 / Frame 11 / Frame 12`, each a `Title` (+ optional subtitle) and an |
| 71 | + `Isolated` canvas. Repurpose each subsection to a storybook story group: clear the |
| 72 | + `Isolated` children, set it to `layoutMode="VERTICAL"` with padding/itemSpacing, |
| 73 | + and append labeled blocks (a Semibold caption text + a component instance). |
| 74 | +- Populate copy straight from the storybook: the `meta`/`docs.description.component` |
| 75 | + becomes the hero description; each exported `Story` (its `args` + its |
| 76 | + `docs.description.story`) becomes one labeled variant block. |
| 77 | +- Map storybook props → the **main component's** instance properties; build every |
| 78 | + example as an `createInstance()` + `setProperties()`. **Do not modify the documented |
| 79 | + component** — only instantiate it. |
| 80 | + |
| 81 | +## Plugin API gotchas (each cost real iterations) |
| 82 | + |
| 83 | +- Import variables via `figma.variables.importVariableByKeyAsync` (NOT |
| 84 | + `figma.importVariableByKeyAsync`). |
| 85 | +- Bind a color token: |
| 86 | + `figma.variables.setBoundVariableForPaint({type:"SOLID",color:{r:0,g:0,b:0},opacity:1},"color",v)` |
| 87 | + then assign to `node.fills` / `node.strokes`. |
| 88 | +- Text styles: `importStyleByKeyAsync` → `await loadFontAsync(style.fontName)` → |
| 89 | + `setTextStyleIdAsync`. |
| 90 | +- Editing an existing TEXT node (`characters`, `textAutoResize`) requires loading |
| 91 | + ITS fonts first: `getRangeAllFontNames()` then `loadFontAsync` each. |
| 92 | +- Set `layoutSizingHorizontal/Vertical` **only after** the node is appended to an |
| 93 | + auto-layout parent. |
| 94 | +- For vertical hug: set the component `primaryAxisSizingMode="AUTO"` AND inner |
| 95 | + frames `counterAxisSizingMode="AUTO"` / `layoutSizingVertical="HUG"`, otherwise a |
| 96 | + wrapped multiline text overflows a fixed height (this once made a divider cut |
| 97 | + through the description text). |
| 98 | +- Icon component **sets** ARE importable directly by the `componentKey` that |
| 99 | + `search_design_system` returns for an `assetType: "component_set"` (Fluent System |
| 100 | + Icons): `const set = await figma.importComponentSetByKeyAsync(key)`. Then pick the |
| 101 | + variant child by `variantProperties` — Fluent icons expose `Size` (`"12".."48"`) and |
| 102 | + `Theme` (`"Regular"`/`"Filled"`); chevrons add `Direction` (`"Up"/"Down"/"Left"/"Right"`). |
| 103 | + `set.children.find(c => Object.entries(props).every(([k,v]) => (c.variantProperties||{})[k] === v))` |
| 104 | + → `.createInstance()` → `.resize(size,size)`. Colorize by binding a token paint onto |
| 105 | + every descendant with a non-empty `fills` array |
| 106 | + (`inst.findAll(n => 'fills' in n && Array.isArray(n.fills) && n.fills.length > 0)`). |
| 107 | + `search_design_system` icon matching is fuzzy/noisy — query the exact icon name and |
| 108 | + read the first `component_set` result. (Older fallback, only if a key is unavailable: |
| 109 | + instantiate a host component, `findAll` instances, match `mainComponent.parent.name`.) |
| 110 | +- `loadAllPagesAsync` is NOT available here; load individual pages with |
| 111 | + `page.loadAsync()` (see doc-template workflow). |
| 112 | +- **`figma.combineAsVariants([comps], parent)`** derives the variant property from the |
| 113 | + component names (`"State=Expanded"` / `"State=Collapsed"` → property `State` with those |
| 114 | + values). DO NOT put the resulting variant set into auto-layout (`set.layoutMode="HORIZONTAL"`) |
| 115 | + — that **freezes each child component's height** at its current fixed value (a shorter |
| 116 | + variant got stuck tall and its footer overflowed the background). Instead set |
| 117 | + `set.layoutMode="NONE"`, per variant `v.primaryAxisSizingMode="AUTO"` (hug), position the |
| 118 | + variants manually side-by-side, then `set.resizeWithoutConstraints(w,h)` to bound the set. |
| 119 | +- When cloning the Doc template, the **Isolated canvas frames have FIXED height** from the |
| 120 | + template. If your component instance is taller than the template's example, it overflows |
| 121 | + and overlaps the next section (clipsContent=false). FIX: make each Isolated hug vertically — |
| 122 | + for a VERTICAL Isolated set `primaryAxisSizingMode="AUTO"`, for a HORIZONTAL one set |
| 123 | + `counterAxisSizingMode="AUTO"`. Ancestor section frames are already AUTO and reflow. |
| 124 | +- **Never `throw` to return diagnostics** — an uncaught error rolls back ALL mutations |
| 125 | + from that `use_figma` call. End with a plain final expression instead, or write data |
| 126 | + into a temp TEXT node and read it back with `get_design_context`, then `remove()` it. |
| 127 | +- An instance's runtime property keys are suffixed and differ from codegen prop names |
| 128 | + (e.g. `"Show actions#16:2"`, not `showActions`). Read the real keys from |
| 129 | + `componentPropertyDefinitions` (build a `baseName → fullKey` map by splitting on |
| 130 | + `#`) before calling `setProperties`; guessing silently no-ops the booleans. |
| 131 | +- In this **dynamic-page** file, sync `figma.getNodeById` is unreliable for deep |
| 132 | + descendants of a freshly-cloned subtree (returns `null` mid-script even when the |
| 133 | + node exists). Use `await figma.getNodeByIdAsync(id)` for every lookup of clone |
| 134 | + children. |
| 135 | +- `figma.currentPage.selection = [node]` throws if `node` lives on a non-active page |
| 136 | + (cross-page selection is illegal) — and that throw rolls back the whole script. |
| 137 | + Call `await figma.setCurrentPageAsync(page)` at the start, and/or wrap the final |
| 138 | + selection in `try/catch`. Same applies to `set figma.currentPage` (not supported — |
| 139 | + use `setCurrentPageAsync`). |
| 140 | + |
| 141 | +## Button (Axis Fluent 2) — key `a39515bf9246e33a7ca60093544b639a4ab31bfa` |
| 142 | + |
| 143 | +Variants: `Style[Primary | Secondary (Default) | Outline | Subtle | Transparent]`, |
| 144 | +`State`, `Size[Large | Medium (Default) | Small]`, |
| 145 | +`Layout[Icon and label (Default) | Icon only]`. |
| 146 | +Props include `Label#157663:151` and `Icon#157715:1057` (boolean). |
| 147 | + |
| 148 | +To set a leading icon: enable `Icon#157715:1057`, then find the nested INSTANCE named |
| 149 | +**"Placeholder"** and `ph.swapComponent(await figma.importComponentByKeyAsync(iconKey))`. |
| 150 | +Do NOT pass icon keys to the Button's INSTANCE_SWAP property — that fails. |
| 151 | + |
| 152 | +Other components: Card set `792c5f710359daacb9045f96efac19da2a082dcd`, |
| 153 | +Spin button set `dfb2afc7193d72dc5e4cd525e387104c1fac24e3` (resize wider, ~80px, or |
| 154 | +it wraps tall). |
| 155 | + |
| 156 | +## Verified token keys (color variables) |
| 157 | + |
| 158 | +| Token | Key | |
| 159 | +|---|---| |
| 160 | +| Neutral/Background/1/Rest | `fac2264608cbaab9b3ef758326f113387441343b` | |
| 161 | +| Neutral/Background/2/Rest | `927459e310c18205e5a4d8d4c0105d0bea0c966f` | |
| 162 | +| Neutral/Foreground/1/Rest | `7b4fababf67f3aa7dcb51d93aa147b48b47a9b30` | |
| 163 | +| Neutral/Foreground/2/Rest | `7bcc04c3456d2df7dba73ad15cab329ff9440e0a` | |
| 164 | +| Brand/Foreground/1/Rest | `4bc56a6988040de75286b200d47c3203b991f3e4` | |
| 165 | +| Brand/Background/1/Rest | `598ea29abf1410c6ca792571d01b4b14e664d29a` | |
| 166 | +| Brand/Stroke/1/Rest | `800d441d12a5a3a5094aea1fee06b914cb1df882` | |
| 167 | +| **Neutral/Stroke/1/Rest** (real divider / colorNeutralStroke1) | `62c3aea8fb72b2f4886958a3e15dc654133b1757` | |
| 168 | +| Neutral/Stroke/2/Rest | `9faa2843efdf7cac8bace16d9a375b44501264b8` | |
| 169 | + |
| 170 | +> ⚠️ `ae2f0c2a9ccbf2b736857b83e400cbde1cd00607` is Neutral/Stroke/**on Brand**/1/Rest |
| 171 | +> (near-white) — NOT a normal divider. Picking it gives a wrong/invisible divider. |
| 172 | +
|
| 173 | +## Verified text style keys |
| 174 | + |
| 175 | +| Style | Key | |
| 176 | +|---|---| |
| 177 | +| Subtitle 1 | `0f5baaeb806fe58b4bfac7c6c0747b2843715f05` | |
| 178 | +| Body 1 | `020362c24075dd02e2fc8965234d194899620770` | |
| 179 | +| Body 1 Strong | `29ef820c9a8d755de0b6999cd09cd75baee53cfe` | |
| 180 | +| Caption 1 | `4870c2b71d882387b33810d647e0514a882d448a` | |
| 181 | +| Caption 1 Strong | `87c3650fd4940e18abeae7576f6789fcb0acba05` | |
| 182 | + |
| 183 | +## Parameterize instead of variant explosion |
| 184 | + |
| 185 | +When the user asks for "proper variants / booleans / slots", add properties on the |
| 186 | +main COMPONENT rather than duplicating frames: |
| 187 | + |
| 188 | +- `addComponentProperty(name, "BOOLEAN", true)` → set |
| 189 | + `node.componentPropertyReferences = { visible: id }` to toggle a region. |
| 190 | +- `addComponentProperty(name, "TEXT", def)` → `{ characters: id }` for editable copy. |
| 191 | +- `addComponentProperty(name, "INSTANCE_SWAP", defaultComponentId, { preferredValues:[{type:"COMPONENT_SET", key: BUTTON_KEY}] })` |
| 192 | + → `{ mainComponent: id }` for swappable slots (e.g. action buttons). |
| 193 | + |
| 194 | +Validate by `createInstance()` + `setProperties()`, read back, then remove the temp |
| 195 | +instance. |
| 196 | + |
| 197 | +## Components already built in this file |
| 198 | + |
| 199 | +- **File Upload** (Page 1) — Card-like dropzone + Button + Cloud Arrow Up / Document |
| 200 | + PDF / Dismiss icons + progress bars. |
| 201 | +- **Section Header** (Page 1, node `16:122`) — meta / title / description + 2 action |
| 202 | + button slots; booleans + text + instance-swap slots. Its **Doc template**-based |
| 203 | + documentation page lives on the same page (frame "Section Header — Documentation") — |
| 204 | + use it as the reference example for the doc-template workflow. |
| 205 | +- **Pagination** (page "Pagination", node `21:1608`) — row counter + Spin button page |
| 206 | + selector + prev/next chevron icon-only buttons; top border Neutral/Stroke/1; |
| 207 | + booleans (`Show row counter` / `Show page selector` / `Show navigation`) + text |
| 208 | + (`Row counter text` / `Page label before` / `Page label after`) + button slots. Its |
| 209 | + **Doc template**-based docs live on the same page (frame "Pagination — |
| 210 | + Documentation"). |
| 211 | +- **Side Navigation** (page "Side Navigation") — now a **variant SET** (`42:193`) with a |
| 212 | + `State` property of `Expanded` / `Collapsed`. Expanded (260px rail): |
| 213 | + bg Neutral/Background/2 + right border Neutral/Stroke/2; toggle (Chevron Left) + |
| 214 | + item rows (44px, 60px icon column, Body 1 label, group chevron), selected item |
| 215 | + (bg Neutral/Background/2/Pressed, Foreground 1, Filled icon, 3×20 yellow indicator |
| 216 | + bound to Brand/Stroke/1/Rest, absolute-positioned at nav left edge), open sub-items |
| 217 | + (36px, paddingLeft 60), divider, footer items. Collapsed (68px icon-only rail): |
| 218 | + toggle (Chevron Right) + icon-only rows, selected item keeps the yellow indicator + |
| 219 | + grey bg + filled icon, divider, footer icons. Real Fluent System Icons (Home / |
| 220 | + Apps / Layer / Person / Settings / Chevron) imported via `importComponentSetByKeyAsync` |
| 221 | + then variant picked by `variantProperties` (`Size`/`Theme`/`Direction`). Its |
| 222 | + **Doc template**-based docs live on the same page (frame "Side Navigation — |
| 223 | + Documentation", `44:157`). |
0 commit comments