agentsclimarketplace

Token sync layer

Skill jrpease/throughline/skills/token-sync-layer

Sync Figma design variables into code-ready token files via DTCG JSON and Style Dictionary, emitting framework-specific outputs (shadcn/Tailwind, MUI, vanilla CSS, iOS Swift, Android Kotlin, or custom), and set up the reusable "sync figma tokens" command. Use this when the user wants to turn Figma tokens into code, sync design tokens, generate tokens.css or a Tailwind config or tokens.swift, set up a token pipeline, or run "sync figma tokens". Also trigger when the user mentions Style Dictionary, design token export, or keeping code in sync with Figma.From its SKILL.md

Install
npx -y skills add jrpease/throughline --skill token-sync-layer

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

16.7 KB, as published. Nobody here has run it

Token sync layer

Compiles Figma variables into code. The pipeline is one-directional — Figma is the source of truth for token values; code consumes generated artifacts that are never hand-edited:

Figma variables → DTCG JSON → Style Dictionary → adapter output (per platform)

This skill both performs the sync and installs the reusable /sync-figma-tokens command so the user can re-run it anytime tokens change.

Calibrate

Read user.codingLevel (${CLAUDE_PLUGIN_ROOT}/references/coding-level.md) and scale explanation accordingly. The pipeline involves several developer concepts (JSON, build tools, PRs) — for new users, explain each plainly the first time; for comfortable users, be terse. Actions are identical across levels.

