A working technical foundation for design + engineering to review together and decide: do we adopt this architecture for the real design system, on web and native?
Not the final design system. Token values are placeholders drawn from the v1.1 spec and the Figma typography page; the architecture is the thing under review.
apps/
web/ Storybook 10 (React + Vite) → Foundations · Components · Patterns
mobile/ Expo 57 + React Native Storybook → same organisation, on device
packages/
tokens/ DTCG JSON → generated TS + CSS vars (primitive → semantic, light + dark)
motion/ springs / durations / behaviours + per-platform adapters
icons/ one path source, SVG on web, react-native-svg on native
components/ Button · Pill · Card · List Item · Sheet · Text · ThemeProvider
pnpm install # Node ≥ 20, pnpm 10
pnpm web # Storybook → http://localhost:6006
pnpm mobile # Expo dev server; press i for iOS simulator
pnpm mobile:ios # native build (Reanimated + Gesture Handler need a dev build, not Expo Go)
pnpm typecheck # every package, web and native tsconfigs
pnpm tokens:build # regenerate packages/tokens/src/generated + dist/tokens.cssTestFlight later: cd apps/mobile && eas build --profile testflight --platform ios (fill in the
EAS project id in app.json; eas.json already has preview (internal) and testflight profiles).
Share meaning, not implementation.
| Shared (one source) | Per platform (*.web.tsx / *.native.tsx) |
|---|---|
token names + values (color.action.primary) |
how a colour reaches a pixel (CSS var vs style) |
semantic typography (type.action.lg) |
font loading, line-height units |
| spacing, radius, opacity numbers | — |
motion physics (motion.spring.snappy) |
Reanimated withSpring vs CSS linear() |
behaviours (motion.behavior.button-press) |
gesture handling, haptics |
| component props and state names | press handling, focus rings, a11y plumbing |
| the recipe: props → which tokens apply | — |
Each component has three files:
button/
Button.types.ts the conceptual API (mirrors Figma component properties)
Button.recipe.ts props → token paths + geometry; imported by BOTH platforms
Button.web.tsx <button>, CSS custom properties, :active/:focus-visible, spring-derived transition
Button.native.tsx Pressable + Reanimated spring + Expo Haptics, min-height not fixed height
Bundlers pick the file: Metro resolves .native.tsx, Vite is configured for .web.tsx
(apps/web/.storybook/main.ts), and each package type-checks twice via tsconfig moduleSuffixes.
Figma variables ──(later: export)──▶ packages/tokens/src/tokens/*.json (DTCG format)
│ node scripts/build.mjs
▼
src/generated/*.ts (typed objects) + dist/tokens.css (--coverd-* vars)
│ │
native components web components
Three tiers, references flow one way: primitive → semantic → component. Components import only
the semantic tier. Light and dark are two files with the same names pointing at different
primitives; the Theme toolbar in Storybook (and the Theme control on device) proves components
never change.
Naming follows the spec grammar {category}.{concept}.{variant}.{state} so it maps 1:1 to Figma
(color.text.primary ⇄ color/text/primary) and to CSS (--coverd-color-text-primary).
One deliberate deviation: where a token has states, the JSON keeps an explicit default leaf
(color.action.primary.default / .pressed) because a JSON key can't be both a value and a group;
the generator drops .default in CSS names so the spec rule still holds where people read it.
See docs/ARCHITECTURE.md for the reasoning behind each choice and the questions this prototype is meant to surface for the review.
No production build/publish of packages (apps consume TS source directly), no tests beyond type-checking and visual review, no Style Dictionary (the JSON is already in its format; swap the 60-line generator when you want multi-platform exports like SwiftUI/Compose), no Cera Round font bundling, no full variant matrix (3 of the spec's 7 button variants, 3 of 5 sizes).