ADR-0019: packages/web-ui β Component Architecture and Definition of Done β
Last Updated: 2026-06-08 Status: Active Context: Decksmith
Context β
Before scaffolding packages/web-ui, Session C (Phase 4.0.5) aligned on what a "complete component" means and how the package should be structured. Without this alignment, components are judged inconsistently and the boundary between shared UI and app-specific code drifts.
Current Decision β
1. Package Structure β
packages/web-ui/src/
ui/ β shadcn/ui + Base UI generated components (Button, Input, Dialogβ¦)
components/ β Decksmith custom prop-driven components (DeckCard, ManaSymbolβ¦)
typography/ β Semantic type components (Heading, Body, Label, Overline)
icons/ β Custom animated SVG icon components (BellIcon, TrashIconβ¦)
index.ts β Re-exports everythingEach component is colocated with its files:
ui/Button/
Button.tsx
Button.stories.tsx
Button.mdx
index.tsapps/web structure (app-specific components):
apps/web/src/
components/ β Functional components coupled to the app (Navbar, Sidebarβ¦)
routes/ β Pages and loaders
store/ β Zustand UI state2. The packages/web-ui vs apps/web Boundary β
Rule: A component belongs in packages/web-ui if and only if it is prop-driven and app-agnostic β it could be used in another React app without modification.
Mental test: "Could this component be used in another React app without changing anything?" Yes β packages/web-ui. No β apps/web/src/components/.
Components in packages/web-ui never import from:
- TanStack Router (no
useNavigate,<Link>with app routes) - TanStack Query hooks (no
useUser,useDecks) - Zustand store
packages/api-clientapps/api
| Component | Location | Reason |
|---|---|---|
<Button> | packages/web-ui/ui/ | Pure UI, props only |
<ManaSymbol symbol="W"> | packages/web-ui/components/ | Pure UI, props only |
<DeckCard name format colors> | packages/web-ui/components/ | Prop-driven, reusable |
<Navbar> | apps/web/src/components/ | Uses router + Zustand |
<Sidebar> | apps/web/src/components/ | Uses global UI state |
<CardSearchSlideOver> | apps/web/src/components/ | Uses TanStack Query |
3. When to Create a Component β
Rule of three: Don't create a component preemptively. When a pattern repeats in 3 different contexts, extract it. Building abstractions before seeing 3 real uses produces components that don't fit actual usage and must be refactored immediately.
4. Definition of Done β Two Levels β
v1 β Utilisable β
A component reaches v1 when it is safe to use in the app. It is not yet considered stable.
- [ ] Functional β all main use cases work
- [ ] TypeScript strict β explicit props, no
any - [ ] JSDoc on the component export and non-obvious props
- [ ] Semantic tokens only β no hardcoded values
- [ ] Every variant and state has a Story (default, hover, focus, disabled, error, loadingβ¦)
- [ ] Keyboard navigable (usable without mouse)
- [ ] Storybook
@storybook/addon-a11yβ 0 violations
Complet β Stable β
All v1 criteria, plus:
- [ ] Play functions on key interactions
- [ ] Dark + light mode verified in Storybook
- [ ]
ComponentName.mdxβ anatomy diagram, API table, dos/don'ts, accessibility notes - [ ]
a11y-revieweragent run β 0 violations - [ ] Motion tokens used for all transitions β never arbitrary durations
- [ ] Error and loading states covered where applicable
A component is promoted to
packages/web-uiwhen it reaches v1. It is considered stable when it is complete. Development on the app is never blocked waiting for component perfection.
5. Storybook MDX Documentation Structure β
Each component's .mdx file follows this structure:
# ComponentName
Brief description β what it is and when to use it.
## Anatomy
<Anatomy>
<ComponentName ...props />
<Anatomy.Label target="root">Root β semantic token usage</Anatomy.Label>
<Anatomy.Label target="label">Label β typography token</Anatomy.Label>
</Anatomy>
## API
<ArgTypes />
## Dos and Don'ts
| Do | Don't |
| ---------------------------------------------------- | ------------------------------------------ |
| Use `<Button variant="primary">` for the main action | Don't put two primary buttons side by side |
| Use `on-accent` text on amber buttons | Don't use white text on amber β fails WCAG |
## Accessibility
Notes on keyboard interaction, ARIA roles, focus management.
## Related
Links to related components.The <Anatomy> component is a Storybook-only utility (.storybook/components/Anatomy.tsx) that renders the real component with floating annotation labels. It is never imported in apps/web.
6. Code Comments Policy β
Two distinct levels β not to be confused:
- JSDoc (required on exports): describes the component's purpose, props, and any non-obvious behaviour. This is the API documentation.
- Inline comments (only when the WHY is non-obvious): a hidden constraint, a workaround for a specific bug, behaviour that would surprise a reader. If removing the comment wouldn't confuse a future reader, don't write it.
Do not comment what code does β well-named identifiers already do that.
Rationale β
Why separate ui/ from components/: ui/ files are regenerable via npx shadcn add β they are scaffolded first and customized after. components/ files are written from scratch and represent Decksmith's visual identity. The distinction signals intent and prevents accidentally overwriting custom code with a shadcn regeneration.
Why typography/ is separate: <Heading>, <Body>, and <Label> are not UI components in the traditional sense β they are contracts on the type scale. A <Heading level={2}> encodes "text-2xl, font-700, tracking-tight" in a single token. Separating them signals that raw Tailwind type classes are never used directly in feature JSX.
Why the prop-driven boundary matters: It mirrors the architectural rule already in place for packages (ADR-0005): packages don't reach into each other's concerns. A UI component that imports a routing hook is no longer a UI component β it is a feature. Keeping this boundary makes packages/web-ui independently testable and reusable.
Why two levels of done: A checklist with 12 required items before a component can be used creates a bottleneck β nothing ships while components are being perfected. v1 sets a safe minimum; "complete" sets the quality bar for stability. Components earn stability through use, not before.
Trade-offs β
Benefits:
- Clear ownership boundary β no ambiguity about where a component lives
packages/web-uiis independently testable (no app coupling)- Two-level done prevents both under-engineering and over-engineering
- MDX anatomy pattern produces living, always-accurate documentation
Costs:
- The
<Anatomy>component must be built as a Storybook utility before docs can use it - The prop-driven rule sometimes requires extracting data-fetching to a parent β slightly more wiring per feature
Risks:
- Boundary erosion over time (adding a routing import "just this once")
- Mitigation:
domain-revieweragent pattern applied topackages/web-uiβ any import from router/query/store in the package is a red flag to catch in review
- Mitigation:
Evolution History β
2026-06-08: Initial decision β
- Validated during Session C (Phase 4.0.5)
- Key decisions: prop-driven boundary rule, two-level done, rule of three for component creation, MDX anatomy pattern,
typography/separation, JSDoc vs inline comment policy