Prerequisites (offer to run them, don't bail)

Read the manifest. This skill needs two things:

  1. Tokens in Figma — tokens.semanticBuilt should be true. If no tokens exist, offer to run token-builder first.
  2. At least a local git repo — workspace.stage must be local-git or github (so the sync can land changes as a reviewable diff/PR). If it's still folder, this is the soft-nudge seam: offer to run repository-builder to add version history first. Explain plainly why ("the sync writes code files, and git lets you review exactly what changed before keeping it"). If the user is at local-git but not github, strongly recommend connecting a remote (GitHub or similar) so the sync can land as a real reviewable PR — the cleanest review model — but don't force it; a reviewed branch diff works at local-git.

A live Figma connection is also required — do a cheap liveness read; if it fails, offer figma-environment-setup.

Step 1 — Choose target platform(s)

Ask which platform(s) the user is building for. Read ${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md for the two-tier adapter model.

  • Curated (Tier 1): shadcn, tailwind, mui, vanilla-css, ios-swift. Vetted presets — high confidence.
  • Generated (Tier 2): any other framework (Ant Design, Chakra, HeroUI, Android/Kotlin, Flutter, etc.). The skill generates an adapter and verifies it against a real component before trusting it.

Always tell the user which tier they're on. If they name a curated one, say it'll be solid. If they name anything else, be honest: "That's not one I have a vetted preset for — I'll generate an adapter from its conventions and we'll check it against a real component before relying on it." Follow the Tier 2 protocol in the reference, and offer to save a validated generated adapter to packages/tokens/adapters/ (record it in sync.customAdapters) so future syncs reuse it.

A user can target several platforms at once. Record the choices in sync.platforms. The adapter is what absorbs all framework-specific shaping — which is why the Figma variables stayed framework-neutral.

Also set project.uiFramework if it's still null (this or component-builder sets it at first relevance, whichever runs first; the other reads it). If component-builder already set it, the chosen adapter(s) should be consistent with it.

Step 2 — Extract Figma variables to DTCG JSON

Using the active write mechanism (figma.mechanism, default Console MCP — prefer its full-design-system extraction, which works on any Figma plan), read every variable collection (there are now several per tier, e.g. _Color/Primitive, Spacing/Primitive, Color/Semantic, …) and normalize them into DTCG-format JSON ($value, $type, with semantic tokens expressed as {group.token} references to primitives, not flattened literals).

Three rules for the multi-collection structure:

  • Iterate N collections, not a fixed two. Don't assume one Primitives + one Semantic.
  • Single-mode collections are non-themed — a collection with one mode (e.g. Spacing/Semantic) emits flat values, never a phantom default theme alongside :root/.dark.
  • Primitives can be multi-mode — _Color/Primitive carries a Brand axis, so emit brand themes from the primitive tier, not just light/dark from semantics. Preserve modes (light/dark/brand/device) as the DTCG structure the adapters expect.
  • Name mapping: collapse the tier/category prefix into clean token names — _Color/Primitive + gray/500 → color.gray.500 (not color.primitive.gray.500); Color/Semantic + text/primary → color.text.primary. The category drives the top-level group; the tier (and the _) is dropped from the emitted name.
  • Opacity: normalize 0–100 → 0–1 on extraction. Opacity tokens are stored in Figma on the 0–100 scale (a quirk of how Figma binds variables to a node's opacity field — see the opacity scale rule in token-builder). CSS opacity, Tailwind opacity, and native alpha are all 0–1, so when normalizing any opacity token (the Opacity/* collections, or any FLOAT bound to opacity) divide its value by 100 before writing DTCG (40 → 0.4). Do this once at extraction so every adapter emits a correct 0–1 value; an un-normalized 40 lands as opacity: 40 in CSS and renders the element fully opaque. token-builder and token-sync-layer are a matched pair on this — neither is correct alone.

This DTCG JSON is the canonical intermediate. Write it to a known location in packages/tokens/ (e.g. packages/tokens/dtcg/tokens.json). Do not trust the MCP's built-in CSS/Tailwind exporters for final output — route through Style Dictionary so the adapter system governs the result.

Brownfield transforms (learned the hard way)

On a retrofit (tokens.intakeMode: "retrofit"), apply these transforms in addition to the opacity normalization above. Each cost real debugging time on a live retrofit — see the guardrails in ${CLAUDE_PLUGIN_ROOT}/references/brownfield-retrofit.md.

  • Alpha as channels, not baked CSS vars (so Tailwind /opacity survives). Emit colors as space-separated channels with a slash-alpha slot — --color-x: 239 68 68; consumed as rgb(var(--color-x) / <alpha-value>) — rather than a finished rgba(...). A baked rgba() can't accept Tailwind's /opacity modifier; the channel form keeps bg-x/50 working after the retrofit.
  • /opacity on var-based tokens → color-mix or channel alpha (guardrail 6). Where existing code applies a /opacity modifier to a token that is now a CSS var, you can't fold the alpha into the var. Convert to color-mix(in srgb, var(--color-x) NN%, transparent) (or the channel-alpha form above). Never carry a raw /opacity onto a var-based token.
  • Round float32 noise at the export boundary (guardrail 2). Figma stores values as float32 and re-quantizes on write, so normalizing inside Figma is a no-op. Round at the export boundary instead — Math.round(v * 100) / 100 as values leave the pipeline — so 0.30000001192092896 lands as 0.3 in the generated files. Do this in the extraction/transform step, never by hand-editing the generated output (guardrail 7).

These are transforms on the values the adapters emit; the adapter presets themselves are unchanged. See ${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md for where they fit in the adapter output.

Step 3 — Set up Style Dictionary + adapters

Install and configure Style Dictionary v4 in packages/tokens/. For each chosen platform, apply its adapter preset (per ${CLAUDE_PLUGIN_ROOT}/references/sync-adapters.md): register the platform, transform group, format, and outputReferences (true for web → preserves the semantic→primitive cascade; false for native → flattens). Web adapters emit :root/.dark (or [data-theme]); the shadcn adapter also emits a Tailwind preset.

Native targets import the shipped configuration; they do not transcribe it. Copy ${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs into packages/tokens/scripts/lib/ (see Step 4) and call it. The stock ios-swift and compose transform groups emit every px-authored dimension at ×16 its value — valid, compiling, silently wrong — and mishandle color-mix() and dual-node DTCG the same way. Never build a native platform from a stock transformGroup.

import StyleDictionary from 'style-dictionary';
import { registerNativeTransforms, nativePlatform, nativeSources }
  from './scripts/lib/sd-native.mjs';

registerNativeTransforms(StyleDictionary);

for (const mode of MODES) {                     // e.g. ['light', 'dark']
  const sd = new StyleDictionary({
    source: nativeSources(sourcesFor(mode)),    // guards against a mode collapse
    preprocessors: ['dtcg/resolve-dual-node'],
    platforms: {
      ios: nativePlatform({ platform: 'ios-swift', buildPath: `ios/${mode}/` }),
      // android also requires packageName:
      // android: nativePlatform({ platform: 'android-kotlin',
      //   buildPath: `android/${mode}/`, packageName: 'com.example.tokens' }),
    },
  });
  await sd.buildAllPlatforms();
}

One build per mode combination, and never a glob. Style Dictionary dedupes by dot-path, so a single build over the whole token directory collapses light and dark into whichever file sorted last, silently dropping a mode. Passing each mode's sources through nativeSources turns that into a thrown error naming the colliding paths. The dtcg/resolve-dual-node preprocessor throws too, on its own collision: a dual node's hoisted child renamed to a camel-joined name that an existing sibling or an earlier hoist in the same pass already has. Then run tokens:validate-output against each generated file with that same source list. See ${CLAUDE_PLUGIN_ROOT}/references/native-adapter-config.md.

Execution model — subagent dispatch with model routing. Generating each platform's output is independent and verifiable. If your host supports subagent dispatch, dispatch one code-executor per adapter — each produces its platform's files and verifies them (for web: the config builds, the expected files appear, references resolve; for native: tokens:validate-output passes — "the config builds" is not verification, it is the condition under which all four known native failure modes ship silently) — then a reviewer to check each before combining. Choose each subagent's model from its role tier per ${CLAUDE_PLUGIN_ROOT}/references/agent-routing.md (code-executor → fast, reviewer → balanced), and only dispatch once each adapter's spec is complete enough to transcribe. If your host has no subagent dispatch, generate and verify each adapter inline instead. For a single platform, run inline either way. This is a code-gen stage, so these subagents may run in parallel — unlike Figma authoring, which is always sequential.

Step 4 — Build and place outputs

Run the Style Dictionary build. Outputs land in packages/tokens/<platform>/ as build artifacts — regenerated every sync, never hand-edited. Wire packages/tokens/package.json to export them so the UI package, Storybook, and any future app consume them.

Install the native token toolkit — all four files, as a set. Copy ${CLAUDE_PLUGIN_ROOT}/scripts/validate-token-output.mjs into packages/tokens/scripts/, and ${CLAUDE_PLUGIN_ROOT}/scripts/lib/dtcg.mjs, ${CLAUDE_PLUGIN_ROOT}/scripts/lib/native-literal.mjs, and ${CLAUDE_PLUGIN_ROOT}/scripts/lib/sd-native.mjs into packages/tokens/scripts/lib/. sd-native.mjs and the validator both import dtcg.mjs and native-literal.mjs, so copying any of them without the others breaks at import. Then register the gate so it stays live on every future sync:

"tokens:validate-output": "node scripts/validate-token-output.mjs --min-match 1"

Invoke it once per native output file, passing the same --source list that file's build used. --min-match 1 is what makes it a gate: the flag defaults to 0.5, so without it a 60% match rate exits 0.

Step 4.5 — Icon code sync (install check + custom SVGR)

Icons reach code differently from tokens, so handle them here if the system has icons (icons.built true):

  • Library icons (Lucide/Material) — verify the npm package is installed (icons.packageInstalled); install it if not. Then check version drift: compare the installed package version against icons.version (the version the Figma mirror was built from). If they differ, an icon could exist on one side and not the other — flag it and offer to align them (bump the package, or note the Figma mirror should be refreshed). Never generate library icon component code — the package is the code.
  • Custom icons — these the repo owns, so generate them: export the custom SVGs from Figma, optimize, and componentize via SVGR into packages/ui (or a dedicated icons package). This is real code generation and rides the same code-executor (fast) + reviewer (balanced) routing and PR-review as token output (SVGR transforms are the textbook mechanical op the fast tier is for) — see the Step 4 execution model.

Step 5 — Full regeneration + rename detection (the safety net)

The sync is a full regeneration every run: outputs are rebuilt from current Figma state, so a token deleted in Figma disappears from output (no orphans). But a naive full regen can't tell a rename from a delete-plus-add — and a rename silently breaks every consumer of the old name.

So before writing, diff against the previous output and run a rename heuristic: if a token vanished and a new one appeared with an identical $value and $type, flag it as a probable rename (e.g. color.bg.default → color.surface.default?). Surface these prominently. Also surface plain deletions (consumers will break — that's intentional, but the human should see it).

Step 6 — Land it as a reviewable PR (never silent)

Never overwrite token files silently. The output of a sync is a pull request (or, if the repo is only local-git, a clearly-described diff/commit on a branch the user reviews). The PR body is the human safety net — it should summarize: tokens added, changed, deleted, and probable renames flagged for review so the reviewer can do a find-and-replace instead of merging a break.

For new users, explain what a PR is plainly ("a proposed change you review and approve before it becomes official — like track-changes for code") and walk the review. For comfortable users, just open it.

Chromatic note. This PR is token-only — it changes packages/tokens but no story files. Token changes are global, so Chromatic should re-snapshot every story for a sync PR. Don't rely on TurboSnap (onlyChanged: true) here — its incremental model keeps missing global token changes; default to full snapshots. If a sync PR reports "0 snapshots captured," TurboSnap is the cause — see the TurboSnap section in storybook-chromatic-builder.

Update the manifest: sync.lastRun (timestamp), tokens.lastSync, confirm sync.platforms. Append token-sync-layer to completedSkills.

Step 7 — Install the /sync-figma-tokens command

Set up the reusable command (the plugin ships it in commands/) so the user can re-run this whole pipeline anytime tokens change in Figma. Explain that the workflow going forward is: tweak tokens in Figma → run /sync-figma-tokens → review the PR → merge. That loop is the entire point of the one-directional source-of-truth model. Confirm the command works by describing how to invoke it.

What this skill must NOT do

  • Never hand-edit generated token files or let the user treat them as editable — they're build artifacts; changes go in Figma and re-sync.
  • Never flatten semantic→primitive references for web adapters (kills theming).
  • Never write token files silently — always a reviewable PR/diff.
  • Never merge or push to a protected branch on the user's behalf without review.
  • Never skip rename detection — a silent rename is the worst failure mode.
  • Never bake alpha into finished rgba(...) on a retrofit — emit channels (rgb(var(--x) / <alpha-value>)) so Tailwind /opacity modifiers survive.
  • Never carry a /opacity modifier onto a var-based token — convert to color-mix or channel alpha (guardrail 6).
  • Never normalize float32 inside Figma (it re-quantizes) — round at the export boundary (Math.round(v*100)/100, guardrail 2).

Keep looking

Skills are one crate of 627,801. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.