| name | build-design-system |
| description | Build a multi-framework design system on top of the @eidra-umain/mosaic kit (Figma-driven tokens, React + Solid adapters, white-label tenants, Ark UI components). Covers both consuming the kit to build a new system and contributing back to the kit itself, plus the Claude-driven workflow to sync the active Figma file's brand values back into tokens.json so the demos pick them up. The kit ships tokens, components, the OKLCH ramp derivation, a Figma plugin for seed sync, build pipeline, and diagnostics. |
| disable-model-invocation | false |
| user-invocable | true |
Build a design system with the Mosaic kit
@eidra-umain/mosaic is the kit. It ships tokens, Ark UI-based React + Solid components, white-label scaffolding, and a Figma plugin so a single seed change re-skins everything. This skill helps you either consume the kit to build a new design system (the common case), or contribute back to the kit when something is missing.
When invoked, identify which path the user is on and confirm before diving in:
- Path A: Consume the kit. User wants a new design system in their own codebase. Most setup work disappears because the kit provides it.
- Path B: Contribute to the kit. User wants to add components, tune tokens, or fix the build inside
eidra-umain/mosaic itself. The detailed multi-phase work lives here.
Do not silently pick a path. Ask the user which they're on if it isn't obvious from context (the cwd being inside the mosaic repo strongly implies Path B; otherwise Path A is the default).
1. The contract (do not deviate)
Both paths inherit these rules from the kit:
- Figma is the source of truth. Tokens and component anatomy are authored in Figma; code is downstream.
- Every kit change is made in BOTH Figma and code. This is the load-bearing principle for keeping designer mocks and rendered apps aligned. A token tweak (e.g. bumping
border/default Dark to a different neutral step) must update (a) tools/figma-export/tokens.json, (b) the matching Figma Semantic variable's mode value, (c) any Figma component that was bound to a different token and now should rebind. A new component must land as (a) CSS + React + Solid adapters, (b) a Figma Component Set on the Components page bound to the same semantic tokens, (c) a card in both demo apps. Skipping either side creates drift that surfaces hours later as "the demo and Figma no longer match". See §1.1 below for the recipe.
- Multi-framework output from one CSS token system. React + Solid today, Web Components addable later without restructuring.
- White-label runtime configurability. Consumers re-skin via CSS custom property overrides at
[data-tenant="..."] on <html>. No rebuild, no fork.
- Brand presence is sparing. Only buttons, focus rings, links, accents, and selection carry tenant colour. A re-skin must never make a button look like the wrong product.
- Not built on top of branded toolkits. No Carbon, Fluent, Mantine, MUI, shadcn, or similar. Ark UI / Zag.js for headless behaviour is fine.
1.1 Both-sides change recipe
For any kit change, write down the side that's easy to forget. The Figma side IS easy to forget because code changes are visible immediately in the demo, while Figma changes are not visible in code unless the designer sync runs.
| Change type | Code side | Figma side |
|---|
Token value (e.g. border/default Dark) | Edit tools/figma-export/tokens.json, pnpm tokens:build, pnpm build | Update recipe 01 (tools/figma-build/01-variables.figma.mjs), pnpm figma-plugin:build, re-run Plugins > Mosaic > Build design system against the file (or edit the Semantic variable's mode value directly in Figma) |
New token (e.g. bg/field) | Add to tokens.json semantic section; component CSS references it | Add the alias to recipe 01, rebind any existing Figma Components that used the old token, rebuild the plugin |
| Brand seed change | pnpm figma:sync-seeds (or hand-edit tokens.json) | Edit the seed primitive in Figma + run Plugins > Mosaic > Sync both to rederive ramp steps; or use the Build command on a fresh file to set both seeds + ramps in one pass |
| New component | CSS in src/styles/, React + Solid adapter, tsup entries, copy-assets order, demo card in both apps | Add the NN-<name>.figma.mjs recipe in tools/figma-build/, add a section to 21-sample-page.figma.mjs, pnpm figma-plugin:build, re-run the plugin build command |
Component rebinding (e.g. swap field bg from bg/canvas to bg/field) | Edit the component CSS | Edit the relevant recipe in tools/figma-build/ and re-run the plugin build command, or use mcp__figma__use_figma for a one-off mutation in an existing file |
Use the pnpm figma:sync-seeds command to pull current Figma seed values into tokens.json automatically. For non-seed token changes there's no equivalent reverse-sync yet; updates flow in the direction the change was authored.
2. Critical rules (read before touching CSS)
These rules also live in the kit's docs/specification.md and bit hard during development. Save 6 months of debugging.
@theme static, never plain @theme. Tailwind v4 tree-shakes plain @theme blocks: any custom property not referenced by a Tailwind utility class gets stripped. The kit emits @theme static so every token survives to :root. Do not remove static.
data-theme and data-tenant live on <html>, never on <body>. CSS resolves var() chains at computed-value time on the defining element. If a tenant override is applied at <body>, the semantic alias declared at :root has already been computed against the default primitive and inherited. Putting the attribute on <html> makes the override target the same node the alias was defined on, so the chain re-resolves.
- Solid JSX ships as
.jsx, not transpiled .js. Solid's reactivity depends on babel-plugin-jsx-dom-expressions, which esbuild cannot run. The kit's tsup config emits .jsx with jsx: preserve; consumers' vite-plugin-solid finishes the transform via the solid export condition.
- Three tsconfigs, one per JSX flavour. React needs
jsx: "react-jsx" + jsxImportSource: "react". Solid needs jsx: "preserve" + jsxImportSource: "solid-js". A single tsconfig can express only one combination; the kit has tsconfig.{base,react,solid}.json.
- Mutate Figma components in place, never
remove() + recreate. A Figma Component has a unique internal key. Existing instances reference that key. Deleting and recreating leaves all existing instances orphaned. Walk children and mutate in place.
- Brand ramps are seed-derived.
color/brand-primary/seed and color/brand-secondary/seed are the only inputs. The 10 steps each are derived in CSS via oklch(from var(--seed) L calc(c * SC) h). Editing individual steps in tenants or in CSS breaks the model. Override the seed only.
- Figma variable names cannot contain
. Use _ instead (e.g. space/0_5, not space/0.5). The kit's tokens.json already enforces this.
- Plugin API gotchas (apply equally to the Figma plugin runtime and
use_figma). figma.currentPage = page is rejected; use await figma.setCurrentPageAsync(page). Font style names contain a space: "Semi Bold", "Extra Bold". getPluginData/setPluginData need a manifest id; recipes use getSharedPluginData("mosaic", key). vectorPaths accepts only M/L/C/Z with explicit space-separated values — see tools/figma-build/icons.mjs for the dialect rules. vector.resize() fills the path bbox; for icons that should preserve viewBox padding (Lucide), use vector.rescale(size / viewBox) instead.
- After toggling data-theme / data-tenant in tests, wait past CSS transitions. The Button has
transition: background-color 120ms ease. getComputedStyle within that window returns mid-transition values. Wait 200 ms (≥ longest transition) before reading.
Path A: Consume the kit to build a new design system
The kit provides tokens, components, build pipeline, and Figma plugin. The consumer adds package name, brand seeds, tenants, and (optionally) a Figma file. Roughly four phases:
A.1: Set up the consumer repo
mkdir my-design-system && cd my-design-system
pnpm init
pnpm add @eidra-umain/mosaic
package.json exports / structure is up to the consumer. Most consumers integrate the kit into an existing app rather than a new package; in that case just pnpm add and skip the rest of A.1.
A.2: Wire the kit in
In the app's entry:
import "@eidra-umain/mosaic/tokens.css";
import "@eidra-umain/mosaic/tenants.css";
import "@eidra-umain/mosaic/styles";
import { Button, Input, Checkbox, Switch, Tabs, Dialog } from "@eidra-umain/mosaic/react";
import { Button, Input, Checkbox, Switch, Tabs, Dialog } from "@eidra-umain/mosaic/solid";
Set data-theme="light" (or dark) and data-tenant="..." on <html> (NEVER <body>).
A.3: Brand the system
The consumer writes one CSS file:
:root {
--color-brand-primary-seed: #16a34a;
--color-brand-secondary-seed: #ee000c;
}
[data-tenant="acme"] { --color-brand-primary-seed: #0ea5e9; --type-family-sans: "IBM Plex Sans"; }
[data-tenant="globex"] { --color-brand-primary-seed: #8b5cf6; --type-family-sans: "Fraunces"; }
The 10 derived ramp steps recompute automatically via OKLCH. Done.
A.4 (optional): Hook up the consumer's own Figma file
If the consumer has their own Figma file with Mosaic-shaped variables (Primitives + Semantic collections, seed variables):
- Install the Mosaic Figma plugin. From a checkout of the mosaic repo run
pnpm install && pnpm figma-plugin:build, then in Figma Desktop: Plugins > Development > Import plugin from manifest → pick tools/figma-plugin/manifest.json. (The plugin is in the mosaic repo, not the npm tarball.)
- After editing a seed in Figma, run Plugins > Mosaic > Sync both. The plugin rederives the 10 dependent ramp variables via OKLCH so designer mocks match the rendered CSS.
If the consumer does not have a Figma file yet, run Plugins > Mosaic > Build design system in a fresh design file to generate a complete one (Tokens / Components / Sample pages, all 17 components, Light + Dark) from a brand-name + two seeds + two fonts. See Path B §B.1 for the deeper details of the file's shape and the recipe pipeline.
A.5: Browser support
The kit uses oklch(from ...) relative-color syntax for seed-derived ramps. This requires Chrome 119+, Safari 16.4+, Firefox 128+ (all shipped 2024). If the consumer needs older browser support, they cannot use the seed model and must fork the tokens.
Path B: Contribute to the kit itself
This is the work of adding a component, tuning the token system, fixing the build, or improving the codegen. The repo is eidra-umain/mosaic.
B.1: Figma file setup (only if creating a new design system in Figma)
For an existing Mosaic file this is already done. For a fresh start:
- Use a Figma plan that supports multi-mode variable collections (Pro+ or org/enterprise). Starter caps each collection at one mode and breaks Light/Dark.
- Create three Variable collections:
- Primitives (one mode):
color/brand-primary/seed, color/brand-secondary/seed, the 10 derived ramp steps per brand (initially hand-set, kept in sync by the seed-sync plugin), neutral / success / warning / danger / info ramps, status-dark counterparts, spacing scale, radii, type sizes, weights, font slot, border widths.
- Semantic (Light + Dark modes):
bg/canvas, fg/default, accent/*, border/*, ring/color, ring/on-solid, per-status solid / subtle / fg / border / fg-on-solid.
- Component (optional, often empty): per-component overrides.
- Brand anchors. Use the values collected during the upfront brand prompt (see "When the user invokes this skill" step 2). The hex values for
color/brand-primary/seed and color/brand-secondary/seed come from those answers, NOT from hardcoded examples. Once set:
- For an existing file: run Plugins > Mosaic > Sync both to populate the 10 dependent step variables from the seeds.
- For a fresh file: run Plugins > Mosaic > Build design system, which sets seeds + derives every ramp + builds every page in one pass.
- If the user picked a non-default sans/mono font, the Build command captures it from the form; for an existing file, update
type/family/sans and type/family/mono in Primitives manually.
- Per-status (success/warning/danger/info) tokens need dark counterparts as sibling primitives (
subtle-dark, solid-dark, fg-dark, border-dark). The Semantic Dark-mode aliases bind to those.
- Focus ring is a dedicated semantic var
ring/color (brand), plus ring/on-solid (neutral/950) for buttons on solid coloured backgrounds. currentColor-based focus fails contrast on solid variants.
warning/fg-on-solid binds to neutral/950 in both modes (white text on amber fails WCAG). Other fg-on-solid slots bind to white (or black for dark mode).
B.1.1 MANDATORY: Build the Figma file via the Mosaic Figma plugin. Do not hand-dispatch recipes via use_figma.
The repo ships one Plugin API recipe per kit component, plus one for variables, one for the Sample page, and one for the Tokens page (see tools/figma-build/README.md). These recipes encode 20+ Plugin API gotchas that bit during the original build (autolayout sizing flips on resize, combineAsVariants ordering, setBoundVariableForPaint, ellipses can't nest children, etc.).
The recipes are bundled into the existing Mosaic Figma plugin (tools/figma-plugin/) via tools/figma-build/bundle.mjs. The plugin runs the full build inside Figma Desktop, with no MCP, no OAuth, no PAT — just the user's standard Figma session.
Why a Figma plugin, not an external MCP runner: Figma's MCP server at mcp.figma.com is gated by the Figma MCP Catalog. Dynamic Client Registration is advertised but rejected (403 Forbidden); third-party MCP clients can't get the mcp:connect scope. The plugin runtime sidesteps that entirely: same Plugin API, same recipes, no auth dance.
When bootstrapping a new design system Figma file, you MUST:
-
Tell the user to install the plugin once (Figma Desktop > Plugins > Development > Import plugin from manifest > pick tools/figma-plugin/manifest.json). If tools/figma-plugin/code.built.js doesn't exist, the user must first run pnpm figma-plugin:build (which assembles the recipes into the plugin entry).
-
Direct the user to run the plugin against the target file (File > New for a fresh design system, or open the existing file; then Plugins > Mosaic > Build design system). The plugin opens a form for the brand name, the two seeds, and the sans/mono font families, then runs all recipes in order with a progress UI.
-
Completeness is enforced by the recipes, not by your dispatch order. The bundler walks every NN-<name>.figma.mjs in numeric order. If the kit has a component, there must be a recipe; if there's a recipe, the bundler picks it up automatically. Adding a kit component (Path B §B.4) requires adding the recipe AND re-running pnpm figma-plugin:build so the plugin entry includes it.
-
Do not paraphrase, simplify, or "improve" the recipes. They are the source of truth. The only sanctioned brand-time customisations are the seeds + fonts captured by the plugin's build UI; recipe 01 reads them as config.primary, config.secondary, config.sans, config.mono and computes the OKLCH ramps at run time. Other customisations belong in the recipe files themselves.
-
Verify completeness with a screenshot of the Sample page AND a screenshot of the Tokens page once the plugin reports success. Use mcp__figma__get_screenshot (still available as a read-only inspection tool). If either is missing or visibly broken, fix the responsible recipe, re-run pnpm figma-plugin:build, ask the user to re-run the plugin; do not paper over with inline workarounds via use_figma.
B.1.2 What lives where in tools/figma-build/ and tools/figma-plugin/
tools/figma-build/NN-<name>.figma.mjs — the Plugin API recipes. Canonical, hand-maintained.
tools/figma-build/_helpers.mjs — shared bootstrap (variable lookups, semPaint, findVariant, page handles). Inlined by bundle.mjs into recipes 02..22.
tools/figma-build/ramps.mjs — OKLCH ramp math reference. The STEPS table is the canonical curve and must match the inlined math in 01-variables.figma.mjs, the STEPS in tools/figma-plugin/code.js, and the derive entries in tools/figma-export/tokens.json.
tools/figma-build/bundle.mjs — wraps each recipe as async function (config) { ... }, prepends tools/figma-plugin/code.js (the plugin glue), writes tools/figma-plugin/code.built.js (the plugin's manifest main). Run via pnpm figma-plugin:build.
tools/figma-plugin/manifest.json — Figma plugin manifest. Menu entries: Build design system, Sync brand-primary/secondary/both, Export brand.css.
tools/figma-plugin/code.js — hand-maintained plugin glue (seed sync, brand.css export, build dispatch). Calls MosaicRecipes.run(...) from the prepended recipes module.
tools/figma-plugin/ui-build.html — build form (seeds, fonts, progress).
tools/figma-plugin/ui.html — export-brand-css UI (existing).
tools/figma-plugin/code.built.js — GENERATED. Concatenation of bundled recipes + code.js. Checked in so the plugin works out of the box; re-run the bundler after editing any recipe or code.js.
B.1.3 Editing the ramp curve
If you tune lightness/chroma (you usually shouldn't), update all four of these in lockstep:
tools/figma-build/ramps.mjs STEPS
tools/figma-plugin/code.js STEPS
tools/figma-export/tokens.json derive entries
- The table in this section below.
The canonical curve:
| step | L | cMul |
|---|
| 50 | 0.97 | 0.08 |
| 100 | 0.94 | 0.15 |
| 200 | 0.87 | 0.40 |
| 300 | 0.78 | 0.65 |
| 400 | 0.72 | 0.88 |
| 500 | seed | anchor |
| 600 | 0.62 | 0.92 |
| 700 | 0.50 | 0.78 |
| 800 | 0.38 | 0.55 |
| 900 | 0.25 | 0.35 |
| 950 | 0.15 | 0.20 |
B.2: Figma MCP usage (read-only inspection)
For full builds, run the Figma plugin's Build design system command (see §B.1.1). The mcp__figma__* tools remain useful for read-only inspection, ad-hoc debugging, and one-off mutations during recipe development:
mcp__figma__whoami – probe authenticated plans, note the org key you want the file in.
mcp__figma__create_new_file – bootstrap a design file outside of --new.
mcp__figma__use_figma – one-off plugin code; reserve for debugging a single recipe interactively, never as the path for a full build.
mcp__figma__get_metadata, get_design_context, get_variable_defs – read structure / resolved variables.
mcp__figma__get_screenshot – render a node to a downloadable PNG. Use this for the post-build verification screenshots in §B.1.1 step 5.
B.3: Token codegen
Lives at tools/figma-export/:
tokens.json: hand-authored or AI-curated dump of Figma variables. Brand seed entries hold { value: hex }; the 10 ramp steps hold { derive: <ramp>, l: <0..1>, cMul: <0..1>, [anchor: true] }.
build-tokens.mjs: reads tokens.json and emits packages/mosaic/src/tokens.css. For derive entries it emits oklch(from var(--<seed>) L calc(c * cMul) h); for anchor: true it emits var(--<seed>); for { value } it emits the literal. The output starts with @import "tailwindcss"; and wraps the primitives + Light-mode semantics in @theme static { ... }, then a [data-theme="dark"] { ... } override block.
- Run via
pnpm tokens:build.
B.4: Component addition (the dominant ongoing task)
A component in the kit has FIVE parts (it used to be four; the Figma recipe is the fifth, mandated by the both-sides rule §1.1):
- CSS at
packages/mosaic/src/styles/<component>.css. Selectors target the data-attribute contract that Ark UI emits ([data-state], [data-disabled], [data-orientation], etc.) under .mosaic-<component> class names. No conditional class strings.
- React adapter at
packages/mosaic/src/react/<component>.tsx. Imports the Ark UI namespace (import { Tabs as ArkTabs } from "@ark-ui/react/tabs";), wraps the Root + slot components, hands them the mosaic-* classNames. Slot components stay as separate exports (Tabs, TabsList, TabsTrigger, TabsContent) when the user authors content, or wrap into a single component when slots aren't needed (Switch, RadioGroup).
- Solid adapter at
packages/mosaic/src/solid/<component>.tsx. Mirror the React API exactly. Use class instead of className, For for lists, Show for conditionals. Note: Ark UI Solid's asChild is (props) => Element (render-prop), not a boolean. For simple cases let Ark UI render the default <button> wrapper instead of using asChild.
- Re-exports in
packages/mosaic/src/react/index.ts and packages/mosaic/src/solid/index.ts. Add entries to tsup.config.ts (both React and Solid entry arrays) and to the order list in scripts/copy-assets.mjs.
- Figma build recipe at
tools/figma-build/NN-<component>.figma.mjs. The Plugin API code that creates the Figma Component Set / Component for this control. tools/figma-build/bundle.mjs inlines _helpers.mjs and icons.mjs for you when it bakes the recipe into the plugin entry, so write the recipe assuming the helpers globals (primByName, semByName, semPaint, findVariant, componentsPage, samplePage, makeLucideIcon, ICONS) are in scope. The recipe must produce variants that bind paints to the same Semantic tokens the CSS references, so a Figma mode-flip mirrors what data-theme does at runtime. Add a section to 21-sample-page.figma.mjs so the showcase frames stay complete; the next pnpm figma-plugin:build run will instantiate it.
The kit aims for the full Ark UI surface. All 17 components are present today: Button, Input, Checkbox, Switch, RadioGroup, Tabs, Tooltip, Dialog, Slider, Accordion, Select, Menu, Toast, Combobox, TagsInput, DatePicker, Tree. Future additions land continuously.
B.4.1: Reading the existing recipes before adding a new one
tools/figma-build/README.md lists 23 Plugin-API gotchas that bit during the original build (ellipses can't nest children, layoutWrap=WRAP requires layoutMode=HORIZONTAL, layoutSizingHorizontal only works post-appendChild, paint binding via setBoundVariableForPaint, the strict-dialect vectorPaths parser, etc.). Skim it before writing a new recipe; copy patterns from the closest existing component (Switch → Radio, Tooltip → simple Components, Tabs → variant set with single-axis state).
B.5: Demo apps as regression surface
apps/demo-react/ and apps/demo-solid/ render the same component matrix. Both apps use data-theme + data-tenant selectors driven by header dropdowns, and both serve as the visual regression target for tools/inspect/snapshot.mjs. When you add a component to the kit, add a demo card showing it in both apps.
B.6: Diagnostics
tools/inspect/snapshot.mjs: boots vite preview for both demos, captures full-page screenshots of every theme x tenant combo into /tmp/mosaic/. Run after any visual change.
tools/inspect/check-tenant.mjs: cascade-rule regression test. Verifies --accent-solid at <html> matches the rendered button background across all theme x tenant combos. Catches accidental moves of data-tenant to <body>.
B.7: Figma plugin
The kit ships a Figma plugin at tools/figma-plugin/ with three menu commands:
- Build design system — runs every recipe in
tools/figma-build/ against the current Figma file, producing Tokens / Components / Sample pages from brand seeds + fonts collected in a form.
- Sync brand-primary / Sync brand-secondary / Sync both — recompute the 10 derived ramp steps after a seed edit, using the same OKLCH math the CSS uses.
- Export brand.css — emit the handoff CSS snippet (seeds + sans family) for the designer to send the developer.
The STEPS ramp table appears in five places that must stay aligned (tools/figma-build/ramps.mjs, 01-variables.figma.mjs, tools/figma-plugin/code.js, tools/figma-plugin/ui-build.html, and the derive entries in tools/figma-export/tokens.json). If you tune the lightness/chroma curve, change all five. See tools/figma-plugin/README.md for the user-facing flow and tools/figma-build/README.md for recipe maintenance.
B.8: Build pipeline
Three sub-builds:
- React: tsup,
jsx: "automatic", output .js.
- Solid: tsup,
jsx: "preserve", outExtension: () => ({ js: ".jsx" }). Consumer's vite-plugin-solid finishes the transform.
- Types: emitted by
tsc -p tsconfig.react.json && tsc -p tsconfig.solid.json (tsup does not emit dts because react + solid need different jsx settings; one tsconfig can't represent both).
- Assets:
scripts/copy-assets.mjs copies tokens.css, tenants.css, and the per-component CSS to dist/, then writes a styles/all.css barrel.
The package exports map declares subpaths for ./tokens.css, ./tenants.css, ./styles, ./styles/*, ./react, ./react/*, ./solid, ./solid/* (and reserves ./wc for later). sideEffects: ["**/*.css"] so CSS imports survive tree-shaking.
B.9: Publishing
- Changesets at
.changeset/. Demo apps are in ignore.
- GitHub Action at
.github/workflows/release.yml using changesets/action@v1. On push to main: builds, on Version Packages PR merge: publishes.
- Repo secret
NPM_TOKEN (npm Automation token, publish rights to @eidra-umain).
B.10: Documentation
docs/specification.md: locked contract + critical rules + decision log. The decision log is the highest-value doc. Each non-trivial choice gets a dated entry with the reason.
packages/mosaic/README.md: npm-facing consumer guide. This is what shows on npm.
tools/figma-plugin/README.md: install + use for the Figma plugin.
Sync the live Figma file to local tokens (Claude-driven)
The Figma plugin can't write files (it's sandboxed; Export brand.css only renders text the user has to paste). When the user says "update the demos from the currently open Figma file" (or any close paraphrase: "sync Figma to the demos", "pull current brand from Figma", "rebrand demos from the file", "import the seeds from Figma"), Claude runs the sync end-to-end via the Figma MCP and tools/figma-export/tokens.json. Do NOT direct the user to the plugin's Export command for this workflow.
Step 0: ALWAYS get the file URL from the user (no defaults, ever)
mcp__figma__use_figma requires a fileKey. The kit has no canonical file — different consumers and different sessions point at different files. There is NO hardcoded fallback in tokens.json, sync-seeds.mjs, or anywhere else. Never guess a file key. Never reuse one from a prior turn. Never read a $meta block or grep the repo for a "default" key — those have been intentionally removed.
Before touching the Figma MCP, ask the user once: "Paste the URL of the Figma file to sync from." Extract :fileKey from figma.com/design/:fileKey/:fileName (or figma.com/design/:fileKey/branch/:branchKey/... — in which case use branchKey). If the user provided the URL earlier in this turn, reuse it. If they haven't, stop and ask. Past sessions don't carry over.
The mcp__figma__* (official Figma) MCP has no "currently open file" concept; every call takes an explicit fileKey. URL paste is the only path.
What to read from Figma
Use mcp__figma__use_figma to run this snippet against the active file (it returns JSON Claude can parse). It reads only the primitives that drive the brand:
(async () => {
const cols = await figma.variables.getLocalVariableCollectionsAsync();
const prims = cols.find(c => c.name === "Primitives");
if (!prims) return { error: "No Primitives collection. Is this a Mosaic-shaped file?" };
const m = prims.modes[0].modeId;
const out = { fileName: figma.root.name };
const hex = (c) => {
const to = (n) => Math.round(n * 255).toString(16).padStart(2, "0");
return `#${to(c.r)}${to(c.g)}${to(c.b)}`;
};
for (const id of prims.variableIds) {
const v = await figma.variables.getVariableByIdAsync(id);
const value = v.valuesByMode[m];
if (v.name === "color/brand-primary/seed" || v.name === "color/brand-secondary/seed") out[v.name] = hex(value);
if (v.name === "type/family/sans" || v.name === "type/family/mono") out[v.name] = String(value);
}
return out;
})();
What to patch in tools/figma-export/tokens.json
Only these four primitive entries. Leave everything else (derived ramps, status colors, semantic aliases, spacing/radii/type scale) untouched.
| tokens.json key | Type | Source field from the snippet above |
|---|
primitives["color/brand-primary/seed"] | COLOR | color/brand-primary/seed |
primitives["color/brand-secondary/seed"] | COLOR | color/brand-secondary/seed |
primitives["type/family/sans"] | STRING | type/family/sans |
primitives["type/family/mono"] | STRING | type/family/mono |
Shape per entry:
- Color:
{ "type": "COLOR", "value": "#xxxxxx" } (lowercased hex)
- Font:
{ "type": "STRING", "value": "<family>" }
Steps Claude performs
- Verify the file is Mosaic-shaped. If the snippet returns
{ error }, stop and tell the user to open the right file in Figma Desktop. Don't guess.
- Compare to the current
tokens.json. If all four values already match, say "already in sync" and skip the rest. No write, no rebuild.
- Patch
tokens.json with the new values. Use Edit with targeted old_string/new_string per entry; do not rewrite the whole file with Write.
- Run
pnpm tokens:build to regenerate packages/mosaic/src/tokens.css.
- Run
pnpm build so the kit's dist/ is fresh (demos consume dist/ via workspace:*, not src/).
- Summarize the change: print a small table of before/after for each updated field (e.g.
primary: #0ea5e9 -> #16a34a). Mention which Figma file the values came from (fileName field).
- Surface the dev server hint: if the user hasn't started one, mention
pnpm --filter demo-react dev and pnpm --filter demo-solid dev. Don't start it yourself unless asked.
What this workflow does NOT cover
- Hand-tuned semantic aliases in Figma (e.g. swapping
bg/canvas Light to a different primitive). The snippet only reads the four brand primitives; if the user changed an alias, do that as a separate Path B §1.1 both-sides edit.
- Per-step ramp tweaks. The kit derives the 10 brand-ramp steps from seeds via OKLCH in CSS; tuning individual steps in Figma is OUT-of-model. Override the seed only.
- New components. That's Path B §B.4, not a sync.
- File structure / page contents. The sync touches
tokens.json and the kit build; it does not rebuild Figma pages.
Consumer-repo fallback
If cwd doesn't contain tools/figma-export/tokens.json (the kit isn't here; the user is in a consumer app), don't invent that path. Ask the user where the brand override CSS lives (typically src/brand.css next to the app entry) and write the equivalent :root { --color-brand-primary-seed: ...; ... } block. Do not run pnpm tokens:build; that's a kit-internal script.
Pitfalls list (read before you start)
| # | Pitfall | Fix |
|---|
| 1 | Tailwind v4 tree-shakes plain @theme blocks | Use @theme static |
| 2 | data-theme / data-tenant on <body> doesn't re-resolve var() chains | Put them on <html> |
| 3 | Solid JSX won't run if pre-transpiled by esbuild | jsx: preserve + .jsx output + solid export condition |
| 4 | React + Solid need different jsx settings | Three separate tsconfigs |
| 5 | Brand ramps drift when tenant overrides individual steps | Override the seed only; the 10 steps derive automatically |
| 6 | Figma seed change doesn't propagate to ramp variables | Run Plugins > Mosaic > Sync both in the Figma plugin |
| 7 | getComputedStyle after toggling theme returns mid-transition values | Wait past the longest CSS transition (200 ms covers 120 ms transitions) |
| 8 | Lit @property decorator with class field initializer breaks reactivity silently | Use static properties = { … } + declare + constructor defaults. Live in every src/lit/*.ts |
| 9 | <slot> doesn't project in Light DOM | For composition use a child custom element (<mosaic-dialog-actions>) or move children manually in connectedCallback; for the host-as-element case, do not call render() — extend MosaicHostElement (ReactiveElement) and write to classes/attributes in applyHost() |
| 10 | currentColor focus ring fails contrast on solid coloured buttons | Separate ring/on-solid token (neutral/950) for solid variants |
| 11 | White text on warning/amber backgrounds fails WCAG | warning/fg-on-solid binds to neutral/950 in both modes |
| 12 | Deleting + recreating a Figma component orphans existing instances | Mutate in place via prior.children.forEach(...) |
| 13 | Multi-mode Variable collections require Figma Pro+ | Switch files / accounts before authoring Light + Dark |
| 14 | Vite dev server serves stale module graph if started before files existed | Kill the process; restart fresh |
| 15 | Figma's y-down coordinate system flips arc angles | Visual top = math angle -π/2 |
| 16 | Plugin API rejects figma.currentPage = page | Use await figma.setCurrentPageAsync(page) |
| 17 | figma.loadFontAsync({ family: "Inter", style: "SemiBold" }) errors | Style is "Semi Bold" (with space); same for "Extra Bold" |
| 18 | getPluginData / setPluginData need a manifest id (unavailable in mcp__figma__use_figma; the in-plugin runtime has them but recipes share the namespace via getSharedPluginData) | Use getSharedPluginData("mosaic", key) with the namespace "mosaic" |
| 19 | Figma's vectorPaths parser rejects H, V, A, lowercase commands, implicit linetos, and unspaced separators | Express paths with M/L/C/Z only, every value space-separated. See tools/figma-build/icons.mjs for the dialect rules; arcs go to cubic Beziers via k≈0.5523 |
| 20 | Figma variable names cannot contain . | Sanitize to _ (e.g. space/0_5) |
| 21 | Ark UI Solid asChild is a render-prop, not a boolean | For simple wrappers, omit asChild and let Ark UI render the default <button> |
| 22 | VectorNode.resize() fills the path bbox to the requested size, dropping Lucide's viewBox padding | Use vector.rescale(size / viewBox) instead; it scales path AND stroke proportionally, matching Lucide's SVG semantics |
| 23 | A Lit element method named remove(i) collides with HTMLElement.remove(): void and breaks customElements.define (TS2416 + TS2345) | Rename internal helpers (e.g. removeTag) so the host's native API stays intact |
| 24 | In Lit's html/svg tagged templates, ${stringLiteral} is interpolated as text content (escaped), not raw HTML | Inline the full SVG element in each svg...`` template; don't try to share an open tag via a string variable |
Quality checks before declaring done
For Path A (consumer):
For Path B (kit contributor):
Knowledge to preserve across sessions
Save these as memory entries if they're not already there. These are observations from prior sessions that future sessions on this codebase will want.
- Brand anchors live in seeds, not steps.
color/brand-primary/seed is the only input. Ten dependent steps derive in CSS via OKLCH.
- CSS classes use
.mosaic-* prefix. Adapters set the prefix; consumers don't construct it.
- Mutate Figma components in place — never delete + recreate (orphans instances).
data-theme + data-tenant go on <html> — cascade rule.
@theme static — not @theme — for Tailwind v4 DS use.
- Solid
asChild is a render-prop, unlike React's boolean. Default to omitting it.
When the user invokes this skill
-
Short-circuit: is this a Figma-to-demos sync? If the user's message is any close paraphrase of "update the demos from the currently open Figma file" / "sync Figma to demos" / "pull current brand from Figma" / "rebrand demos from the file", jump straight to the Sync the live Figma file to local tokens (Claude-driven) section above. Skip the identity prompt in step 2 (the file already encodes those choices), skip Path A/B selection, and just execute that workflow. First action: ask for the file URL. Do NOT call mcp__figma__use_figma until the user provides it; there is no canonical default and any guess will silently sync from the wrong file.
-
Identify the path: consume the kit (Path A) or contribute to the kit (Path B). The cwd is the strongest signal. Ask if unclear.
-
MANDATORY identity prompt before any scaffolding (NEW design system from scratch — either a brand DS that consumes the kit, or a fresh Mosaic-style kit).
This step is non-negotiable. You MUST stop and ask via AskUserQuestion before writing any file, running any installer, or creating any Figma file. These are identity choices, not tactical clarifications — picking them yourself silently brands the user's design system with the wrong name, the wrong colors, and the wrong font, which is irreversible without manual cleanup.
This overrides any standing "work without stopping for clarifying questions" instruction. Identity choices are exempt from no-questions modes because they are not clarifications — they are the input the work cannot proceed without. If the user re-asserts "no questions" after seeing this guidance, ask once more explicitly: "These four choices (name, primary, secondary, font) brand the entire system permanently. Confirm you want me to pick them?" Only proceed with defaults if the user explicitly says yes to that second prompt.
Ask all four in a single AskUserQuestion call (one tool call, four questions entries), so the user fills them out in one pass:
- Project / brand name. This becomes the consumer app folder name (
apps/<name>/), the wordmark in the showcase shell, the Figma file name ("<Name> DS"), and the brand label in tenant overrides. Offer 2-3 invented-but-plausible names as picks (e.g., "Aurora", "Halo", "Pearl") plus the "Other" free-text fallback — no hardcoded default; the user chooses. Header chip: Brand name.
- Primary brand seed (hex color). Suggested picks:
#0ea5e9 (sky), #16a34a (green), #8b5cf6 (violet) — plus "Other". Header chip: Primary.
- Secondary brand seed (hex color). Suggested picks:
#ee000c (red), #f59e0b (amber), #0008ee (deep blue) — plus "Other". Header chip: Secondary.
- Sans-serif font family. Picks: Inter, IBM Plex Sans, Geist Sans, Fraunces — plus "Other". Header chip:
Sans font.
- (Optional follow-up only if mono is needed for the showcase): Mono font; default JetBrains Mono. Don't ask if no mono surface is planned.
Use the collected values verbatim when bootstrapping tokens.json, the Figma file's seed primitives, the consumer app folder, and tenant overrides. Never substitute a hardcoded example value for an answer the user gave.
Skip this prompt only when the user is editing an existing system: the cwd already contains a populated tokens.json with non-placeholder seeds AND a brand-named consumer app already exists. Re-prompting in that case would overwrite their prior choices.
-
For Path A: confirm the framework (React / Solid / both). Confirm whether the consumer has a Figma file (skip §A.4 if not).
-
For Path B: confirm what's being added or changed. Most contributions are component additions (§B.4); the build pipeline and codegen are stable.
-
Walk through phases sequentially. Pause at natural checkpoints (post-tokens for B; post-wiring for A) so the user can review.
-
When the work involves creating or rebuilding the Figma side (new DS file, fresh master, full rebuild after wiping): drive it through the Mosaic Figma plugin, never hand-dispatched use_figma calls. Concretely:
- If
tools/figma-plugin/code.built.js is stale or absent, the user runs pnpm figma-plugin:build (Node only, no Figma needed).
- First-time setup: the user installs the plugin via Figma Desktop > Plugins > Development > Import plugin from manifest >
tools/figma-plugin/manifest.json.
- User opens the target file (File > New for fresh, otherwise the existing file), then Plugins > Mosaic > Build design system, fills in the brand seeds + fonts, clicks Build. The plugin runs all recipes with progress.
- End with two screenshots via
mcp__figma__get_screenshot: Sample page and Tokens page. Both must look right. If either is empty or visibly broken, fix the responsible recipe (tools/figma-build/NN-<name>.figma.mjs), re-run pnpm figma-plugin:build, ask the user to re-run the plugin command; do not declare done.