diff --git a/.claude/settings.json b/.claude/settings.json index e9658ef..18b25a1 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -36,7 +36,32 @@ "WebFetch(domain:figma.com)", "WebFetch(domain:help.figma.com)", "WebFetch(domain:developers.figma.com)", - "WebFetch(domain:developer.mozilla.org)" + "WebFetch(domain:developer.mozilla.org)", + "mcp__figma__whoami", + "mcp__figma__get_metadata", + "mcp__figma__get_libraries", + "mcp__figma__use_figma", + "mcp__figma__get_screenshot", + "mcp__figma__search_design_system", + "mcp__figma__get_variable_defs", + "mcp__figma__get_design_context", + "mcp__figma__get_code_connect_map", + "mcp__figma__add_code_connect_map", + "mcp__figma__get_code_connect_suggestions", + "mcp__figma__send_code_connect_mappings" + ] + }, + "hooks": { + "PostToolUse": [ + { + "matcher": "mcp__figma__use_figma", + "hooks": [ + { + "type": "command", + "command": "echo 'QA: Verify style/variable bindings on all returned node IDs.'" + } + ] + } ] } } diff --git a/.claude/skills/component-rules/SKILL.md b/.claude/skills/component-rules/SKILL.md new file mode 100644 index 0000000..2724609 --- /dev/null +++ b/.claude/skills/component-rules/SKILL.md @@ -0,0 +1,71 @@ +--- +name: component-rules +description: "Triggers when building any UI element in Figma — 'create a card', 'build a nav', 'add a section', 'make a component'. Enforces library-first component lookup, correct Auto Layout structure, and semantic node naming. For visual property binding (colors, text styles, spacing values), defer to figma-style-binding." +disable-model-invocation: false +--- + +# Component Rules + +Governs how Claude constructs UI in Figma. Supplements `figma-generate-design`. For visual property binding, defer to `figma-style-binding`. + +--- + +## Rule 1 — Library-first hierarchy + +Before building anything, resolve the component source in this order: + +``` +1. search_design_system → importComponentByKeyAsync → createInstance() +2. Local file scan → createInstance() +3. Build from scratch — ONLY if nothing matches +``` + +Never rebuild primitives the DS provides: Button, Input, Checkbox, Toggle, Badge, Tag, Avatar, Icon, Tab, Breadcrumb, Toast, Alert, Spinner. + +```js +// Library import +const comp = await figma.importComponentByKeyAsync("key_from_search"); +const instance = comp.createInstance(); +parent.appendChild(instance); + +// For component sets (variants) +const set = await figma.importComponentSetByKeyAsync("key"); +const instance = set.defaultVariant.createInstance(); +``` + +--- + +## Rule 2 — Auto Layout + +Every container must use Auto Layout. Property order matters: + +1. Set `layoutMode` FIRST (`"VERTICAL"` or `"HORIZONTAL"`) +2. Set `layoutSizingHorizontal` / `layoutSizingVertical` AFTER `appendChild` +3. Set `layoutMode` BEFORE any `setBoundVariable` call + +| Goal | primaryAxisSizing | counterAxisSizing | layoutSizingH | layoutSizingV | +|---|---|---|---|---| +| Hug both | AUTO | AUTO | HUG | HUG | +| Fixed card | FIXED | FIXED | FIXED | FIXED | +| Full-width section | AUTO | FIXED | FILL | HUG | + +--- + +## Rule 3 — Node naming + +Name every node semantically with slash hierarchy. Never leave defaults. + +``` +Card / Container Card / Title Card / Body Card / Action Row +Hero / Background Nav / Link Group Button / Primary +``` + +--- + +## Rule 4 — Incremental building + +One section per `use_figma` call. Validate via screenshot after each step. Return all created/mutated node IDs: + +```js +return { created: { card: card.id, title: title.id } }; +``` diff --git a/.claude/skills/figma-preflight/SKILL.md b/.claude/skills/figma-preflight/SKILL.md new file mode 100644 index 0000000..607d615 --- /dev/null +++ b/.claude/skills/figma-preflight/SKILL.md @@ -0,0 +1,117 @@ +--- +name: figma-preflight +description: "Triggers on 'let's start', 'begin', 'preflight', 'start the session', or when a Figma file URL is first shared. Verifies MCP connection, reads CLAUDE.md, audits connected libraries, and loads a Token Map of all Styles and Variables — required before any design work." +disable-model-invocation: false +--- + +# Figma Preflight + +Run at the start of every design session. Do NOT start design work until all steps pass. + +**Prerequisite:** Load `figma-use` skill before any `use_figma` call. + +--- + +## Step A — Connection + Config (parallel) + +Run these two in parallel: + +1. **MCP Connection:** Call `mcp__figma__whoami`. Must return user email and plan. Fail → stop, re-authenticate. +2. **CLAUDE.md:** Read CLAUDE.md. Extract Figma file URL (required — stop if missing), font families, session goal. If fonts field is a placeholder, auto-populate after Step C using STRING variables starting with "Family". + +--- + +## Step B — File + Libraries (parallel) + +Parse `fileKey` from the Figma URL, then run in parallel: + +1. **File Access:** Call `get_metadata` with extracted nodeId and fileKey. Must return file name and pages. +2. **Libraries:** Call `get_libraries` with fileKey. Store subscribed libraries as **Library Registry** (name, libraryKey, description). These enable `search_design_system` to find library styles and components during design work. + +--- + +## Step C — Styles + Variables + Components (single use_figma call) + +Combine all three inventories in one script: + +```javascript +const textStyles = await figma.getLocalTextStylesAsync(); +const paintStyles = await figma.getLocalPaintStylesAsync(); +const collections = await figma.variables.getLocalVariableCollectionsAsync(); +const variables = await figma.variables.getLocalVariablesAsync(); + +const grouped = {}; +for (const v of variables) { + const key = v.resolvedType; + if (!grouped[key]) grouped[key] = []; + grouped[key].push({ name: v.name, scopes: v.scopes }); +} + +const components = {}; +for (const page of figma.root.children) { + await figma.setCurrentPageAsync(page); + const sets = page.findAll(n => n.type === "COMPONENT_SET"); + const solos = page.findAll(n => n.type === "COMPONENT" && n.parent.type !== "COMPONENT_SET"); + if (sets.length > 0 || solos.length > 0) { + components[page.name] = { + sets: sets.map(c => c.name), + solos: solos.map(c => c.name).slice(0, 15), + }; + } +} + +return { + textStyles: textStyles.map(s => s.name), + paintStyles: paintStyles.map(s => s.name), + collections: collections.map(c => c.name), + variableCount: variables.length, + byType: Object.fromEntries( + Object.entries(grouped).map(([type, vars]) => [type, vars.map(v => v.name)]) + ), + components +}; +``` + +Store **names only** in context. IDs are looked up on-demand during design work. Library styles/variables are discovered via `search_design_system`. + +--- + +## Token Map + +After Step C, derive a semantic index from variables grouped by `scopes`: + +| Role | Scope | Example names | +|---|---|---| +| Background fill | `FRAME_FILL`, `SHAPE_FILL` | `background/surface`, `color/neutral-100` | +| Text color | `TEXT_FILL` | `text/primary`, `color/neutral-900` | +| Border / stroke | `STROKE_COLOR` | `border/default`, `color/neutral-300` | +| Gap | `GAP` | `gap/sm`, `spacing/xxs` | +| Padding | `PADDING` | `padding/md`, `spacing/section-xl` | +| Border radius | `CORNER_RADIUS` | `radius/sm`, `radius/full` | + +--- + +## Status Report + +``` +✅ MCP Connection — [name] ([email]) · [plan] +✅ CLAUDE.md — Font: [primary] / [code] · Goal: [goal] +✅ Figma File — [file name] · [N] pages +✅ Libraries — [N] connected: [names] +✅ Styles — [N] text + [N] paint +✅ Variables — [N] across [N] collections +✅ Components — [N] sets across [N] pages + +── Token Map ────────────────────────────── +Background : [names] +Text : [names] +Border : [names] +Gap : [names] +Padding : [names] +Radius : [names] +──────────────────────────────────────────── + +Ready to design. +``` + +If any step fails, output ❌ with error and stop. diff --git a/.claude/skills/figma-style-binding/SKILL.md b/.claude/skills/figma-style-binding/SKILL.md new file mode 100644 index 0000000..8bd5227 --- /dev/null +++ b/.claude/skills/figma-style-binding/SKILL.md @@ -0,0 +1,137 @@ +--- +name: figma-style-binding +description: "Triggers on any visual property change in Figma — creating text, setting colors, adjusting spacing/padding/gap/radius. Enforces that ALL values bind to Figma Styles or Variables, never hardcoded. Includes post-write QA verification." +disable-model-invocation: false +--- + +# Style Binding + QA + +Every visual value must come from the design system. Supplements `figma-generate-design`. Prerequisite: `figma-preflight` must have run this session. + +--- + +## Binding Hierarchy + +For any visual property, follow this order. Stop at the first match. + +``` +1. Connected Library → search_design_system → import → apply +2. Local Style → Style Registry → apply by ID +3. Local Variable → Variable Registry → apply by ID +4. Gap found → Report to user, wait for decision +``` + +--- + +## Text + +Every text node must use `textStyleId`. Individual font properties (`fontSize`, `fontFamily`, etc.) are forbidden. + +```js +const style = await figma.getStyleByIdAsync(""); +await figma.loadFontAsync(style.fontName); +node.textStyleId = ""; +``` + +If no local style matches, search libraries via `search_design_system`. If no match anywhere: +``` +⚠️ Text style gap: no style for "[role]". Available: [top 5]. Use closest, or add missing style? +``` + +--- + +## Color Fills + +Every fill/stroke must bind to a COLOR Variable (preferred, supports theming) or Paint Style. + +```js +// Variable binding (preferred) +const variable = await figma.variables.getVariableByIdAsync(""); +const fill = { type: "SOLID", color: { r: 0, g: 0, b: 0 } }; +node.fills = [figma.variables.setBoundVariableForPaint(fill, "color", variable)]; + +// Paint Style binding +node.fillStyleId = ""; +``` + +Never use raw `{ r, g, b }` without a binding. + +--- + +## Spacing, Padding, Gap, Radius + +Bind to FLOAT Variables. `layoutMode` must be set BEFORE `setBoundVariable`. + +```js +node.setBoundVariable("paddingTop", spacingVar); +node.setBoundVariable("paddingBottom", spacingVar); +node.setBoundVariable("paddingLeft", spacingVar); +node.setBoundVariable("paddingRight", spacingVar); +node.setBoundVariable("itemSpacing", spacingVar); +node.setBoundVariable("cornerRadius", radiusVar); +``` + +Spacing can fall back to raw values temporarily with user confirmation. Color and text cannot. + +--- + +## Forbidden / Required + +| Forbidden | Required | +|---|---| +| `node.fontSize = 24` | `node.textStyleId = id` | +| `node.fills = [{ type: "SOLID", color: { r: .2, g: .4, b: 1 } }]` | Variable or Style binding | +| `node.paddingLeft = 16` | `node.setBoundVariable("paddingLeft", var)` | +| `node.cornerRadius = 8` | `node.setBoundVariable("cornerRadius", var)` | +| Creating a Button from scratch | `importComponentByKeyAsync` from library | + +--- + +## QA Verification + +After every `use_figma` call that creates or modifies nodes, run this verification on the returned node IDs: + +```javascript +const nodeIdsToAudit = [/* paste returned IDs */]; +const results = []; + +for (const id of nodeIdsToAudit) { + const node = await figma.getNodeByIdAsync(id); + if (!node) { results.push({ id, status: "NOT_FOUND" }); continue; } + + const checks = []; + + if (node.type === "TEXT") { + checks.push({ prop: "textStyleId", bound: !!node.textStyleId }); + } + + if ("fills" in node && Array.isArray(node.fills) && node.fills.length > 0) { + const bound = !!node.fillStyleId || (node.boundVariables?.fills?.length > 0); + checks.push({ prop: "fills", bound }); + } + + if ("layoutMode" in node && node.layoutMode !== "NONE") { + for (const p of ["paddingLeft","paddingRight","paddingTop","paddingBottom","itemSpacing"]) { + if (p in node) checks.push({ prop: p, bound: !!(node.boundVariables && p in node.boundVariables) }); + } + } + + if ("cornerRadius" in node && node.cornerRadius > 0) { + checks.push({ prop: "cornerRadius", bound: !!(node.boundVariables && "cornerRadius" in node.boundVariables) }); + } + + const failed = checks.filter(c => !c.bound); + results.push({ id, name: node.name, type: node.type, status: failed.length === 0 ? "PASS" : "FAIL", failed: failed.map(c => c.prop) }); +} + +return { auditResults: results }; +``` + +**If FAIL:** Fix each unbound property using the binding rules above, then re-audit. Do not proceed to the next design step until all pass. + +**Report format:** +``` +✅ All [N] nodes passed. +// or +❌ FAIL "Card" (FRAME) — paddingTop, cornerRadius unbound. Fixing... +``` diff --git a/.claude/skills/reference-interpreter/SKILL.md b/.claude/skills/reference-interpreter/SKILL.md new file mode 100644 index 0000000..d48daa8 --- /dev/null +++ b/.claude/skills/reference-interpreter/SKILL.md @@ -0,0 +1,88 @@ +--- +name: reference-interpreter +description: "Triggers when user shares a screenshot, image, URL, or design description — or says 'analyze this', 'make a brief', 'interpret this reference'. Outputs a structured Design Brief mapping visual intent to design system tokens. Waits for 'confirmed' before designing." +disable-model-invocation: false +--- + +# Reference Interpreter + +Analyze a reference (screenshot, URL, or description) and produce a **Design Brief** that maps visual observations to design system tokens. Output the Brief, then **stop and wait for user confirmation** before building anything. + +Prerequisite: `figma-preflight` should have run so the Token Map and Style Registry are available. + +--- + +## Phase 1 — Analyze + +Examine the reference across these dimensions: + +1. **Layout** — structure, columns, section heights, grid width, alignment +2. **Typography** — heading/body hierarchy, weight contrast, tracking +3. **Color** — dark/light sections, accent usage, neutral dominance +4. **Spacing** — generous vs compact, section padding, internal gap +5. **Visual Anchor** — what draws the eye: large type, hero image, illustration +6. **Components** — what UI elements are visible: cards, buttons, forms, nav + +--- + +## Phase 2 — Map to Design System + +For each observation, identify the specific Token or Style from the session's Token Map: + +``` +"Large dark headline" → Text Style: heading/h1 · Color: text/primary +"Neutral section bg" → Variable: background/surface-2 +"Tight card spacing" → Gap: gap/xs · Padding: padding/sm +``` + +If no token exists, flag it: +``` +⚠️ Gap: [observation] — no matching token. Options: (a) nearest match: [name], (b) add token first. +``` + +--- + +## Phase 3 — Output the Design Brief + +``` +## Design Brief + +**Reference**: [source] +**Section**: [what this covers] +**Aesthetic**: [3-5 keywords] + +### Layout +[structure, height, alignment] + +### Typography +- Heading: [Style name] — [why] +- Body: [Style name] + +### Colors +- Background: [Variable] — [context] +- Text: [Variable] +- Accent: [Variable] — [used for] + +### Spacing +- Section padding: [Variable] +- Internal gap: [Variable] + +### Components needed +- [Name]: [from library? source] + +### Gaps +- [Gap description] — awaiting decision +- (none) [if all mapped] +``` + +--- + +## Phase 4 — Wait + +Output exactly: + +``` +Brief complete. Type `confirmed` to begin building, or tell me what to adjust. +``` + +Do NOT call `use_figma` or place any nodes until user types `confirmed`. diff --git a/.storybook/main.ts b/.storybook/main.ts index c211c1f..ae805b3 100644 --- a/.storybook/main.ts +++ b/.storybook/main.ts @@ -9,6 +9,7 @@ const config: StorybookConfig = { "@storybook/addon-vitest", "@storybook/addon-a11y", "@storybook/addon-docs", + "@storybook/addon-designs", "@storybook/addon-mcp" ], "framework": "@storybook/react-vite" diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5a9d3aa..c7bc437 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -107,6 +107,20 @@ src/ - Key tools: `get_design_context`, `get_variable_defs`, `get_screenshot`, `search_design_system`, `use_figma` - Design tokens extracted via `get_variable_defs` → mapped to `@theme` values in `tokens.css` +### Code Connect +- Links Figma components to their React implementations +- Once linked, `get_design_context` returns actual component code instead of generic markup +- Mapped as we build each component via `add_code_connect_map` (label: "React") + ### Storybook MCP - Available at `localhost:6006/mcp` when Storybook dev server is running - Provides: component listing, documentation retrieval, story generation, a11y testing +- `@storybook/addon-designs` embeds Figma frames in the story panel for side-by-side comparison +- `@storybook/addon-mcp` serves the MCP endpoint + +### claude2figma Skills +- 4 skills in `.claude/skills/` that enforce design system compliance when writing to Figma +- **figma-preflight**: Validates MCP connection, audits libraries, builds a Token Map of all styles/variables +- **component-rules**: Library-first lookup, Auto Layout conventions, semantic node naming +- **figma-style-binding**: All visual values must bind to Figma Styles or Variables, never hardcoded; includes post-write QA +- **reference-interpreter**: Converts screenshots/references into structured Design Briefs mapped to design tokens diff --git a/package-lock.json b/package-lock.json index 99823cc..1aeddf8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "@chromatic-com/storybook": "^5.2.1", "@eslint/js": "^10.0.1", "@storybook/addon-a11y": "^10.4.0", + "@storybook/addon-designs": "^11.1.3", "@storybook/addon-docs": "^10.4.0", "@storybook/addon-mcp": "^0.6.0", "@storybook/addon-vitest": "^10.4.0", @@ -916,6 +917,27 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@figspec/components": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@figspec/components/-/components-2.1.0.tgz", + "integrity": "sha512-PFKBX2oFz+vhThKTNsu7Mh4ZT3X7YCiM694UkAMT36j/p0tdmXs9Je0Sf88stTEcMgwYvNv9TZtvniYmgaE+bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@figspec/react": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@figspec/react/-/react-2.0.1.tgz", + "integrity": "sha512-xflqJ3XQZVzm8+7dsm8OFxVAmBNNA3Mg65sqwNHiq7VRSMSD7qwH4BPsBy07ZaX+9nHeaacBpFZd3Q0aIsISqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@figspec/components": "^2.0.1", + "@lit-labs/react": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@fontsource-variable/public-sans": { "version": "5.2.7", "resolved": "https://registry.npmjs.org/@fontsource-variable/public-sans/-/public-sans-5.2.7.tgz", @@ -1056,6 +1078,26 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lit-labs/react": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@lit-labs/react/-/react-2.1.3.tgz", + "integrity": "sha512-OD9h2JynerBQUMNzb563jiVpxfvPF0HjQkKY2mx0lpVYvD7F+rtJpOGz6ek+6ufMidV3i+MPT9SX62OKWHFrQg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/react": "^1.0.3" + } + }, + "node_modules/@lit/react": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@lit/react/-/react-1.0.8.tgz", + "integrity": "sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==", + "dev": true, + "license": "BSD-3-Clause", + "peerDependencies": { + "@types/react": "17 || 18 || 19" + } + }, "node_modules/@mdx-js/react": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", @@ -2125,6 +2167,33 @@ "storybook": "^10.4.0" } }, + "node_modules/@storybook/addon-designs": { + "version": "11.1.3", + "resolved": "https://registry.npmjs.org/@storybook/addon-designs/-/addon-designs-11.1.3.tgz", + "integrity": "sha512-AK+ij478Y6S16TCNPwm7H90OipVe2wZApOlHjC6qDvMW61zyd4yP1icrRtjehSadw5SCoz8HcAmIYfQCOY6E4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@figspec/react": "^2.0.0" + }, + "peerDependencies": { + "@storybook/addon-docs": "^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.0.0 || ^10.0.0-0 || ^10.1.0-0 || ^10.2.0-0" + }, + "peerDependenciesMeta": { + "@storybook/addon-docs": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/@storybook/addon-docs": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.4.0.tgz", diff --git a/package.json b/package.json index e17518d..adde099 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@chromatic-com/storybook": "^5.2.1", "@eslint/js": "^10.0.1", "@storybook/addon-a11y": "^10.4.0", + "@storybook/addon-designs": "^11.1.3", "@storybook/addon-docs": "^10.4.0", "@storybook/addon-mcp": "^0.6.0", "@storybook/addon-vitest": "^10.4.0",