Skip to content

ADR-0062: One spacing scale and a shared screen rhythm

  • Status: Accepted
  • Date: 2026-10-17
  • Amended: 2026-10-17 (issue #203 — one screen-level step; compact-Home exception dropped)
  • Amended: 2026-10-19 (review fix — one owner per inter-block gap; Activity's scope chip row carries no vertical padding of its own)
  • Amended: 2026-10-20 (Warm ledger — compact/hero steps and sectionGap 24 dp; ADR-0079)

Context

Each top-level destination chose its own outer gap: Home inset its scroll container by 8 dp, Reports and the Settings index by 16 dp, and Activity and Budgets by nothing, so tapping between tabs visibly changed the distance from the app bar to the first element (issue #171). Every gap was a bare dp literal at the call site, with no shared vocabulary to compare them.

Decision

ui/theme/Spacing.kt owns the app's one spacing scale — xs 4 dp, sm 8 dp, md 16 dp, lg 24 dp, xl 32 dp — plus two screen-level tokens: Spacing.titleToContent (the gap between a destination's app bar and its first content element) and Spacing.screenEdge (the horizontal content inset). Every top-level destination applies titleToContent as its outer top inset: Home, Activity, Budgets, Reports, the Settings index and task pages, and the review queue.

The screen rhythm is one step: Spacing.sm owns the gap from the first control to the block below it and every block-to-block gap on Home, Activity, Budgets, Reports, the Settings index and the review queue (issue #203). Home's former "compact rhythm" is now the app rhythm rather than an exception. Card interiors keep their own deliberate steps — the shared step fixes the screen grid without flattening intentional density (#143, #198).

Warm ledger amendment (2026-10-20)

The scale gains three named steps: Spacing.compact 12 dp (dense row vertical inset and card gap), Spacing.hero 20 dp (the Home and Reports hero interior) and Spacing.sectionGap 24 dp (aliasing lg). sectionGap is the only separation before a SectionHeader; a raw 24 dp literal or Spacing.lg before a heading fails ui-guard rule 7, and a sectionGap with no heading nearby fails as misplaced. In-section gaps stay sm or component-owned, so the one-step screen rhythm and rule 6 are unchanged (ADR-0079).

Consequences

Navigation now lands the first element at the same distance on every destination, and the block below it starts at the same step. A new screen reaches the shared rhythm by using the tokens rather than copying a number, and scripts/ui-guard.sh fails a destination that drops titleToContent or hand-rolls a screen-level gap (ADR-0065). Adopting the token narrowed Reports and Settings from 16 dp to 8 dp and gave Activity and Budgets their missing 8 dp. The one-step amendment also narrowed the first-control-to-content gap on Budgets, Reports and the Settings index from 16 dp to 8 dp, switched Budget rows from an off-scale 12 dp to 8 dp, and gave the review queue's summary-to-list seam its missing 8 dp. The review queue's summary row keeps only its horizontal inset so its own padding cannot stack on the token.

Verification

TitleToContentRhythmTest renders every destination (Home, Activity, Budgets, Reports, Settings index, review queue) and asserts the measured gap from the app bar's bottom equals Spacing.titleToContent, using getUnclippedBoundsInRoot() so it stays hermetic to the 320×640 CI AVD. Home's first content element is the capture-status notice (issue #242), so the test anchors the shared gap on that notice when it is shown. It also measures the first-control → next-block gap (Home month→hero, Activity month→search, Budgets month→first card, Reports month→snapshot, Settings search→first card) and the block→block gap (Home hero→recent, Activity scope→list, Reports snapshot→charts, Settings card→card, review queue summary→list) at Spacing.sm; Activity's scope chip row sits below the search/filter controls so the month→search step holds, and it carries no vertical padding of its own — the search row's bottom sm owns search→scope and the results surface's top sm owns scope→list, so no two tokens stack into a 16 dp gap (issue #247; review fix). HomeAdaptiveLayoutTest keeps the compact Home fold green. scripts/ui-guard.sh rule 6 rejects a raw screen-level gap with fail/pass self-test fixtures, including a legitimate card-interior literal. Rule 7 fails a raw or alias 24 dp section gap and a misplaced sectionGap; WarmLedgerContractTest pins the 12 / 20 / 24 dp steps.