Storybook source shape

SkillAI & models

Open-source desktop app for content creation, with an agent runtime and standalone CLI.

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the Storybook source shape skill

What this skill tells your AI

The instructions your AI receives, as published by alecs5am/ralphy in apps/desktop/.ds-sync/storybook/SKILL.md and read by ahel’s review.

Storybook is the fidelity oracle, not the runtime. The converter bundles the package's compiled dist/ into _ds_bundle.js — the same bundle the claude.ai/design agent builds with — and generates each preview by compiling the story source module itself (hooks, fixtures, local helpers — the whole closure comes along), with every component import resolved to that shipped bundle (lib/story-imports.mjs redirects package and relative component imports to window.<Global>). The repo's own storybook render is the ground truth those previews must match: a compare harness screenshots each story in the reference storybook and the matching preview render side by side, and you iterate until they match. Nothing from storybook-static is uploaded, and no story code is ever evaluated at build time — stories run only in the browser, against the real artifact.

Requires React 18+. Playwright + chromium are required for this shape (the compare loop is the verification), not optional.

First sync or re-sync? A re-sync is marked by a config whose projectId and pkg were both in place before this run started — most of this document then doesn't apply; go to §7, where one driver run routes the work and untouched components cost nothing. Everything else takes the full flow (§2 build → §3 self-heal → §4 match → conventions header (base SKILL.md, before upload) → §6 upload), where every component gets verified and graded once — that includes a partial config left by an aborted run, and a pin this run itself just recorded in the base skill's §1. (Only the old design-sync.config.json present? Move it first and commit: mkdir -p .design-sync && mv -n design-sync.config.json .design-sync/config.json, then apply the same test.)

