| name | sync-shadcn |
| description | Sync upstream shadcn/ui updates into our forked primitives in packages/storefront-ui/src/components/ui/ via a 3-way merge that preserves our customizations (relative imports, rounded-ui/shadow-ui/border-ui shape tokens, data-slot attributes, added props), AND apply our house-style shape tokens to ANY component via `restyle` โ including a brand-new `npx shadcn add`ed primitive with no baseline. Use when shadcn ships new variants, accessibility fixes, or dependency bumps and you want to pull them into our forks without clobbering local changes (`sync`); when applying/normalizing our shape tokens on a new or existing component (`restyle`); when checking which primitives have drifted behind upstream (`status`) or off our house style (`infer`); or to see exactly what we customized for a component (`diff`). Repo-agnostic: resolves the ui dir from components.json, so it runs in a flattened customer/mirror repo, not just the monorepo.
SKIP when: editing fashion/cosmetic primitives directly (those derive from packages/template via the mvt-* mirror skills โ restyle the storefront-ui source instead); making a one-off manual tweak to a single primitive.
|
sync-shadcn โ 3-way merge for upstream shadcn updates
Our src/components/ui/ primitives are forks of shadcn/ui (copy-paste, not an npm
package), edited in place with our customizations. This skill pulls upstream updates in
without losing those edits, using a 3-way merge:
| input | what it is | where it lives |
|---|
| base | pristine upstream we last synced from | packages/<pkg>/.shadcn-baseline/<name>.tsx |
| theirs | current upstream, freshly fetched | shadcn registry (new-york-v4) |
| ours | our customized fork | packages/<pkg>/src/components/ui/<name>.tsx |
git merge-file replays the baseโtheirs delta (what upstream changed) onto ours.
Our customizations are the oursโbase delta, so they survive automatically; a conflict
appears only where upstream changed a line we also customized.
The .shadcn-baseline/ snapshots are not imported by anything โ they exist solely as the
merge's reference point. They contain vanilla @/ imports and no copyright header, so the
root ESLint config ignores **/.shadcn-baseline/** and tsconfig (include: src/**) never
compiles them.
Two composable mechanisms
The skill is customization-as-merge plus customization-as-transform:
| mechanism | what it handles | needs a baseline? |
|---|
sync | 3-way git merge-file | STRUCTURAL deltas: added props, data-slot, behavior, variants | yes (the .shadcn-baseline/ anchor) |
restyle | declarative ruleset (ruleset.json) | MECHANICAL shape tokens: rounded-*โrounded-ui, shadow-*โshadow-ui, borderโborder-ui (Card), import convention | no โ works on any file |
They compose: sync brings upstream structure in, then restyle normalizes any raw shape tokens
the merge reintroduced. A brand-new component (no baseline) gets its house style from restyle
alone.
When to run
- shadcn announced a release, or you want to check for drift โ
status.
- A primitive needs an upstream a11y/variant fix โ
sync <name>.
- Just ran
npx shadcn add <name> โ restyle --path <file> to apply our shape tokens.
- Want to confirm forks haven't drifted off our house style โ
restyle --all --check / infer --all.
- Periodically (e.g. quarterly) to keep forks from drifting far.
Default package is storefront-ui (the canonical fork off upstream; the only package with
.shadcn-baseline/ snapshots). For any other layout โ including a flattened customer/mirror
repo โ the skill discovers the ui dir from the nearest components.json (walk up from --path
or cwd), so no --package is needed. Fashion/cosmetic primitives are out of scope for direct
edits โ they mirror packages/template (which inlines storefront-ui), so restyle the
storefront-ui source, not the mirror output.
Commands
S=.claude/skills/sync-shadcn/sync.mjs
node $S status
node $S sync button
node $S sync button dialog card
node $S sync --all
node $S diff button
node $S advance button
node $S restyle button
node $S restyle --all
node $S restyle --path src/components/ui/toggle.tsx
node $S restyle --all --check
node $S infer --all
node $S infer card --emit
node $S sync --all --bootstrap
Restyle โ the house-style ruleset
ruleset.json (beside this skill) is the declarative source of truth โ token families, exclude
list, the Card-only border scope, and the import convention. restyle rewrites only
className / cn() / cva() string literals, matching tokens by exact variant-stripped core:
rounded-md/-lg/-xl/โฆ โ rounded-ui; shadow-sm/-xs/โฆ โ shadow-ui.
border โ border-ui only on Card (families.border.scope.only). Every other primitive
keeps literal border โ border-ui is Card-only (see docs/README-SHAPE-TOKENS.md).
- Preserved (never members):
rounded-full, rounded-none, directional/arbitrary radii
(rounded-t-lg, rounded-[2px]), border-2, border-input, shadow-none.
- Import convention auto-adapts: where the
@/ alias resolves via tsconfig paths (the
customer/mirror convention), @/ imports are KEPT; where it does not (storefront-ui bans @/
under ui/), they are relativized (@/lib/utils โ ../../lib/utils).
restyle is idempotent โ the replacements are never members, so running twice is a no-op.
New component flow
cd packages/storefront-ui
npx shadcn@latest add <name>
node $S restyle src/components/ui/<name>.tsx
pnpm lint && pnpm typecheck
node $S sync <name> --bootstrap
Customer brand layer (dogfood path)
A generated customer project gets this skill in .claude/skills/sync-shadcn/. Customers tailor it
without editing ruleset.json: drop a ruleset.customer.json beside it (copy
ruleset.customer.json.example). It deep-merges OVER ours โ precedence upstream โ our ruleset โ
customer overlay. Semantics are additive: members UNION (with explicit removeMembers to
subtract โ so you can't accidentally un-protect rounded-full, which was never a member),
replacement overrides, scope.only and relativizeAliases UNION (with removeAliases).
Workflow
- Check drift.
node $S status โ lists which primitives are BEHIND.
- Sync.
node $S sync <name> for each behind component. The merged result (with any
conflict markers) is written in place into the fork file. On a clean merge, sync
auto-runs restyle to normalize any raw shape tokens the merge reintroduced (reported as
merged clean (restyled N token(s))). The command reports per component:
up-to-date / merged clean / MERGED WITH CONFLICTS (N hunks), plus any anomalies.
- Resolve conflicts (only if reported โ see below). Conflicted files are not
auto-restyled during
sync (the engine won't tokenize conflict-marker lines). Once you've
removed the markers, advance (step 5) normalizes any raw shape tokens you kept before
promoting the baseline โ or run node $S restyle <name> yourself to see the changes first.
- Verify (always):
cd packages/storefront-ui && pnpm lint && pnpm typecheck
pnpm lint is the safety net: storefront-ui's ESLint errors on any @/* import in
src/components/ui/**, so an alias import pulled in from upstream fails here, not in prod.
If your change altered rendered DOM, also run the consuming package's snapshot tests
(cd packages/template && pnpm storybook:test --type=snapshot).
- Advance the baseline. Once the fork is clean and verified:
node $S advance <name>
This promotes base โ current upstream so the next sync is conflict-free for those lines.
advance refuses while any conflict marker remains โ the baseline never advances from a
half-merged tree. It also runs restyle on the (marker-free) fork first, normalizing any raw
shape tokens a manual conflict resolution kept โ idempotent, a no-op after a clean merge.
- Commit the merged fork file(s) and the updated
.shadcn-baseline/ together.
Conflicts โ what to expect
A conflict means upstream changed a line you also customized. With --zdiff3 you see all three
versions, so you can tell what each side changed:
<<<<<<< ours (our fork)
outline: "border bg-background shadow-ui hover:bg-accent ...",
||||||| base (last synced upstream)
outline: "border bg-background shadow-xs hover:bg-accent ...",
=======
outline: "border bg-background shadow-xs hover:bg-accent ... dark:bg-input/30",
>>>>>>> theirs (current upstream)
Read base โ ours (we changed shadow-xsโshadow-ui) and base โ theirs (upstream added
dark:bg-input/30). Usually the resolution is the union: keep our token, take their addition.
Delete the four marker lines, leaving the resolved line. Then re-run verify + advance.
Common customizations you'll be protecting: rounded-mdโrounded-ui, shadow-xsโshadow-ui,
the border-ui utility, data-slot/data-variant/data-size attributes, relative
../../lib/utils imports, and added props (e.g. dialog showCloseButton, card CardAction).
Anomalies the skill reports (does not auto-merge)
| report | meaning | action |
|---|
[new-dependency] | upstream declares a dep we don't have (e.g. unified radix-ui) | review; our fork may intentionally use individual @radix-ui/* packages |
[multi-file] | upstream split the component into several files | merge the primary <name>.tsx; handle extra files manually |
[renamed] | upstream's file basename โ <name>.tsx | likely rename/split; inspect before trusting the merge |
[missing-fork] | exists upstream, not in our set | npx shadcn add <name>, then restyle --path to apply our shape tokens + sync --bootstrap to seed the baseline |
NOT FOUND (404) | not published upstream (e.g. our custom native-select) | skipped; nothing to sync |
Notes
- No npm dependencies; uses Node global
fetch + node:crypto + system git.
- The registry URL uses the
-v4 style suffix. The un-suffixed new-york path serves a
stale, pre-data-slot snapshot older than our fork โ sync.mjs resolves the suffix from
components.json automatically; don't hardcode the un-versioned path.
storefront-ui is a private package โ no changeset needed for changes confined to it.
- This skill ships into generated customer projects: the customer-skills allowlist in
scripts/lib/ship-skills.mjs copies it into the mirror artifact (packages/template's
mirror.mjs) and the standalone template (scripts/generate-storefront.js). Tests and fixtures
are excluded from the shipped copy.