Skip to content

ADR-0079: Warm ledger visual language

  • Status: Accepted
  • Date: 2026-10-20
  • Amended: 2026-11-01 (W02 — content-driven Save, shared field shapes, stable amount reserve, Reports/item totals and ledger status use the named roles; #372, #380, #387)
  • Amended: 2026-11-03 (one left-to-right bidi policy for monetary values in RTL layouts; #405)

Context

ADR-0062–ADR-0069 named spacing, role and control ownership but deliberately stopped at a neutral Material surface. Issue #253 asked for a distinct visual language across every destination: "Warm ledger" — warm paper, forest-green ink, restrained grouped cards, one hero surface and bundled type. The mockup used DM Serif Display for the hero and month label, but that face ships no tnum (tabular figures), so a changing amount would jitter and money would lose its column alignment (ADR-0009). The implementation also had to keep dynamic colour, large text, RTL and the existing money rules intact.

Decision

The shipped language is owned by ui/theme/ and is the only palette, type and motion source screens may compose.

Brand palette

Warm paper and forest/attention tones are the static brand schemes.

Token Light Dark
background #F6F4EC #11150F
surface #FCFBF6 #151A13
surfaceContainerLowest #FFFFFF #0D0F0A
surfaceContainerLow #EDF1E6 #1C2319
surfaceContainer #F0F3E9 #1D241A
surfaceContainerHigh #E4EBDD #242D20
surfaceContainerHighest #DCE2D5 #2D3628
onSurface #192017 #EEF2E7
onSurfaceVariant #5C6559 #A8B3A0
outline / outlineVariant #74786F / #DCE2D5 #8E9288 / #2D3628
primary / onPrimary #1F6B45 / #FFFFFF #9BD5A8 / #0C2B1B
primaryContainer / on #D7EEDB / #0C2B1B #284932 / #D7EEDB
tertiary (attention) / on #9A5B1E / #FFFFFF #F0B36A / #3B2C1A
tertiaryContainer / on #F6E5CC / #4A2A0B #3B2C1A / #F8DFC2
error / onError #B3261E / #FFFFFF #FFB4AB / #690005
Warm hero #1D5136 #224B33
onHero #FFFDF7 #F8FBF2
heroMuted #C9DDCE #C3D8C7
heroAccent #F3C978 #F3C978
Navigation surface #F1F4EA #141A12

Raw hexes live only in Color.kt / Roles.kt; screens compose AppSurface. ui-guard.sh rejects a raw colour literal outside ui/theme (scoped allowlist for photographic scrims).

Bundled fonts and type roles

Both families are bundled in res/font/, so the design does not depend on a download or the platform default.

  • DM Sans supplies interface weights 400 / 500 / 600 / 700.
  • Newsreader 400 supplies the display face for the hero amount and period label. It replaces the mockup's DM Serif Display because Newsreader has real tabular figures. This is a deliberate substitution, not an oversight.
  • Money figures always request lining + tabular features (MONEY_FONT_FEATURES = "lnum", "tnum"). A formatted amount is never truncated, size-reduced, abbreviated or horizontally scrolled; the layout reflows instead (ADR-0009). Reports' donut and 6-month totals use compactAmount; the itemization total uses amount and carries its currency; non-completed status labels use status in both the ledger row and the editor picker (#380).
  • Editor actions use content-driven height with a 48 dp minimum, so the Material Button target and semantics survive a wrapped large-text label (#372). The money-first Amount field reserves a stable seven-digit width while a normal amount is typed; a longer exact value still reflows instead of shrinking the currency control (#387).
Role (AppType / AppTextRole) Size / line Weight Family
heroAmount 40 / 48, −0.8 tracking 400 Newsreader + tabular
periodLabel 24 / 32, −0.48 tracking 400 Newsreader
screenTitle 22 / 28 700 DM Sans
sectionTitle 16 / 24 700 DM Sans
identity 14 / 20 600 DM Sans
amount 14 / 20 600 DM Sans + tabular
compactAmount 24 / 32, −0.48 tracking 600 DM Sans + tabular
supporting 12 / 16 400 DM Sans
eyebrow 10 / 16, 1 tracking 700 DM Sans
formBody 16 / 24 400 DM Sans
status 14 / 20, 0.1 tracking 500 DM Sans

Material's type slots are aliases of these named roles, so existing call sites inherit the contract without a second table.

Spacing, shapes and motion

  • Spacing keeps ADR-0062's scale and adds compact 12 dp (row inset and card gap), hero 20 dp (hero interior) and sectionGap 24 dp (the one block-to-block separation before a SectionHeader). titleToContent stays 8 dp and screenEdge stays 16 dp.
  • Shapes are extraSmall 4, small 8, medium 16, large 20 and extraLarge 28 dp. The named meanings in AppShape are field (medium), amountField (extraLarge), grouping (large) and chip (extraSmall); grouping cards use grouping, the hero and the money-first Amount/Currency pair use amountField, ordinary text fields and item rows use field. Pills stay CircleShape at the call site. Repeated controls do not re-pick a corner (#387).
  • Motion is a single Motion object: tab 180 ms, detail 240 ms + 32 dp, expand 200 ms, search 180 ms, control 180 ms, toast 180 ms + 8 dp with a 1,700 ms dwell, and one easing CubicBezierEasing(0.2, 0, 0, 1). Compose honours the animator scale, so scale 0 settles immediately. Money is never interpolated; only exact formatted snapshots crossfade (ADR-0071).

Dynamic colour

Dynamic colour is opt-in (Android 12+). When enabled, LocalDynamicColor is true and AppSurface.hero / onHero / heroMuted / heroAccent / nav fall back to steps of the active dynamic ColorScheme (primary, onPrimary, full onPrimary for supporting copy, tertiaryContainer for the accent, surfaceContainer; the copy/accent roles were amended by ADR-0087 for measured contrast). The static Warm literal is used only when dynamic colour is off, so a wallpaper-derived scheme never mixes in a brand hex. Grouping, text and income/expense roles are always scheme steps.

Money-figure policy

Amounts are Long minor units (ADR-0009), rendered from MoneyFormatter in the transaction's own currency or the screen's base currency. Expense amounts are neutral onSurface, never error red; income is the one positive accent. Over-limit messages and progress may use the error family, and an unavailable figure is an em dash with localized TalkBack copy, never a partial number. The hero, budget card and row each measure their text and reflow before clipping.

A monetary value is one left-to-right run whatever the ambient paragraph direction is (#405). The display string authors the order — the calling surface prefixes the direction +/-, and MoneyFormatter supplies the currency symbol and grouped digits — and the money type roles (amount, heroAmount, compactAmount) pin TextDirection.Ltr so bidi cannot move a leading +/- to the visual end: without that pin an RTL paragraph paints -RM 45.00 as RM 45.00- because the sign is a neutral character. The policy is direction only: the string, Long minor units, grouping and the locale's currency symbol/scale are unchanged, and the surrounding layout keeps its own RTL reading and focus order. It applies to the value surfaces — ledger row, day-header net, Home hero, and the itemization total line, which composes the money role as a label-plus-value line. An amount substituted into a prose sentence rendered with a prose role (budget over-limit copy, the Home budget pulse) stays part of that sentence's direction; no prose reordering is claimed.

Consequences

Every destination now shares one palette, two bundled faces and one motion vocabulary. Dark, light, dynamic colour and large text are variations of the same tokens rather than separate implementations. The substitution of Newsreader for DM Serif Display keeps money tabular at the cost of a slightly different display face than the mockup. Adding a Warm role means adding it in Roles.kt and, when mechanically checkable, a ui-guard.sh rule.

Verification

WarmLedgerContractTest (JVM) pins both schemes, every type role (including status), the tabular money features, the spacing/shape steps and every motion constant. RoleContractTest covers the named roles. FontResourceTest (instrumented) resolves all five bundled font resources on device. DesignSystemMatrixTest reads the laid-out hero and budget amounts across 320 dp, 100/150/200 % text and both themes and fails on ellipsis or lost characters; BudgetContrastTest covers over-limit contrast. TransactionDetailScreenTest covers the Save button label/bounds and the stable currency trigger, ItemizationSectionTest and AttachmentSectionTest cover the stacked narrow/large-text controls, and the Reports/Donut/TransactionRow tests pin currency and the tabular money roles (#372, #380, #387). RoleContractTest pins the money roles' left-to-right bidi policy, RtlMoneyPolicyTest measures the sign, currency and digit glyph order under forced RTL and the Arabic/Hebrew locales the platform resolves to RTL (320 dp, 200 % text, light/dark/dynamic), and RtlSignedMoneyOrderTest checks the rendered row, day header and hero stay complete (#405). The full suite evidence is recorded in Warm ledger verification.