2. Build, then run the converter

  1. Build the DS package and its workspace dependencies. The converter bundles dist/ into window.<Global>. Run <pm> run build; in a monorepo use turbo run build --filter=<pkg> or pnpm -F "<pkg>..." build (the trailing ... is required — bare -F <pkg> skips dependencies and you'll see Cannot find module '@scope/tokens'). If package.json module/exports['.'] points at TS source, find the actual built entry and pass it via --entry. Do this before step 2 — storybook often imports sibling packages from their built dist/.

  2. Build the reference storybook ONCE into .design-sync/sb-reference/ — NOT under ds-bundle/ (the converter wipes --out on every rebuild, and storybook builds take minutes; the reference must survive the fix loop):

    npx storybook build -c <storybookConfigDir> -o .design-sync/sb-reference
    

    Run it from the directory whose package.json has the storybook devDependencies — usually the one containing .storybook/; monorepos often have several storybooks, so pick the one covering the package you're syncing. Make -o the repo-root path (e.g. -o "$(git rev-parse --show-toplevel)/.design-sync/sb-reference"): the converter and compare resolve .design-sync/ from the repo root, so a cwd-relative -o in a subpackage puts the reference where nothing will find it. Use npx storybook build directly, not the repo's npm run build-storybook script (wrong output dir). Then check .design-sync/sb-reference/iframe.html exists and is >10KB — index.json alone can exist with a failed build.

    Long builds: background them through your shell tool's background mode only and wait for the completion notification. Never a bare & (untracked — the notification never comes), and never a pgrep -f '<script>' poll loop (it matches its own command line and spins to timeout). Headless / -p sessions: run long commands synchronously instead — there is no task-notification re-invocation there, so a backgrounded run is never resumed.

    .gitignore additions: .design-sync/sb-reference/, .design-sync/learnings/, .design-sync/.cache/, .design-sync/node_modules (fork symlink — recreated per clone), .ds-sync/, ds-bundle/ — build artifact, transient scratch, verification working state, the symlink, staged scripts, regenerated output. Committed: the durable set (the rule in non-storybook §2, same here: everything under .design-sync/ not gitignored — previews/ holds your authored files ONLY; generated story-module wrappers live in .design-sync/.cache/previews/ and regenerate every build; the converter never writes or deletes anything in previews/). Verification state is never committed — cross-machine carry-forward comes from the uploaded project's _ds_sync.json. Rebuild the reference only when stories or the DS source change.

  3. Write .design-sync/config.json — only pkg and globalName required. If it already exists, read it first and keep what's theretitleMap, overrides, and provider accumulate fixes from prior syncs. Also Read .design-sync/NOTES.md first — its Re-sync risks section is the prior run's watch-list; re-verify those items instead of assuming carry-forward covers them. The package-shape field table in ../non-storybook/SKILL.md §2.6 applies verbatim; the fields that matter most here:

    FieldValue
    pkg / globalNamepkg required; globalName auto-derived from it when omitted
    shape"storybook" — pins detection
    storybookStatic".design-sync/sb-reference" — so re-syncs and compare find the reference without flags
    storybookConfigDirthe .storybook/ dir (monorepos)
    buildCmdwhat to re-run before the converter on re-sync
    titleMap{title: ExportName} when story titles don't match export names; {title: null} excludes a non-visual/internal component from the sync entirely
    overrides{<Name>: {skip: [storyIds], cardMode: "single"|"column", primaryStory: "<Export>", viewport: "WxH"}}skip for stories that can't render statically; cardMode: "single" for overlay components (§4a.5, §5), "column" for stories wider than a grid cell (the [GRID_OVERFLOW] row in §3)
    providerusually unnecessary for previews.storybook/preview decorators are auto-bundled; set only when that fails. Before §6 upload, distill decorator-provided context into cfg.provider — README/prompt.md wrap guidance is generated from config only (decorator-only wrapping ships a generic note). Setting it also replaces the decorators as the preview wrapper on the next build: scoped-compare a themed component after the switch — an incomplete distillation regresses previews the decorators rendered fine, and carried-forward grades won't catch it. Format: {"component": "ThemeProvider", "props": {…}, "inner": {…}} — a nested chain, outermost first; each component must be a bundle export. Literal props are for small scalars ("theme": "light") and stable snippets. For data that already exists in the repo — a locale JSON, a theme object — prefer {"$ref": "<export>"} backed by a 2-line module added via cfg.extraEntries (e.g. export { default as previewI18n } from '../locales/en.json'): a $ref emits window.<Global>.<export>, so the data lives once in the bundle and re-reads from its source file on every build. Inlining a copy is acceptable for something tiny and stable, but know the cost — a literal duplicates into every card's html and silently rots when the source file changes, so anything sizable or evolving belongs behind a $ref. Path forms for extraEntries: a bare name resolves from node_modules; a repo-owned module needs an explicit .//../ package-relative path (workspace-bounded — the build logs ! extraEntries: … skipped if it escapes).
  4. Stage scripts + install converter deps (isolated in .ds-sync/, repo lockfile untouched):

    mkdir -p .ds-sync && cp -r "<skill-base-dir>"/package-build.mjs "<skill-base-dir>"/package-validate.mjs "<skill-base-dir>"/resync.mjs "<skill-base-dir>"/lib "<skill-base-dir>"/storybook "<skill-base-dir>"/non-storybook .ds-sync/
    echo '{"name":"ds-sync-deps","private":true}' > .ds-sync/package.json
    (cd .ds-sync && npm i esbuild ts-morph @types/react playwright && npx playwright install chromium)
    

    If chromium install fails, npx playwright install-deps chromium first; if the environment can't install chromium, set DS_CHROMIUM_PATH=<system-chromium>.

  5. Run the converter, validator, and compare — synchronously, stopping at the first non-zero exit (compare only runs once build + validate are clean — §3). Large DSes (≈100+ components) may need NODE_OPTIONS=--max-old-space-size=<MB> for the build; never pipe the build through head/tail (the pipeline masks the exit code — an OOM looks like success); redirect to a file and read it:

    node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules <pkg-node-modules> \
      --entry <built-dist-entry> --out ./ds-bundle
    node .ds-sync/package-validate.mjs ./ds-bundle
    node .ds-sync/storybook/compare.mjs --out ./ds-bundle --storybook-static .design-sync/sb-reference \
      --components <solo-phase picks>   # scope the FIRST compare to the §4b solo components
    

    In a monorepo, --node-modules is the DS package's own node_modules — unless hoisting leaves it sparse (yarn's node-modules linker keeps react only at the repo root): if react/ or react-dom/ is missing inside, pass the repo-root node_modules instead. In the DS's own source repo node_modules/<pkg> doesn't exist, hence --entry. The build logs [ICON_PKG] / [TOKENS_PKG] auto-detections and bundles .storybook/preview decorators as the preview wrapper (preview-decorators.js) so previews get the same provider chain stories do.

    Scope the first compare run: a full capture of a large DS is thousands of chromium navigations — pointless before the solo phase has flushed global issues (each global fix invalidates every capture). The first roster-wide run happens per §4b step 3 — and on a DS over 20 storied components even that is size-gated into §4c's scoped batches, so the only mandatory full-roster run is the §4d receipt, which carries graded work forward instead of recapturing it. For a DS with >100 storied components, also tell the user the expected scale (components × stories) before fan-out and let them narrow scope if they want.

