Repo-specific gotchas for future syncs of the uploads.sh design system.
Same layout as either/releases: project inputs live under committed
.design-sync/; the converter scripts are staged into gitignored
.ds-sync/ at the repo root (never committed). Invocations always use
node .ds-sync/… against this tree.
.ds-sync/ is machine state. A fresh clone, git clean, or wiping the dir
removes it — re-stage from the /design-sync skill (or any surviving sibling
checkout that still has the tree, e.g. another repo's .ds-sync/):
mkdir -p .ds-sync
# From the skill base dir, or a known-good .ds-sync copy:
cp -R <skill-or-copy>/{package-build.mjs,package-validate.mjs,package-capture.mjs,resync.mjs,lib,storybook} .ds-sync/
# Install converter deps in ONE npm command (separate --no-save installs prune
# each other):
(cd .ds-sync && npm i esbuild ts-morph @types/react playwright)
# Optional render/capture browser:
(cd .ds-sync && npx playwright install chromium)Do not commit .ds-sync/ or its node_modules.
.worktreeinclude lists .ds-sync/ so Claude Code copies the staged harness
from the main checkout into new worktrees (same mechanism as .env). That
only helps when main already has a staged tree; it does not replace restaging
after a full clean. Plain git worktree add does not read .worktreeinclude —
restage by hand (or copy from main) in those worktrees.
- Shape is package (no Storybook). Source lives in
packages/ui/src/, built with tsup →packages/ui/dist/{index.js,index.d.ts,uploads-ui.css}. cfg.buildCmd=pnpm --filter @uploads/ui build. That needs the package's devDeps installed. In an isolated worktree with no workspace install, build it standalone:cd packages/ui && npm install --no-save --no-package-lock && npm run build(npm avoids pnpm workspace resolution; approve the esbuild postinstall). Usenpm run build, notnpx tsup.tsup.config.tssetsdts: false, so tsup alone emits onlyindex.js+uploads-ui.css; the 12.d.tsfiles come from the second half of the build script (tsc -p tsconfig.build.json --emitDeclarationOnly --outDir dist). Skip it and every<Name>Propscontract the design agent codes against degrades silently.- Converter invocation (from repo root):
node .ds-sync/package-build.mjs --config .design-sync/config.json --node-modules packages/ui/node_modules --entry ./packages/ui/dist/index.js --out ./ds-bundlePKG_DIRis walked up from--entry, so the entry must be the realpackages/ui/dist/index.jspath, not./dist/....
packages/ui/fonts/holds three self-hosted woff2:geist.woff2andgeist-mono.woff2(the latin variable files copied from@fontsource-variable/geist@5.2.9/geist-mono@5.2.8), andgeist-pixel.woff2(fromapps/web/src/styles/fonts/geist-pixel, SIL OFL — seeGeistPixel-OFL.txt).@font-facerules live insrc/styles.css(=dist/uploads-ui.css=cfg.cssEntry) and reference../fonts/*.woff2, a path that resolves from bothsrc/anddist/. If a Geist version bumps, refresh these three files; otherwise[FONT_MISSING]is impossible.
cfg.cssEntry=dist/uploads-ui.css— one self-contained stylesheet carrying the:roottoken layer +@font-face+ everyul-component class. No separate tokens package/glob; tokens are inline there and ship via thestyles.cssclosure.- No component-scoped custom-property declarations — thread per-instance
color through
color/currentColorinstead. claude.ai/design'scheck_design_systemtreats every--*declaration as a design token and flags any declared outside a theme scope (:root/[data-theme]). A component that needs one color knob shared across an element and its pseudo-elements (e.g. Progress's fill + dither cap) must setcolor: var(--token)on the element (overriding per[data-*]) and consume it ascurrentColor— not declare a--ul-*custom property on a component selector. Do not "fix" such a finding by hoisting the property to:root: the value varies per instance, so hoisting breaks it. Consume-with-fallback (var(--ul-x, 116px), like--ul-files-min-height) is also fine — it's never declared, so it isn't flagged. @kindtoken annotations are load-bearing — do not strip them. The token declarations insrc/styles.css/src/tokens.csscarry trailing comments the claude.ai/design compiler reads to classify tokens:--sans/--mono/--pixel→/* @kind font */,--pixel-shape→/* @kind other */(a1/0flag, not a color). Without them the compiler misgroups/flags those tokens. oxfmt preserves the trailing comments; keep them on the same line as the declaration's;.
- Playwright + chromium live under
.ds-sync/node_modules(see restage above — install with the other converter deps in onenpm i). On a fresh clone the.ds-sync/tree is gitignored and regenerated, so reinstall before validating. - 7 components use
cfg.overrides.<Name>.cardMode = "column"(Button, Divider, Field, GalleryTile, Panel, Progress, Select) to resolve[GRID_OVERFLOW]— their previews are wider than a grid cell. Not a warn once the override is applied. - Progress and Select were added to that list on the 2026-08-14 re-sync. The
2026-08-14 sync recorded them as fitting the default card width; a later converter
measures grid cells differently and flagged both
[GRID_OVERFLOW](wide). If a future converter flags more components the same way, the remedy is the same one the warn names — mergecardMode: "column", thenpreview-rebuild.mjs --components <them>; grades carry through it. - No
[RENDER_THIN]/variants-identicalwarns to record — all cells graded good. - Progress and Select were added to
@uploads/uiand authored on the 2026-08-14 sync (previews/Progress.tsx,previews/Select.tsx) — 3 cells each, all graded good (both later neededcardMode: "column", see above). Select is a new export fromField.tsx(ul-select/ compactul-select--sm); Progress is its own file (ul-progress__*,data-levelfill bands). Both added toconventions.md. That brought the DS from 12 → 14 components.
- Sync from an up-to-date
main, and check before uploading. A sync uploads whatever the working tree builds, so a stale base silently reverts merged design-system fixes in the Claude Design project — the anchor diff can't catch it, because the bundle genuinely changed and looks like a legitimate update. This bit us on 2026-08-14: a worktree branched at #663 re-synced ~1h after #664 merged and re-uploaded the pre-#664 stylesheet, restoring the exact--ul-progress-filldeclaration #664 removed. The design agent kept reporting the finding and was correct. Rungit fetch && git log --oneline -3 origin/mainbefore the build, and after building grep the bundle for whatever the most recent DS fix removed (grep -c 'ul-progress-fill' ds-bundle/_ds_bundle.css→ expect 0). - Harness is not in git — if
.ds-sync/is missing, restage before build/ validate (see "Converter harness" above). Worktrees only inherit it when main already has a staged copy. - Previews (
.design-sync/previews/*.tsx) import from'@uploads/ui'and are fully self-contained: no network, no fixtures.GalleryTilethumbnails are inline SVG data-URIs. Safe to re-render on any machine. .design-sync/conventions.mdenumerates real tokens/props/components validated against the build. Ifpackages/uirenames a token or component, re-validate the header (the base skill's conventions step does this) and fix drift. Drift is usually additive, not renames — the 2026-08-14 re-sync foundButton.varianthad gained"solid"(a real variant with its ownul-btn--solidCSS) while the header still listed only four. Nothing the header named was wrong, so a name-existence check alone passes; also diff each enumerated union against the fresh<Name>.d.tsfor added members, or the design agent never learns the new option exists.- The scoped
npm installinpackages/uiwrites no lockfile (--no-package-lock), so the exact tsup/esbuild versions aren't pinned there — the committedpackage.jsonranges are the source of truth. A realpnpm installat repo root is the canonical build path.