Design-system usage¶
How the Candle & Ink system is consumed in code. The visual language and
its rationale live in Candle & Ink and
ADR 0005; this page
documents the implementation surface in frontend/src/design/ (3 files:
tokens.ts, ThemeContext.tsx, useResponsive.ts).
tokens.ts — the single source of truth¶
"Every color, spacing value, radius, shadow, and typography scale in the app
should be imported from this module. Do not define design constants
elsewhere." (frontend/src/design/tokens.ts:1-6.)
The semantic warm layer: surface / ink / accent¶
The app-wide language is the semantic trio added in #798
(tokens.ts:681-740), derived from the journal's paper palette so the whole
app reads as paper-on-desk:
export const surface = {
canvas: colors.paper.background, // #faf6ef — the app ground
raised: '#ffffff', // lifted cards / sheets
sunken: colors.paper.backgroundAlt, // #f3ecdf — recessed wells
desk: colors.paper.desk, // #e7dcc8 — the deeper ground a sheet floats above
hairline: colors.paper.hairline, // #e3dccd — faint warm rule
} as const;
ink—primary #2b2620(13.9:1 AAA),soft #5a5046(7.3:1),muted #6b6055(5.7:1); every value clears WCAG AA on the canvas and is asserted insemanticTokens.test.ts(tokens.ts:699-707).accent— terracottaprimary #a5572f(4.9:1 as text),strong #8f4a28,onPrimary #ffffff; deliberately darkened from the graphical-only tier swatch so it clears AA as text (tokens.ts:709-718).- Dark counterparts
surfaceDark/inkDark/accentDarkare warm umber/off-white — "NOT Material's neutral#121212… candlelit paper rather than a black slab" (tokens.ts:742-778) — plussurfaceShadow/surfaceShadowDarkelevation pairs (tokens.ts:720-740,784-799). showcase/onShowcase/showcaseShadowgive hero moments (the Practice player, Course cover, Map celebration) a warm-dark band on an otherwise light screen (tokens.ts:801-837). The Practice player is the heaviest consumer (frontend/src/features/Practice/PracticeScreen.tsx:2-5,52-62).
The legacy grey colors.background/colors.text palette remains for
un-migrated screens "but [is] no longer the design default"
(tokens.ts:685-690); colors.paper.* stays the journal-only editorial
palette with AAA-checked ink values (tokens.ts:123-149).
Typography — type(width), editorialType, fonts¶
Only platform-system fonts are used — no bundled faces
(tokens.ts:547-556): serif resolves to Georgia on iOS / serif on
Android / a CSS stack on web, resolved from Platform.OS (not
Platform.select) so the module stays loadable under the repo's
hand-rolled test mocks (tokens.ts:520-545).
The app-wide responsive ramp type(width) (#800) pairs a serif
display/heading face with clean sans body/labels (tokens.ts:596-636):
export const type = (width: number) => {
const base =
width < breakpoints.sm ? 15
: width < breakpoints.md ? 16
: width < breakpoints.lg ? 17
: width < breakpoints.xl ? 18
: 19;
…
return {
display: serif(Math.round(base * 2.1), '700'),
title: serif(Math.round(base * 1.6), '600'),
heading: serif(Math.round(base * 1.25), '600'),
body: sans(base, '400'),
label: sans(Math.round(base * 0.9), '600'),
caption: sans(Math.round(base * 0.8), '400'),
} as const;
};
Usage pattern — call with the live window width and spread the face into
styles, e.g. const t = typeRamp(width); … style={[t.label, styles.rowLabel]}
(frontend/src/features/Settings/SettingsHubScreen.tsx:19,58-77).
editorialType is the journal's all-serif long-form ramp (display 34 →
caption 13, plus the italic marginNote face) (tokens.ts:569-592);
uiType.button and INTERACTIVE_TEXT_MIN = 16 pin interactive text to a
legibility floor distinct from the 44 dp touchTarget.minimum tap-area
floor (tokens.ts:443-455,558-567,642-645).
Layout, motion, accessibility invariants¶
spacing(n, scale)=n * 8 * scale; staticSPACINGconstants include the deliberate 14 pxbuttonV(tokens.ts:337-355).rhythm(#825) is the editorial screen rhythm consumed by the layout primitives (ScreenScaffold/ScreenHeader/EditorialSection): 16 px gutters, 24 px section gaps, and the 64 pxbottomFadeHeightfor theBottomFadeveil (tokens.ts:357-370).motioncentralizes durations (fast 90 / base 220 / threshold 400 ms, settle 6 px); "Every consumer gates onuseReducedMotionand falls back to the resting state" (tokens.ts:372-383).- Stage color utilities:
STAGE_COLORS/STAGE_ORDERwith a legacyTurquoise → Tealalias inresolveStageColor(tokens.ts:166-211),brightenColor/mixColors(tokens.ts:213-269), andreadableGlyphOn, a full WCAG relative-luminance computation that picks a black or white glyph per fill (tokens.ts:271-331).
ThemeContext — mode as token swap¶
ThemeProvider initialises from the system color scheme and setMode
"reskins the tree by swapping the resolved tokens without touching layout or
testIDs (#804)" (frontend/src/design/ThemeContext.tsx:59-68). The context
value is the resolved token set for the active mode:
const LIGHT_TOKENS: ThemeTokens = { mode: 'light', surface, ink, accent, surfaceShadow };
const DARK_TOKENS: ThemeTokens = {
mode: 'dark',
surface: surfaceDark, ink: inkDark, accent: accentDark, surfaceShadow: surfaceShadowDark,
};
(ThemeContext.tsx:31-38.) useTheme() outside a provider resolves the
light set with a no-op setter so screen tests need no wrapper
(ThemeContext.tsx:44-47,71). Consumers read useTheme().mode — e.g.
AppShell selects navThemeFor(mode) so React Navigation chrome follows
the same tokens (frontend/src/App.tsx:224-226,
frontend/src/navigation/theme.ts:12-47).
useResponsive¶
useResponsive() (frontend/src/design/useResponsive.ts:35-60) wraps
useWindowDimensions over the shared breakpoints
(xs 0 / sm 360 / md 600 / lg 900 / xl 1200, tokens.ts:471) and returns:
| Field | Meaning |
|---|---|
width, height |
raw viewport |
contentWidth |
width clamped to contentLayout.maxWidth (900 = journal page 680 + margin column 220, tokens.ts:501-517) |
isXS…isXL |
breakpoint flags |
scale |
breakpoint scale (0.85–1.2) × a 0.85 short-screen factor below 700 px height (useResponsive.ts:5-33) |
columns |
2 in landscape, 1 in portrait |
gridGutter |
spacing(1, scale) |
Typical consumer: HabitsScreen feeds scale into tile density and layout
(frontend/src/features/Habits/HabitsScreen.tsx:9-10).
Where the docs corpus complements this¶
This page covers consumption only; token rationale, palette provenance, and
the visual north star live in docs/design/ (see
Candle & Ink). The design section's
DESIGN.md inside the repo (frontend/src/design/DESIGN.md) remains the
in-repo spec.
Grounded in adepthood@55eef11, 2026-07-31.