3. Self-heal loop (build + validate)

Fix [TAG] errors → rebuild → re-validate until both exit 0, before starting the compare loop in §4 — there's no point pixel-matching previews while the bundle itself is broken. Shared converter tags ([NO_DIST], [WORKSPACE_SIBLING], [CSS_*], [FONT_*], [TOKENS_MISSING], [DTS_*], [RENDER*], …) behave identically to the package shape — use the table in ../non-storybook/SKILL.md §3. Lines printed as hypothesis: under an error are leads, not instructions: run their verify step first, and if it doesn't confirm, drop the hypothesis and diagnose from the error text itself. Storybook-specific:

TagSymptomFix
[SB_REFERENCE_MISSING]compare can't find iframe.htmlBuild the reference (§2.2); set cfg.storybookStatic.
[SB_BUILD_FAIL]converter's own storybook build failedYou skipped §2.2 — build the reference yourself and set cfg.storybookStatic so the converter never needs to.
[ZERO_MATCH] (storybook flavor)no story entries matchedCheck the storybook config's stories glob; then titleMap.
[TITLE_UNMAPPED]N titles don't match an exportcfg.titleMap {<title-name>: <export-name>}.
(preview: <Name> — no story exports paired …)index story names couldn't be matched to module export keys (pairing tries the display name, then the story ID's tail)the component shows the floor card; fix the pairing — usually an owned .tsx re-exporting the stories under matchable names.
a preview cell errors with undefined-component / wrong-context messagesa story import resolved the wrong way — relative, tsconfig-alias, and bare-workspace imports all go through the same policy (see lib/story-imports.mjs's rules)cfg.storyImports.shim / cfg.storyImports.bundle substring patterns force the resolution per resolved path — the cheap fix before forking the seam.
! preview build failed: <Name>the story module didn't COMPILE (top-level await, an import of a package esbuild can't resolve, an asset extension with no loader)read the esbuild error above the line. Unknown asset extension → cfg.storyImports.loaders (merged over the defaults, e.g. {".yaml": "text"}); unresolvable import → own the .tsx and drop it. The component shows the floor card until fixed.
a story's own stylesheet is missing from its cellstory-local .css/.scss side-effect imports compile as empty (component styles ship via the bundle css). Exception: .module.css IS compiled — classes resolve and _preview/<Name>.css is linked automaticallyusually nothing — the styles are decoration the storybook page adds. If the story genuinely depends on them, inline the styles in an owned .tsx.
[BUNDLE_EXPORT]components aren't functions on window.<Global>extraEntries for subpath/icon exports; check the dist entry is the full build.
[SCHEDULER_MISSING]dist imports schedulerreact-dom leaked into the DS dist — check its build's externals.
! preview decorator bundle faileddecorators couldn't be bundledSet cfg.provider manually, or run node .ds-sync/storybook/probe.mjs --storybook-static .design-sync/sb-reference to infer the chain from the live storybook (replace each $hint with a real value).
previews error at _vendor/preview-decorators.js load (storybook-API undefined errors)the .storybook/preview import graph reached a storybook-runtime module the stubs don't covermanager-api/preview-api are stubbed with functional no-op hooks and every other @storybook/*/msw module with inert callables (fn(), action(), setupWorker() at module scope all evaluate harmlessly); if some other API still crashes, set cfg.provider explicitly — it skips decorator bundling entirely.
[ASSETS_BLOCKED] from comparethe capture browser inherited a network-sandboxed shell — story assets (CDN images/fonts) failed on both panels, so grades can falsely pass while end users see different outputre-run package-validate.mjs + compare.mjs --force from a shell with egress to the listed hosts: approve running the command without the sandbox when prompted, or add the hosts to the sandbox allowlist. Don't grade image-bearing components while this prints.

