2026-06-12 10:55:08 -06:00

132 lines
28 KiB
Markdown

<!--
name: 'Skill: Design sync'
description: Skill for syncing a React design system to claude.ai/design by building, verifying, and uploading real component artifacts
ccVersion: 2.1.174
-->
---
name: design-sync
description: Push a React design system to claude.ai/design. This runs a converter that bundles the real component code (from Storybook or a bare package) and uploads it. Use when the user runs /design-sync or says "sync my design system to Claude Design".
---
# Sync a design system to claude.ai/design
## What this is for
**Claude Design** (claude.ai/design) is Claude's design tool: users prompt a design agent and it builds working UI — screens, flows, prototypes — rendered live in the browser from real React code. Out of the box it designs with generic components. This skill changes that: it converts the user's design-system repo into the format Claude Design consumes and uploads it, so from then on **the design agent builds with the customer's actual components** — every design it produces is on-brand, made of their real parts, and maps 1:1 onto code their engineers can ship.
That framing should drive every judgment call in this skill, because each uploaded artifact is an input to that agent (or to the humans steering it):
| Uploaded artifact | Consumed by | For |
|---|---|---|
| `_ds_bundle.js` + `_vendor/` | the design agent's runtime | every design it produces renders these real compiled components from `window.<globalName>.*` |
| `styles.css`, `fonts/`, `tokens/`, `_ds_bundle.css` | every rendered design | the look — tokens, fonts, and component styles, all reachable from `styles.css`'s `@import` closure (designs receive only that closure) |
| `<Name>.d.ts` (`<Name>Props`) | the design agent | the API contract it codes against |
| `<Name>.prompt.md` | the design agent | its usage reference — how to compose the component, with examples |
| `<Name>.html` preview card | humans in the component picker | how they find components and trust the sync |
| `_ds_sync.json` | future syncs | the sync anchor — content hashes that let a re-sync (any machine) skip re-verifying unchanged components AND compute exactly what to upload/delete |
This is why fidelity is the whole game: a component that renders wrong here renders wrong in **every design the agent ever builds with it**, and a wrong `.d.ts` or misleading `.prompt.md` makes the agent misuse the API everywhere. The verification loops in the sub-skills exist because of this — they are not bureaucracy.
The converter builds all of the above deterministically from the repo's own `dist/`. With a Storybook, previews come from the repo's stories and are verified against its own storybook render (kept as a local reference, never uploaded). Without one, every component still ships fully functional, and rich previews are authored from the repo's own usage examples for the components the user scopes in, graded on an absolute rubric. **Core principle: ship what the customer already built** — the bundle is their compiled `dist/`, never a reimplementation.
You have a `DesignSync` tool that reads and writes the user's claude.ai/design projects.
## 0. First sync? Set expectations before any work
A completed sync always leaves `.design-sync/config.json` holding both a `projectId` and a `pkg`. If both are present, this is a re-sync — skip this section (§2 covers honoring prior state). (If `design-sync.config.json` exists instead — the config's old name and location — move it: `mkdir -p .design-sync && mv -n design-sync.config.json .design-sync/config.json`, commit the move, then apply the same test.) Anything less — no config at all, or a partial one left by a run that never finished — gets first-time treatment: tell the user up front, before doing anything else:
- No completed sync was found — this is a first-time import.
- This skill attempts a **high-fidelity** import of their design system: by default that means iterating on the build and visually verifying the quality of every component preview, which can take **up to a few hours** on a large repo.
- They can interrupt at any time — a message mid-run to check progress or redirect the effort is welcome and won't break anything.
- A first-time import goes into a **new Claude Design project created for it** (§1). Everything that needs their approval happens **near the start** — creating that project, and one approval that covers this run's uploads into it. After that, **verified components appear in the project as the run progresses**: they can open the project at any time and watch it fill in, and nothing waits on their approval at the end.
- The run records config and notes as it goes, so future syncs are faster and mostly deterministic.
(If §1 routes this run into an existing project — the user re-adopting one, or a `projectId` left pinned by an aborted run — parts of this won't apply; scale the expectations to what §1 routes them to.)
Then confirm they want to proceed — this process can use a significant number of tokens (`AskUserQuestion`: proceed with the full high-fidelity sync, or adjust scope first). If their request already acknowledged the time/cost, note that and continue without re-asking.
## 1. Pick the target project
If `DesignSync` isn't already in your tool list, load it via `ToolSearch(query: "select:DesignSync")` first. A target gets picked one of three ways, in precedence order:
- **Pinned**: `.design-sync/config.json` has a `projectId` → that's the target. `DesignSync(get_project)` to confirm it still exists and is `PROJECT_TYPE_DESIGN_SYSTEM`, mention which project you're syncing to, and re-ask only if it's gone or the user redirects.
- **Fresh — the first-time default**: no pin → **create a new project**. A fresh project is the only target whose entire contents this run owns; that ownership is what makes the incremental upload (§3) safe to approve in one shot, and it's why existing projects are never offered here — pouring a first import into a project that already has files would show a half-imported mix to anyone using it, with no sync anchor to tell its files apart from this run's. Use `DesignSync(list_projects)` to pick a NON-colliding name (a duplicate gets rejected and costs a round-trip), confirm the name via `AskUserQuestion`, and only then call `DesignSync(create_project)` — it raises its own permission prompt, and an unconfirmed creation can stall an unattended session. If that prompt is denied, stop and ask the user what to do differently; never retry unasked, never continue without a target. One salvage case: a project evidently left by a prior aborted run of this repo (it has the name this skill would propose — `list_files` it to confirm it's actually empty, since `list_projects` shows no file counts) may be offered for reuse instead of creating another, or noted as safe to delete.
- **Re-adopted — on the user's explicit ask only**: the user names an existing project (by name or UUID; typically re-adopting the project a previous sync uploaded to, after the config was lost). `DesignSync(get_project)`, check `type` is `PROJECT_TYPE_DESIGN_SYSTEM`, then warn them in plain language (no tool jargon) that syncing can overwrite or delete files already in it — e.g. "Heads up: syncing into that existing project means I may replace or remove files it already contains so it ends up matching this repo. If anything in there isn't from this repo, it could be lost — want me to continue, or create a fresh project instead?" — and proceed only on their confirmation. This explicit ask is the ONLY way an unpinned run ends up in a pre-existing project.
**Record the pin at settlement.** The moment the target is settled — created, reused, or re-adopted — **record its `projectId` in `.design-sync/config.json`**, before anything uploads. This is the skill's one recording rule: a death at any later point leaves a pinned config, so the retry repairs the SAME project through the atomic path instead of creating a duplicate and orphaning the original. (The post-upload record step in the sub-skills' atomic sections is just the backstop for this rule.)
**Route the upload path.** A `projectId` pinned **before this run started** always takes the **atomic path** (the sub-skill's upload section) — even when its project turns out empty; a bulk re-upload is fine there, and one rule beats a special case. Otherwise the remote decides, via a prompt-free `DesignSync(list_files)` on the target:
- **Empty** (the normal case — this run just created it) → **incremental path** (§3): one upfront approval, then verified components upload as the run progresses.
- **Non-empty** (a re-adopted project) → **atomic path**: it may be in active use, so it updates in one pass at the end of the run, after everything is verified.
The router decides only the **upload** path. **Verification** scope is the anchor's job: a project with `_ds_sync.json` lets the re-sync driver skip unchanged components; no anchor means everything gets verified, whichever upload path applies.
## 2. Explore, then write config
The workflow is **explore the repo → write `.design-sync/config.json` (§1's pin has already created the directory and the file — read it and add to it, never dropping `projectId`; `mkdir -p .design-sync` stays as a harmless safety net for legacy states) → run the converter deterministically from it**. The converter's discovery is heuristic-based; each heuristic has a config override (after the sub-skill stages the scripts: `grep -r ASSUMPTION .ds-sync/*.mjs .ds-sync/lib/*.mjs` lists them) so repos that don't match the defaults write config, not code. Edit `lib/*.mjs` only as a last resort (see the sub-skill's escape-hatch section: storybook §5, package §Troubleshooting).
**The upload format is the contract; the converter is the deterministic path to it, not the only path.** What the app consumes is fully specified by the output layout: `_ds_bundle.js` + `@ds-bundle` header, `styles.css`, `components/<group>/<Name>/{.html,.jsx,.d.ts,.prompt.md}` with the `@dsCard` first line, `_preview/`, `_vendor/`, `fonts/`, `_ds_sync.json` (see the sub-skill's layout and upload sections).
An off-script layout should also produce `_ds_sync.json` when it can. For the package shape, `lib/sync-hashes.mjs` gives `styleShaFor`/`renderHashFor`/`sourceKeyFor`; the envelope is `{shape, styleSha, renderHashes, sourceKeys, keyRecipe, scriptsSha, sourceHashes, auxSha, bundleSha12}` (see the sidecar block in `package-build.mjs``sourceHashes` itself comes from `stampHeader` in `lib/bundle.mjs`; `sourceKeys` may be omitted, which just means changed artifacts re-verify). The storybook shape's recipe needs story facts an off-script generator may not have; omitting the sidecar is then the honest choice — the next sync simply has no anchor and re-verifies everything, which is correct.
One invariant that's easy to miss when producing the layout by hand: rendered designs receive only `styles.css`'s transitive `@import` closure. Any real component CSS (`_ds_bundle.css`) must be `@import`ed from `styles.css` — a card linking it directly proves nothing about designs.
For a repo genuinely outside the converter's envelope (non-esbuild-bundlable builds, exotic toolchains), produce the layout by whatever means the repo allows. The gates don't move: `package-validate.mjs` must exit clean, and every story must be graded before upload — from true screenshot pairs in the storybook shape, on the absolute rubric in the package shape. Off-script generation is legitimate; off-script *verification* is not.
**State from prior runs.** If `.design-sync/config.json` or `.design-sync/NOTES.md` already exist, Read both first and honor what's there — they hold corrections from earlier syncs. **Whenever the user tells you about an issue mid-run** (a path, a build flag, a component to skip, a package-manager quirk), persist it immediately so the next sync doesn't need telling again: a value that maps to a `cfg.*` field goes into `.design-sync/config.json`; anything else goes as a bullet in `.design-sync/NOTES.md`. Both get committed at the end (the sub-skill says when).
1. **Faithful install with the repo's own package manager.** Use the repo's pinned node version (`.nvmrc` / `engines.node`), then detect via lockfile: `yarn.lock``yarn install --immutable`; `pnpm-lock.yaml``pnpm i --frozen-lockfile`; `bun.lockb`/`bun.lock``bun install --frozen-lockfile`; `package-lock.json``npm ci`.
2. **Determine the source shape.** If `.design-sync/config.json` already exists and has a `"shape"` field, use that. Otherwise `Glob` for `**/.storybook/main.*` and `**/storybook/main.*` (some repos drop the dot; exclude `node_modules`) — monorepo DSes keep it in a subpackage, so never assume it's at repo root:
- Any match → `shape = 'storybook'`. The match's grandparent is the package to run from. Found several → `AskUserQuestion` which one is the design system's; that dir becomes `storybookConfigDir`. **Do not fall back to package just because `.storybook` isn't at repo root.**
- Found `*.stories.*` files but no `.storybook/` dir in the target → `AskUserQuestion`: "Found story files but no `.storybook/` here — is there a Storybook config elsewhere in this repo (e.g. `apps/storybook/.storybook` in a monorepo)?" If they point at one → `shape = 'storybook'`, record that path as `storybookConfigDir`. If they say no → `shape = 'package'`.
- No `.storybook/` and no `*.stories.*``AskUserQuestion` whether a Storybook exists at all. If they point at one, record it as `storybookConfigDir` and `shape = 'storybook'`. If no, `shape = 'package'`.
Then `Read` `<skill-base-dir>/storybook/SKILL.md` or `<skill-base-dir>/non-storybook/SKILL.md` and follow it from there (the storybook one points back into the package one's shared tables where they overlap). Record `"shape"` (and `"storybookConfigDir"` when set) in `.design-sync/config.json` when you write it so re-sync skips detection. Both shapes run `<skill-base-dir>/package-build.mjs` as the converter entry and `<skill-base-dir>/resync.mjs` as the single re-sync driver (build → diff → validate → scoped capture, one verdict JSON); shared adapters live at `<skill-base-dir>/lib/`, and `<skill-base-dir>/storybook/` holds the storybook-only harness (`compare.mjs` — preview-vs-storybook matching; `probe.mjs` — provider inference fallback).
## 3. The incremental upload sequence (first syncs into an empty project)
On the incremental path (§1), the user approves the upload once, early, and then watches verified components appear in their project while the run is still going — instead of waiting hours for one bulk upload at the end. This section is the shared mechanics; the sub-skill says **when** each step fires (its own build and verification gates, marked "incremental path" there). The sub-skill upload section's mechanics apply to every write here too: ≤256 files per `write_files` call and smaller chunks for binary-heavy dirs, upload hygiene, and the what-stays-local list.
### Open the upload channel — at the sub-skill's first-clean-build gate
1. **Explain the approval in plain language first.** Before asking, tell the user what they're about to approve, with no tool jargon (no "plan", "glob", or tool-method names): e.g. *"I'll ask for one approval now that covers uploading everything this run produces into the new project — and cleaning up any files a later rebuild drops. You won't be prompted again; components will appear in the project as they're verified."* The approval dialog shows a structured path list on its own; this message is what makes that dialog make sense to someone who's never synced before.
2. `DesignSync(finalize_plan)` with `localDir: "./ds-bundle"`, `writes: ["components/**", "tokens/**", "fonts/**", "_vendor/**", "_preview/**", "guidelines/**", "_ds_bundle.js", "_ds_bundle.css", "styles.css", "README.md", "_ds_sync.json", "_ds_needs_recompile"]`, and `deletes: ["components/**", "tokens/**", "fonts/**", "_vendor/**", "_preview/**", "guidelines/**"]`. The delete globs are what make the end-of-run reconciliation below prompt-free — and they're consent-trivial here: the project started empty, so anything deletable is something this same run uploaded. The returned `planId` serves the whole run (it lives for the session). Lost mid-run to a context reset → `finalize_plan` again, one fresh approval, before uploading anything more. A whole-session death doesn't resume this path at all: the retry arrives pinned (§1) and correctly goes atomic — expected, not a bug to work around.
3. **If the approval is denied, stop and ask — never continue silently, never re-prompt unasked.** Say in plain language what was denied and what it covered ("the one-time approval for uploading this run's output into the new project"), then offer: try the approval again; target a different project; or finish the build and verification locally with no upload. Local-only → the run proceeds normally except nothing uploads, and the end-of-run report hands over both the `ds-bundle/` path and the project's URL (`https://claude.ai/design/p/<projectId>` — the pin is already recorded, so a later sync finds this project rather than orphaning it). A different project → it goes through §1's re-adoption ask and the router like any other explicit choice, pin included: non-empty → atomic path, this plan abandoned; empty → resume here with a fresh approval.
### Push each verified batch
Nothing uploads until the first batch of components passes the sub-skill's done-bar. **The first push carries the shared base files together with that first batch**: `_ds_bundle.js`, `_ds_bundle.css`, `styles.css`, `README.md`, `_vendor/**`, `tokens/**`, `fonts/**`, `guidelines/**`, plus the batch's `components/<group>/<Name>/` dirs and `_preview/<Name>.*` files. Two reasons they travel together: the first thing the user sees in the project is real components, not an empty shell that claims something was uploaded — and by first-batch time the shared files have earned their place, because grading those components exercised the very same bundle, CSS, and fonts. This first push is the project's first content and its largest, so it takes the full fence: sentinel first (`write_files` `_ds_needs_recompile` — it fences the app's manifest/copy machinery against a half-uploaded state), then the files, then the sentinel re-write (every push on this path ends by re-writing the sentinel — that's what makes the app refresh its view of the project next time it's opened). Output the project URL prominently with this push — `https://claude.ai/design/p/<projectId>` — it's the moment the project first has something to see.
Every later batch that passes the done-bar: `write_files` its `components/<group>/<Name>/` dirs and `_preview/<Name>.*` files, then re-write the sentinel — the new cards appear next time the user opens or refreshes the project. When you report batch progress, include the project URL so the new cards are one click away. If a full rebuild has run since the last push (a global config fix landed), include the shared base files again: the fix rewrote the bundle/CSS/fonts locally, and without re-pushing them every component verified after it renders against stale remote versions until close-out. They're in the approved plan and idempotent, so the re-push costs nothing.
Later batch pushes need no leading fence — they're short and always end re-armed, so the unfenced window is negligible (the first push above and the long close-out below are the ones that fence first). And batches are progressive visibility, not the correctness mechanism: the close-out guarantees the final state, so don't agonize over batch composition — a component pushed early then reworked later simply gets re-pushed.
### Close out — after the sub-skill's final gate
1. **Sentinel first, then full content writes.** Re-write `_ds_needs_recompile` before anything else — the app clears the sentinel whenever the user opens the project (which this path invites mid-run), and the close-out is the longest write+delete stretch, so re-fencing here is what keeps a half-applied state from ever being consumed. Then everything in the plan's writes EXCEPT `_ds_sync.json`, chunked. Re-uploading unchanged files is idempotent and cheap; this pass covers anything the batches missed and anything the final rebuild changed, so the project ends up exactly matching the final verified build no matter how the batches went.
2. **Reconciliation deletes — mandatory, not conditional.** `DesignSync(list_files)` the project and `delete_files` every remote path under `components/`, `_preview/`, `tokens/`, `fonts/`, `_vendor/`, `guidelines/` that the final `ds-bundle/` does not contain (the plan's delete globs cover them — no new prompt). Why this pass exists: a component uploaded by an earlier batch and then dropped, renamed, or regrouped later in the run is invisible to every future re-sync diff — anchor-based diffs only see what the anchor records — so this is the only moment it can ever be cleaned up; skip it and the orphan is permanent. The deletes also retire the orphan's card: the app rebuilds its component index from the currently-uploaded files, so the card disappears once the sentinel is re-armed (next step) and the project is opened.
3. **Sentinel re-arm, then `_ds_sync.json` absolutely last**, in its own `write_files` call — same rule, same reason as the atomic path: the anchor must only ever vouch for a fully-applied state, and it goes after the deletes so a failed delete can't leave remote files the anchor no longer sees. Then output the project URL — `https://claude.ai/design/p/<projectId>` — with the final summary.
A mid-run abort anywhere on this path (user stops the run, session dies) leaves the project **un-anchored** — the documented safe state: the next sync re-verifies everything and re-uploads, nothing silently rots. And as in the sub-skill upload sections, any write/delete failure that retries don't clear means **STOP** — no sentinel re-arm, no `_ds_sync.json`.
## Author the conventions header
You've just spent real effort making this design system's previews render — working out how components must be wrapped, what provider and theme setup they need, what load order matters, and which mistakes silently produce unstyled output. That knowledge evaporates when the sync ends unless you write it down here, for a very specific reader.
**Who reads it.** The file you author is prepended to the generated README (via the `readmeHeader` config key) and inlined into the system prompt of a *design agent* — a model that builds apps WITH this component library, hundreds of times, for users who never see this file. It won't make storybook previews, run this repo's build, or read its source; it gets the README and the bound artifacts, nothing else. An agent in that position follows concrete, enumerated guidance and cannot follow guidance that isn't there: name the tokens and it uses tokens; leave the class vocabulary unnamed and it won't guess at yours — it will invent its own. Say to wrap in the provider and it wraps; don't, and it mostly won't. So every sentence must pass one test: *could the design agent act on this without guessing?* ("Follow the design system's conventions" fails that test; delete it and write the convention.)
**What to write** — four concerns, in whatever structure serves this DS:
- **Wrapping and setup.** If components need a provider/root wrapper to be styled (it's usually where the tokens and theme live), name it, say what breaks without it, and show the wrap in a minimal snippet — plus theme setup, load order, and any gotcha that cost you a preview debugging cycle. Filter by the reader's job: it builds apps, not previews — harness-specific setup (storybook quirks, scaffolding) goes to NOTES.md; what matters for building with the components goes here.
- **The styling idiom, with its actual vocabulary.** Teach THIS system's idiom, never a generic one: utility-class systems get a compact family table with real names from the styling source (a Tailwind preset enumerates them exactly); prop/theme systems get "no CSS classes — style via props" with the props that carry the design language; token systems get the `var(--*)` pattern with real names. Never import an idiom the DS doesn't have.
- **Where the truth lives.** Name the stylesheet/source files the agent should read before styling (the bound copies it will have, e.g. `_ds/<folder>/styles.css` and its imports) and the per-component docs. An agent that reads the real files beats any summary — your job is making sure it knows where to look.
- **One idiomatic build snippet.** A short, real example — a library component for the control, the DS's styling idiom for the agent's own layout glue. Adapt one of your verified previews: it's code you know renders.
Across different kinds of systems that looks like (illustrative, not exhaustive): a Tailwind-preset DS → family table (`bg-surface-1`, `gap-md`, `text-body`…) + root wrapper; a grommet-style DS → no classes, `pad`/`background`/`tone` props + ThemeProvider; a chakra-style DS → theme-token strings (`color="red.500"`); a CSS-modules/BEM DS → the exported class maps and whether new names are ever legitimate; a web-components DS → slots, attributes, and registration order.
**Validate before shipping.** A conventions file that names things which don't exist is worse than none — the agent will trust it, write vocabulary that doesn't resolve, and ship silently unstyled output. Before committing: every class, token, prop, and component you enumerated must exist in the built artifacts — grep classes/tokens against the compiled stylesheets in the output dir; check named components against the `components/<group>/<Name>/` directories in the output dir (the build you just ran emits one per component — that tree is the sync-time name index; `.ds-build-meta.json` carries only counts), then the bundle text (authoritative — e.g. a provider like the root wrapper ships in the bundle without a component folder) before cutting a claim. Verifies in neither → fix the name or cut it; documented in source but absent from the build → that's a NOTES.md finding, not header content.
**Budget.** Be terse — 2-4k characters covers all four concerns, and real names beat vagueness. If the build's size warning fires, read which side it names. Header-side (the header alone exceeds ~31.9k): shorten the header — it survives inline truncation only while it itself fits the ~32k window; past that, its own tail is cut and the body contributes nothing. Body-side: your conventions are safe (prepended, within-window); what's lost is the END of the generated body — typically the component index's tail. Accept that loss deliberately, or reduce the synced surface (package shape: `componentSrcMap` exclusions, a narrower `tokensGlob`; storybook shape: sync fewer stories) — there is no body-section trim knob.
**Where it lives, and reruns.** Write `.design-sync/conventions.md`, set `"readmeHeader": ".design-sync/conventions.md"`, commit both — it's deliberately human-editable. Then rebuild so the README actually carries the header — it's stitched at build time. **The rebuild rule:** the post-authoring rebuild is a fresh DRIVER run on every path — first syncs omit `--remote` — because the closing receipt and the upload plan must both describe the header-bearing build; a bare converter run wipes `.sync-diff.json` and the receipt artifacts, leaving the uploaded build unreceipted. (Every other mention of the post-authoring rebuild defers to this rule.) Whenever the file already exists — regardless of how this run was classified (re-sync, re-adoption after a lost config, recovery from a partial one): never rewrite it — re-run the validation pass against the fresh build and report any name that no longer verifies (NOTES.md + user), proposing edits. Authoring happens only when no `.design-sync/conventions.md` exists. Content belongs to its authors; your standing job is keeping it true.