Rewhaven Design System — Resolved Decisions (2026-06-17)
Captured from a grilling session before building the design_system components.
These are binding for the build. They sit on top of the existing token
foundation (ColorTokens dark+light from the marketsite OKLCH palette,
MotionTokens spring/glide, DsTheme ThemeExtension accessor) and the spec's
design guidance (§6 "tokens-first component library… calm, accessible,
emoji-forward"; §7 a11y NFRs).
The seven decisions
1. Identity — "Same soul, app-tuned"
The app carries rewhaven.com's palette, motion curves, and type personality so site→app feels like one brand — but the scales are app-tuned: tighter spacing, functional list density, smaller display type than the marketing site. We do NOT copy the site's airy spacing/type scales. Calm comes from the palette, spacing rhythm, and motion restraint — chosen deliberately, not inherited.
2. Scope — Atoms + gallery now; molecules per-screen
Build the foundational atoms + the gallery now. App-specific molecules (chore row, wallet card, today tile — anything bound to a domain model) get built in the app when the screen that needs them is built, then promoted into the DS once proven. Rationale: unlike the data domain (knowable up front), UI molecules are discovered from real screens; building them speculatively calcifies wrong assumptions (POC trap #4 — components built ahead of screens, then 17 pages bent around them).
Architectural consequence: design_system stays model-agnostic — it has
no dependency on client_sdk. Atoms take primitive props (strings,
callbacks, variant enums). Domain-bound molecules live in the app first.
Atoms vs molecules (refined 2026-06-17)
The DS layers as atoms/ · molecules/ · templates/. It contains atoms
(single, indivisible token-styled primitives — DsTextField, DsIconButton)
AND generic UI molecules: compositions of two or more atoms with no domain
binding (no client_sdk model, no app strings). The three generic identity
molecules are:
DsPasswordField—DsTextField+ reveal toggleDsIconButton.DsUsernameField—DsTextField+ leading person icon + clearDsIconButton; username autofill ([AutofillHints.username]), plain-text keyboard.DsEmailField—DsTextField+ leading mail icon + clearDsIconButton; email autofill ([AutofillHints.email]), email keyboard.
Username and email are deliberately distinct molecules: on a sign-up screen a
member supplies BOTH (username != email), so each is its own field. (Earlier,
sign-in mis-used DsUsernameField as the email field with [email, username]
autofill + an email keyboard; that's now DsEmailField, and DsUsernameField is
username-only.) These are reusable across any app and so belong in the DS now.
Together the three identity molecules (DsEmailField, DsUsernameField,
DsPasswordField) compose the sign-up, forgot-username (recover via
email), and forgot-password (reset via email/username) flows — which is
exactly why email and username are kept as distinct molecules.
Domain molecules (model-bound — chore row, wallet card, today tile) are still
built per-screen and promoted into the DS only once proven, exactly as
decision 2 requires; this refinement does not relax that rule. (Concretely:
DsPasswordField moved from atoms/ → molecules/ — it was mis-filed as an
atom — and DsTextField gained proper leading/trailing slots so a 44px
control sits inside its rounded border without clipping the corner.)
3. Theme default — Dark
Dark is the default everywhere (matches the marketsite). Light is fully supported as a toggle. Hard build requirement: because dark is default, the dark palette MUST clear WCAG 2.2 AA contrast for body text (light-on-dark is where legibility is hardest, and this audience is least forgiving of it). Verify contrast on the dark set as part of the build.
4. Typography — Split: brand display + hyperlegible body
- Bricolage Grotesque — display/headings (brand personality, used sparingly).
- Atkinson Hyperlegible — body, UI, and numerics (engineered to disambiguate letterforms for low vision; the app's audience is neurodivergent / often low-literacy kids). Use a tabular style for token amounts so columns align.
5. Accessibility — AA floor baked into every atom; Focus Mode reserved
Every interactive atom ships, from day one, with: ≥44px hit targets, semantic
labels, visible focus indicators, honors reduced-motion
(MediaQuery.disableAnimations), and AA-contrast tokens (incl. the dark set).
A11y is the product thesis ("built for every kind of mind"), not a later pass.
"Never red" scope (clarified 2026-06-17): "never red" governs STATUS (good/waiting/almost — the scaffold philosophy, where a kid's undone chore is "waiting" grey-blue, never punitive red); FORM/VALIDATION errors DO use the soft
error(red) token, per the western error convention.
Focus Mode (Tier-3): reserve the structure now — a third ColorTokens
variant (high-contrast, low-stim) + a density flag on the theme — so the mode
slots in later with no atom rewrite. Do NOT build Focus Mode screens/behavior
yet. (Reserving a token set is cheap structure, not a speculative component — so
this does not violate decision 2.)
6. Iconography — Two-track + bundled emoji font
- Content/identity = emoji, rendered via a bundled Noto Color Emoji font so a given emoji (kid avatar, the chore/reward emoji a parent picks — already model data) looks identical on phone, tablet, and TV. Cross-device consistency matters for pre-literacy kids who recognize "the fox chore."
- UI chrome = line icons (Lucide) for nav, buttons, affordances.
- Rule: emoji = identity, icons = controls.
7. Gallery — Widgetbook
A Widgetbook app in gallery/ (non-member, own lockfile — like
client_sdk/example). Provides knobs (live prop tweaking), theme switching
(dark / light / Focus), and device frames (phone / tablet / TV).
Process (revised 2026-06-18 — Widgetbook leads, doesn't trail): the Widgetbook harness is built at the START of Slice 2, BEFORE the atoms — so every atom is validated against real Flutter rendering, never an HTML/mock stand-in. Each atom's definition-of-done is three artifacts: (1) the widget, (2) a Widgetbook story with knobs, (3) golden tests across dark / light / Focus — the CI-enforced regression gate (real Flutter PNG snapshots; a visual regression fails the build). The Widgetbook web build is deployed live (an always-current, browsable gallery — toggle themes/devices yourself) once the first atom batch exists; hosting TBD (home server like the POC app, or Cloudflare Pages). "Show me" from here on means a real render (golden output or a Widgetbook screenshot), never an HTML approximation.
Build implications
- Tokens: finalize the app-tuned spacing scale (4px base, tighter than the
site) and the split type scale (Bricolage display / Atkinson body + tabular
numerics). Add the reserved Focus
ColorTokens. Verify dark-set AA contrast. - Atoms: each interactive atom ships with the a11y floor + golden tests across themes + a Widgetbook story with knobs.
Finalized atom inventory (from the POC audit, 2026-06-17)
A read-only audit of the POC's component layer (which discovered the real set across ~17 screens) validated our draft (~11/14) and corrected it. Final set:
Token groups — SEVEN (the POC's draft mentioned only 4; do not drop the rest)
Color (role-based, light+dark, kidColor(i), spend/save/give, coin,
good/waiting/almost — never harsh red) · Typography (carries no colour) ·
Spacing (4px base, s1..s12 — the single most-used token in the POC, 720 refs;
get the scale right) · Radius (sm..pill) · Elevation (level1/2/3 +
focus-ring, brightness-aware) · Motion (clamp to zero on reduced-motion) ·
Icon (emoji ignore tint / glyph tints to ink). Plus Breakpoints
(phone 640 / tablet 1024) as a layout token.
Atoms to ship now (ordered by proven POC traffic)
- DsButton — primary/ghost/soft/danger;
loadingshimmer that ALSO swallows taps (anti-double-submit); icon/trailing; expand; ≥44px. (POC #1 atom, 43 files) - DsCard — elevated lvl1/2, onTap, radius. (#2, 24 files)
- DsTextField — label, hint, errorText, suffix, obscure, onBlur. (#3, 19 files)
- DsSheet — modal bottom sheet (drag handle + title + scroll), theme-inject internally. (highest-value ADD — 13 files used the app's sheet, not the DS's)
- DsSegmented
— form input AND in-page tabs. (under-weighted in draft; 9 files) - DsTag (kinded) + DsStatusChip (good/waiting/almost dot) — split the draft's single chip; never red.
- DsAvatar + DsAvatarStack (overlap + ring; colour from
kidColor). - DsCoin + DsTokenPill (tabular amount, give/save/spend + coin colours) —
the draft's
DsTokenAmount, split in two. - DsStepper (−/value/+, clamped, 44px) — NEW vs draft; heavy in editors.
- DsIconButton (44×44, required semantic label) — NEW vs draft; ubiquitous.
- DsCheck (off/partial/on tri-state — sub-step completion needs partial)
- DsSwitch (pill).
- DsProgressBar (bar only; defer the ring until a screen needs it).
- DsSection (title + trailing action + dense) — the draft's
DsSectionHeader. - DsRow / DsListTile (generic leading·title·subtitle·trailing) — add deliberately; its ABSENCE in the POC caused 4+ near-duplicate domain rows.
- DsEmptyState (icon + title + body + action) — confirmed absent in the POC (screens improvised); add it.
- (optional, only if an early screen needs it) DsGateChip · DsBonusChip · DsBadge(count).
Template
DsAdaptiveScaffold — phone tabs / tablet rail / desktop sidebar (640/1024).
Bring nearly verbatim from the POC's HBAdaptiveScaffold — its single best reuse.
Build PER-SCREEN, NOT in the DS (validates decision 2)
The POC pre-built these as DS molecules/organisms and the app used ~none of
them, hand-rolling domain versions instead. So defer: all chore/wallet/goal/
ledger/approval/budget cards & rows, all editor-sheet form layouts, the
subtask builder, and trait/color/settings widgets. The DS provides the container
(DsSheet) + fields (DsTextField/DsStepper/DsSegmented); the form layout
stays in the feature.
One structural mistake to NOT repeat
The POC ran two token sources (the DS tokens AND a parallel
app/lib/outside/theme/ stack), forcing ugly double theme-injection. The rebuild
keeps one token source: design_system's DsTheme.
Status of each decision
All seven are RESOLVED and binding. Decision 3 (dark default) was the one the user took against the initial recommendation (which was light-when-unset); its contrast consequence is captured above as a build requirement.