Incremental path (base SKILL.md §3) — this is the open-the-channel gate. The first time build + validate both exit 0, open the upload channel before starting §4: the user approves once here, then watches components land as grading proceeds. Nothing uploads until the first graded batch — the shared base files ride with it — and the batch pushes come from §4b/§4c. (Atomic path: nothing uploads until §6.)

4. Match previews to storybook

compare.mjs is a capture harness — it photographs, you grade. It computes no similarity heuristics (pixel/text/font scores mislead whenever framing legitimately differs); the judgment is made from the two true screenshots. Compiled previews capture per story — each story renders alone via ?story=<Export> at the full capture viewport, exactly as storybook frames the reference side — so sibling stories can't interfere (portal stacking, shared radio-group names, focus, container measurement). Two output tiers:

  • Transient (under ds-bundle/, wiped by rebuilds): _screenshots/compare/<group>__<Name>.png — sheet with one row per story: the true storybook render | the true preview render, side by side. Sheet images are shrunk to fit; the full-resolution originals are in …/compare/raw/ (…__sb.png / …__ds.png) — Read those when the sheet is too small to judge confidently.
  • Campaign state (in .design-sync/.cache/compare/, gitignored): <Name>.grade.json — your verdicts — and <Name>.json — capture facts: story↔cell pairing, shot paths, previewKind, the component's srcSha (story-file fingerprint), spot-check anchors. Reconstructible — absence just means "capture again". The only verdicts the script emits are factual: sb-error (story doesn't render in storybook), unpaired (no preview cell for the story), error (cell threw); every rendered pair is needs-grade.

