ADR-0015: Design System Architecture β Tokens, Theming, and No-Figma Workflow β
Last Updated: 2026-05-30
Status: Active
Context: Decksmith
Context β
Decksmith needs a design system that:
- Supports a dark theme (primary) and a light theme, switchable at runtime without a page reload
- Encodes MTG-specific visual language: mana colour tokens and mana symbol icons
- Can eventually be consumed by both
apps/web(React/Tailwind) andapps/mobile(React Native) - Can be iterated on without a dedicated designer or separate design tool
Four interrelated decisions had to be made:
- Where and how is the visual language defined? (tokens strategy)
- How is theming implemented? (CSS custom properties vs Tailwind
dark:variant) - How do we design without Figma? (no-Figma workflow)
- How are MTG mana symbols rendered? (Keyrune vs custom icons vs coloured circles)
Note: Phase 4.0 produced documentation.
packages/tokensandpackages/web-uiare not yet built. Specific values (colour hex codes, spacing scale, typography sizes, component APIs) are current best thinking and will be refined during Phase 4.1 implementation. The architectural patterns documented here β CSS custom properties, token package, Keyrune β are stable.
Current Decision β
1. Tokens in packages/tokens β
All design tokens are defined once in packages/tokens and consumed by platform packages:
apps/web/tailwind.config.tsimportsdecksmithPresetfrompackages/tokens/src/tailwind.tsapps/mobile(future) reads raw token values directly frompackages/tokens/src/tokens.ts
Token categories:
| Category | Examples |
|---|---|
| Semantic | bg, surface, surface-raised, border, text, text-muted, text-faint |
| Accent | accent, accent-hover, accent-subtle, accent-border |
| MTG colour | mtg-white, mtg-blue, mtg-black, mtg-red, mtg-green, mtg-colorless, mtg-multi |
| Typography | Font families, size scale (xs β 4xl), weights |
| Spacing | Shared spacing scale |
MTG colour tokens are defined as a separate group from semantic tokens β they represent Magic's colour identity system, not UI states. mtg-red β error even though both are red hues.
2. CSS Custom Properties for Theming β
Dark/light theme is implemented via CSS custom properties on :root (light) and .dark (dark override), not via Tailwind's dark: class variant.
:root {
--color-bg: #faf9f4;
--color-surface: #ffffff;
--color-text: #1c1b22;
}
.dark {
--color-bg: #0f0e17;
--color-surface: #1a1827;
--color-text: #e8e6f0;
}Tailwind classes reference the CSS var through decksmithPreset:
// packages/tokens/src/tailwind.ts
colors: {
bg: 'var(--color-bg)',
surface: 'var(--color-surface)',
text: 'var(--color-text)',
}Components use semantic class names (bg-bg, bg-surface, text-text-muted). Theme switches by toggling .dark on <html> β no page reload, no class proliferation in JSX.
3. No-Figma Workflow β
Figma is not used as design source of truth. The design-to-code workflow is:
ASCII mocks (apps/docs/design/screens/)
β
packages/tokens (visual language β colours, type, spacing)
β
Tailwind config (decksmithPreset applied to apps/web)
β
packages/web-ui (shadcn/ui base + Decksmith-specific components)
β
Storybook (visual iteration + living documentation)ASCII mocks in apps/docs/design/screens/ define screen structure and layout regions. They live in version control alongside the specs they reference. packages/web-ui + Storybook replace Figma for visual component iteration.
4. Mana Symbol Icons β Inline SVG (mana-font paths) β
MTG mana symbols ({W} {U} {B} {R} {G} {C} {X} etc.) are rendered using inline SVG path data sourced from mana-font. The implementation consists of three composed components:
ManaIconβ pure SVG element,fill="currentColor", zero dependencies. Takes a symbol key ('w','u','b','r','g','c','x','tap', numeric generics, etc.).ManaSymbolβ wrapsManaIconin a rounded pip container with the correctbg-mtg-*token color. This is the canonical colored pip that mirrors physical card appearance.HybridManaSymbolβ diagonal split pip via SVG clipPath triangles for hybrid costs ({W/U}etc.)ManaCostβ parses a cost string and renders a row ofManaSymbol/HybridManaSymbol.
Convention: {W}, {U}, {B}, {R}, {G} notation in ASCII mocks maps to <ManaSymbol symbol="W" /> in React. In Storybook stories, Lucide icons are used for decoration; mana glyphs always use these components.
WUBRG colour tokens (mtg-white, mtg-blue, etc.) are the canonical source for pip background colors, text in color-identity badges, and rarity chips β always via tokens, never hardcoded.
Rationale β
Why packages/tokens rather than values directly in tailwind.config.ts? β
apps/mobile cannot use Tailwind β it needs raw token values. Centralising tokens in a shared package prevents the web and mobile implementations from drifting apart. The same principle as packages/schema for data contracts: one source, multiple consumers.
Why CSS custom properties over Tailwind dark: variant? β
The dark: variant requires every component to duplicate its classes: bg-white dark:bg-gray-900. As the component library grows, this creates two problems:
- Verbosity: every new component must remember both variants β a maintenance burden, not a one-time cost.
- Semantics:
bg-gray-900is a colour value, not a meaning.bg-surfaceis a meaning whose value changes with the theme. Semantic names are more readable and more robust to palette changes.
With CSS vars, a component uses one class (bg-surface). Runtime theme switching is one line: document.documentElement.classList.toggle('dark'). No rebuild, no page reload.
Why no Figma? β
Figma creates a second source of truth. Design drift β where Figma shows one thing and the code does another β is a constant maintenance tax. Every Figma change requires a code update (or vice versa), and without a dedicated designer to enforce the sync, it breaks quickly.
For a solo/small-team project, the cost of maintaining Figma sync outweighs its benefits:
- ASCII mocks are version-controlled, co-located with specs, and reviewed in the same PR
- Storybook provides the living component gallery that Figma would otherwise be used for
- The no-Figma workflow forces decisions to be made in code, where they actually live
Why inline SVG paths instead of Keyrune or bare coloured circles? β
Every MTG player recognises {W} {U} {B} {R} {G} as canonical symbols β they see them on every card they own. Bare coloured circles lose the iconic semantics: the white mana symbol (sun) is immediately understood; a white circle is ambiguous and requires text labels to communicate meaning.
Keyrune (the original decision) was planned before implementation. The inline SVG approach was adopted instead for three reasons:
- No external asset load β Keyrune requires loading
mana.css(a CSS icon font). Inline SVG paths are bundled directly, with zero network dependency after the initial page load. - Tree-shakeable β only the symbols actually used appear in the bundle. Keyrune ships all symbols regardless.
- Full styling control β
fill="currentColor"onManaIconmeans any CSS color applies. Combined withmtg-*token pip backgrounds, the result matches the physical card appearance exactly β which Keyrune with a CSS font would require additional CSS to achieve.
The mana-font SVG path data covers the same symbol set as Keyrune (including {W/U} hybrids, {C}, {X}, {T}, numeric generics 0β20, Phyrexian mana, snow, energy, etc.).
Trade-offs β
Benefits:
- Single token source shared across web and mobile β no duplication or drift
- Runtime theme switching with zero changes to component code
- Semantic class names (
bg-surface) are more meaningful and more stable than colour names - Mana icons are immediately recognisable to the target audience (canonical WotC glyph shapes)
- No external tool dependency for design iteration
- Design decisions are version-controlled and reviewed in PRs
Costs:
- CSS vars are slightly less discoverable in JSX than explicit
dark:classes - Storybook must be set up before visual component iteration is possible
packages/tokensmust be built beforeapps/weborpackages/web-uican startmana-paths.tsadds bundle weight, but only the symbols used are included (tree-shaken)
Risks:
- Token names agreed here may not match what feels natural when building components
- Mitigation: names are not committed in code yet β they will be finalised during Phase 4.1 (
packages/tokensscaffold). ADR to be updated then.
- Mitigation: names are not committed in code yet β they will be finalised during Phase 4.1 (
- Without Figma, no high-fidelity mockup to show stakeholders
- Mitigation: Storybook serves as the live component gallery; ASCII mocks are sufficient for planning at this stage
Evolution History β
2026-06-10: Web output format updated β Tailwind v4 CSS-first confirmed β
The ADR-0015 section on tokens referenced a Tailwind v3 JS preset approach (decksmithPreset in tailwind.config.ts). The actual implementation (Phase 4.1) uses the Tailwind v4 CSS-first architecture: packages/tokens/src/web/tokens.css uses @import 'tailwindcss' with @theme inline (for dynamic theme-aware tokens) and @theme (for static tokens). No tailwind.config.ts or JS preset object exists. Details in ADR-0017.
2026-06-08: Token values finalised β Session A (Phase 4.0.5) β
- Accent colour shifted from Tailwind amber-500 (
#f59e0b) to golden#e8b84bβ less orange, more "dorΓ© chaud". Light mode accent-text set to#8a6a0cfor WCAG AA compliance on light bg. - New tokens:
on-accent(text on amber button β always#0f0e17),accent-text(accent-hued text that passes contrast). - Tailwind v4
@themeCSS confirmed as the web output format β no JS preset object needed. - Implementation details (layer structure, output format, motion system, Storybook requirements) documented in ADR-0017.
- This ADR remains the source of truth for strategic decisions (where, how, why); ADR-0017 owns the concrete implementation.
2026-05-30: Initial decision β
- Decided after completing Phase 4.0 design documentation (PR #16)
- ASCII mocks created for all 7 screens β documented in
apps/docs/design/screens/ - Visual identity defined:
#0f0e17dark bg,#faf9f4light bg, amber#f59e0baccent, Outfit typeface, JetBrains Mono for numbers/stats - All individual design decisions logged in
apps/docs/design/decisions.md - Status: architectural patterns locked, specific values (hex, spacing, type scale) will be refined during Phase 4.1 implementation