--- 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 You have a `DesignSync` tool that reads and writes the user's claude.ai/design projects. This skill turns a React design-system repo into the format claude.ai/design consumes, then uploads it. **The goal — what a design-system project looks like on claude.ai/design:** - One `_ds_bundle.js` at the project root that assigns every component to `window..*`, so the design agent can build with the real code. - One `styles.css` that `@import`s the tokens, component CSS, and fonts. - Per component, `components///`: a `.d.ts` whose `Props` interface is the component's API contract, a `.prompt.md` with usage examples, and a `.html` preview card. The converter builds all of that deterministically from the repo's own `dist/`. Storybook is the happy path (richest previews); any built npm package also works. **Core principle: ship what the customer already built** — the bundle is their compiled `dist/`, not a reimplementation. ## 1. Pick the target project If `DesignSync` isn't already in your tool list, load it via `ToolSearch(query: "select:DesignSync")` first. Then call `DesignSync(list_projects)`. One or several results → `AskUserQuestion` listing each, plus a final "Create a new project called ''" option (name from the package/design-system); if they pick it, `DesignSync(create_project)`. None → offer `create_project` directly. If the user gave a UUID, `DesignSync(get_project)` and check `type` is `PROJECT_TYPE_DESIGN_SYSTEM`. ## 2. Explore, then write config The workflow is **explore the repo → write `design-sync.config.json` → run the converter deterministically from it**. The converter's discovery is heuristic-based; each heuristic has a config override (`grep ASSUMPTION lib/*.mjs` lists them) so repos that don't match the defaults write config, not code. Edit `lib/*.mjs` only as a last resort (§Troubleshooting). 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. **Storybook?** Search for `.storybook/` and `*.stories.*`. Found one → `npm run build-storybook` and proceed as the `storybook` shape (the converter reads `index.json` for the component list and story args). Found several → `AskUserQuestion` which one is the design system's. Not found → `AskUserQuestion` whether one exists; if they point at it, pass `--storybook-config `; if they say no, fall through. 3. **Package shape — get `dist/` built.** The converter needs the built `dist/` entry + its `.d.ts` tree. Check whether the entry (from `package.json` `module`/`main`/`exports['.']`) already exists — install may have built it via `prepare`. If missing: - Run ` run build`. No `build` script → try `prepare`/`prepack`. In a monorepo, the build may be at the repo root (`turbo build --filter=`, `pnpm -F build`, `nx build `). **Some build scripts fork a watcher and exit 0 early — after the command returns, `ls` the expected output (dist/, build/esm/, or whatever `package.json` `module`/`main` points at) and confirm it's populated before continuing.** If it's empty, check for a `--watch` flag in the script and use the one-shot variant, or poll the output dir. - Still missing → `AskUserQuestion`("What command builds this package?", options = any `scripts.*` containing `tsc|tsup|rollup|vite build|esbuild|swc`, plus freeform). Record the answer as `buildCmd` in the config. - User says there's no build → the converter will synthesize an entry from `src/` (last resort — `.d.ts` contracts will be weaker; recommend adding a build). 4. **Check what's already in the project.** `DesignSync(list_files)` on the target. If it returns files, read `_ds_bundle.js` via `DesignSync(get_file)` and note the component names from its first-line `/* @ds-bundle: {…} */` header — but **always still rebuild** (step 7); the existing bundle is stale the moment source changes. The header's `sourceHashes` diff decides what to *upload* incrementally via `DesignSync`, not what to build. 5. **Confirm the plan with the user before building.** `AskUserQuestion` with: the component list you found (or a count + a few names if it's long), which files the tokens/CSS are coming from, and which build command you'll run. The build can take minutes and burn tokens — aligning now avoids re-running because it was pointed at the wrong package or missed half the components. - If the project already has N components (step 4), include that in the question and offer the scope: **(a)** full rebuild + re-upload everything, **(b)** update only the changed components (diff from `sourceHashes`), **(c)** tokens + CSS only (no component rebuild). Default to (b) when the diff is small. 6. **Write `design-sync.config.json` and commit it** — re-sync reuses it so output is reproducible. Only `pkg` and `globalName` are required. **If the file already exists, read it first and preserve `previewArgs`, `dtsPropsFor`, `libOverrides`, `overrides`, and `notes` — only add to those fields, never replace them.** They accumulate fixes from prior verify-loop iterations. | Field | Value | |---|---| | `pkg` / `globalName` | package name and the `window.*` global to assign — required | | `buildCmd` | the discovered build command; re-sync re-runs it | | `srcDir` | source root when not `src/`/`lib/`/`components/` | | `tsconfig` | path to `tsconfig.json` — esbuild reads `compilerOptions.paths` so `@/…` path aliases resolve in synth-entry mode | | `extraEntries` | package names to merge into `window.` alongside the DS entry (e.g. the DS's separate icon package). Sibling icon packages under the same scope are auto-detected (`[ICON_PKG]`). | | `componentSrcMap` | **sparse** `{Name: path}` — non-null pins/adds a component's src path; `null` excludes a `.d.ts`-exported internal | | `dtsPropsFor` | `{Name: "prop?: Type; …"}` — hand-written `Props` body when auto-extraction fails (complex generics, cross-package types) | | `previewArgs` | `{Name: {prop: value, …}}` — props rendered as a `Preview` export in the auto-generated `.design-sync/previews/.tsx`. Use for simple flat props; for composed JSX children edit the `.tsx` directly. | | `storiesPattern` | regex (string) matched against absolute src paths when sibling-stories default doesn't fit (e.g. `"/__stories__/.*\\.stories\\.tsx$"`) | | `cssEntry` / `tokensPkg` / `tokensGlob` | stylesheet + token files | | `docsDir` | directory (package-relative; may point outside, e.g. `../../apps/docs`) holding per-component `.md`/`.mdx` docs. Auto-detected as `docs/` or `documentation/` under the package. | | `docsMap` | sparse `{Name: path \| null}` — explicit doc path per component (overrides discovery); `null` excludes | | `guidelinesGlob` | string or string[] (package-relative) of design-guideline `.md` files to copy into `guidelines/`. Default `['docs/guides/**/*.md', 'docs/*.md', 'guides/**/*.md']`. | | `extraFonts` | paths (package-relative; may point outside the package, e.g. a sibling typography package) to `@font-face` `.css` files or bare `.woff2`/`.ttf`/`.otf` for brand families the DS expects its host app to provide. CSS entries are parsed and their local font files copied to `fonts/`; bare font files are copied as-is. Use when validate prints `[FONT_MISSING]`. | | `runtimeFontPrefixes` | string[] — family-name prefixes for fonts the host app serves at runtime from a font service (via a `