Compare captures at most 6 stories per component by default — [STORY_CAP] in the log names components with more, and --max-stories <n> raises the cap. The cap is NOT part of the grade contract: raising it just captures the tail stories for incremental grading, and existing verdicts survive. One consequence to know: a capped component that grades fully match/close is verified-by-upload in full on future syncs even though its tail stories were never individually graded — raise the cap when those tail stories carry distinct variants worth verifying. Fan-out subagents must not change it mid-wave (sheets would cover different story sets than the orchestrator's worklist assumed).

State across runs — the first run verifies everything once; after that, one rule: grades follow your sources — the story files, your owned previews, the story set, the preview-affecting config (provider/storyImports/extraEntries/overrides/titleMap), and committed .design-sync/overrides/ forks. Pipeline churn (a skill or toolchain update re-rendering everything) is auto-verified by a sampled [SPOT_CHECK] with grades kept; your edits re-grade only what they touch. Pixel jitter can never churn grades.

  • Sources unchanged + fully graded match/closeskipped outright (carried forward): no capture, no re-grade — even when the bundle, styling, storybook, or the converter itself were rebuilt. --force recaptures everything and clears all grades — systemic re-verification, not casual sheet regeneration.
  • Sources changed (story edited, .tsx edited, config/fork edited) → recapture, grade cleared, re-grade from the fresh sheet. [STORY_CHANGED] marks stories whose code moved — those are the ones where an OWNED .tsx must be updated (generated previews re-derive automatically); a recapture without [STORY_CHANGED] usually just needs the re-grade.
  • [SPOT_CHECK] → re-captures named components without clearing their grades; Read the fresh sheets and confirm they still match the recorded grades. It can arrive driver-triggered after pipeline churn — the normal verification of a skill/toolchain update, not a bug. Divergence remediation scales with the churned set: a couple of components → re-grade just those; widespread → stop, diagnose, then --force a full pass. --spot-check N tunes the full-run random sample (0 disables); --spot-check-components A,B names picks explicitly, honored on scoped runs too (the §7 step-4 audit).
  • [REFERENCE_STALE?] → the bundle changed but the reference storybook didn't. If the DS source changed, rebuild .design-sync/sb-reference before grading — a stale reference makes every grade a comparison against the old design.
  • A story renders differently every capture (new Date()/Math.random() content) → the fingerprint is the story FILE, so the contract is stable — but the pixels aren't, and grading judges pixels. The frozen capture clock stabilizes date renders; for truly random content, pin values in an owned .tsx or cfg.overrides.<Name>.skip the story with a NOTES.md line.

Captures are stabilized for grading comparability (animations fast-forwarded, reduced motion, frozen clock — both panels show the same settled frame, the same rendered date). This is verification-only: shipped previews are untouched and fully animated.

Grading is done by whoever is working the component — you in the solo phase, each subagent for its own components in fan-out. After each compare run: Read the sheet (and raw PNGs when in doubt), judge each story from the images alone, Write the verdicts to .design-sync/.cache/compare/<Name>.grade.json (campaign-local working state — what makes a verdict durable is the upload: the uploaded _ds_sync.json anchors verified-by-upload skips on every future sync, any machine):

{"stories": {"Default": {"verdict": "match"}, "Compact": {"verdict": "match", "basis": "sibling-trusted"}}}
{"stories": {"Loading": {"verdict": "mismatch", "note": "spinner missing — story uses MSW mock"}}}

(Two components' files: a clean one graded under the sampling rule below — Default is the image-judged primary story, match on a warning-free component, which is what licenses the sibling-trusted entries — and a mismatching one, whose note drives the next fix.)

Rubric — grade what a designer would care about, looking at the two renders:

  • match — same content, composition, and styling. Ignore antialiasing fuzz, scrollbar slivers, sub-5px offsets, and framing differences (the storybook canvas and the preview page frame differently — judge the component, not its surroundings).
  • close — recognizably the same rendering with a minor delta (slightly different padding, focus ring, placeholder text). close is still a fix target, not an exit: if you can name the delta, you can usually name the knob — keep iterating. Accept close only after an iteration fails to improve it or no actionable cause remains, and the note must then say both what's off and what you tried / why it's not fixable (e.g. "focus ring color differs — storybook applies a global focus addon, not part of the DS").
  • mismatch — wrong/missing content, unstyled output, wrong variant, missing icons/images, default fonts. The note must say what differs — it drives the next fix.

When the REFERENCE side is the artifact — storybook gates the story behind UI chrome (a theme/control toggle message) while the preview renders the real component — judge the component render on its own and note the gating; a preview that renders more than the gated reference is not close.

Grade the primary story, trust the rest. Sibling stories of one component run through the same pipeline — same imports, same provider chain, same CSS — so when one of them renders faithfully the rest almost always do too. On a first sync, judge from images the component's primary story only (cfg.overrides.<Name>.primaryStory when set — the same story the single-mode card renders — else the sheet's first story). If it grades match and the component is clean — no sb-error/unpaired/error cells, no [PORTAL?], no [RENDER_BLANK], no blank or size-anomalous shots — write match for the remaining stories with a basis marker, {"verdict": "match", "basis": "sibling-trusted"}, so the record says how each verdict was reached (compare reads only the verdict string). All of a component's verdicts — the image-judged primary plus every sibling-trusted entry — go in its one grade.json Write: trusted siblings cost no image opens and no per-story passes. Grade exhaustively, story by story, when the component has portals/overlays, theme or provider sensitivity, an owned preview, or any warning — and always for the §4b solo set, whose exhaustive grading is what earns the trust in the first place.

Capture photographs every story either way — sampling saves grading attention, not capture time, and the sheets stay available for any deliberate later look (the §7 step-4 carried-grade audit uses the same grades-kept spot-check path). This is the same trust class as [STORY_CAP]'s ungraded tail stories, applied deliberately. Sampling never relaxes [FONT_MISSING] (§4a) — that check is invisible to the compare images either way.

4a. Fix decision tree — global first

Work top-down; a global fix repairs every component at once, a per-component fix repairs one:

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
133
Forks
13
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
storybook
Source
github.com/alecs5am/ralphy