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 usecompactAmount; the itemization total usesamountand carries its currency; non-completed status labels usestatusin 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
compact12 dp (row inset and card gap),hero20 dp (hero interior) andsectionGap24 dp (the one block-to-block separation before aSectionHeader).titleToContentstays 8 dp andscreenEdgestays 16 dp. - Shapes are
extraSmall4,small8,medium16,large20 andextraLarge28 dp. The named meanings inAppShapearefield(medium),amountField(extraLarge),grouping(large) andchip(extraSmall); grouping cards usegrouping, the hero and the money-first Amount/Currency pair useamountField, ordinary text fields and item rows usefield. Pills stayCircleShapeat the call site. Repeated controls do not re-pick a corner (#387). - Motion is a single
Motionobject: 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 easingCubicBezierEasing(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.
Related
- ADR-0009 — money as Long minor units
- ADR-0062 — shared screen rhythm
- ADR-0069 — emphasis, surface and type roles
- ADR-0071 — coordinated content motion
- Warm ledger verification
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.