Design contribution workflow
AutoBudget has one design system and no per-screen styling. Any UI change goes through this workflow; it is a checklist, not a gate — a failing primary-action, amount or reflow contract blocks the change, everything else is judgment.
Owners
| Concern | Owner |
|---|---|
| Spacing, shape, type, colour roles, motion | android-app/app/src/main/java/com/zharif/autobudget/ui/theme/ (Spacing.kt, Shape.kt, Type.kt, Color.kt, Roles.kt, Motion.kt) |
| Reusable controls | ui/components/ |
| Design usage and component selection | docs/architecture/ui.md |
| Examples (not a second spec) | the debug component gallery (app/src/debug/.../ComponentGallery.kt + ui/components/gallery/GalleryInventory.kt) |
| Mechanical reuse rules | scripts/ui-guard.sh (+ scripts/ui-guard-allowlist.txt) |
| Behaviour decisions | a numbered ADR |
| Evidence | warm-ledger-verification.md |
Values live once. A screen composes tokens and shared controls; it never re-decides a colour, a spacing step, a duration or a control's implementation (ADR-0062, ADR-0065, ADR-0069, ADR-0071, ADR-0079).
Token workflow
- Change the token, not the screen. Palette entries and Warm literals live
in
Color.kt; named surface/type roles inRoles.kt; the two bundled families and every type role inType.kt; spacing inSpacing.kt; corners inShape.kt; durations, offsets and the single easing curve inMotion.kt. - Add a role when the meaning is new. A new surface, emphasis or type tier
is added once in
Roles.kt(AppSurface/AppTextRole) and documented inui.mdand ADR-0079. Screens never read a brand hex or a rawTextStyle. - Add a step only when it repeats. Use
Spacing.compact12 dp for a dense row/card gap,Spacing.hero20 dp for a hero interior andSpacing.sectionGap24 dp before aSectionHeader. A one-off card interior may keep a local literal; a screen seam may not. - Motion comes from
Motion. Tab 180 ms, detail 240 ms + 32 dp, expand 200 ms, search 180 ms, control 180 ms, progress 400 ms, swipe settle 200 ms, lock-shake step 40 ms, toast 180 ms + 8 dp. Pass the constant totween; never write a new duration —scripts/ui-guard.shfails a rawtween(<number>). Compose honours animator scale, so scale 0 settles immediately, and money is never interpolated. - Update the contract tests with the token.
WarmLedgerContractTestpins the schemes, type roles, tabular features, spacing, shapes and motion;RoleContractTestpins the named roles. A deliberate token change updates both the token and its test in the same change.
Gallery coverage manifest
The maintained gallery is debug-only and never ships. Its inventory is
ui/components/gallery/GalleryInventory.kt, the single source of truth for
scripts/ui-guard.sh rule 9 and GalleryInventoryTest (JVM):
SharedControlSource(file, GalleryComponent)— every Kotlin file directly underui/components/must be mapped to aGalleryComponent, or listed ingalleryExemptSourceswith a reason.GalleryComponent— the section shown in the gallery.GalleryFixture(component, state)— one real control in a meaningful state (Normal,Selected,Disabled,Empty,Filled,Compact,Expanded,LongContent,LargeAmount,Income,Pending,Failed,Zero,AtLimit,NoLimit,Error). Every component needs at least one fixture; a mapping with no fixture fails.GalleryExemptSource(file, reason)— a helper or feature-only surface covered by its own tests, never a design-system control. The reason is required.
Adding a control file without updating the manifest fails ui-guard; adding an
entry that names a missing file fails too. The gallery is a coverage contract,
not a screenshot oracle: it proves the real control and its states are
reachable in light and dark at narrow/large text.
Guard rules (what CI actually enforces)
bash scripts/ui-guard.sh runs self-tests and then checks source structure:
| Rule | Contract |
|---|---|
| 0 | Shared components and Warm foundation tokens exist (compact, hero, sectionGap, shape steps, every Motion constant, the easing curve) |
| 1 | One search field — no screen hand-rolls an OutlinedTextField, BasicTextField or TextField in a file with search intent (structural; issue #347) |
| 2 | Every top-level destination applies Spacing.titleToContent with no raw 8/16 dp literal |
| 3 | No hand-rolled settings chevron or ListItem row |
| 4 | One settings section card; rows are transparent |
| 5 | Grouping composes AppSurface.grouping; no heavy container literal |
| 6 | One Spacing.sm screen-level rhythm; card interiors may differ |
| 7 | Spacing.sectionGap is the only block gap before a SectionHeader; a raw/alias 24 dp or misplaced token fails |
| 8 | Raw colour literals live only in ui/theme; scoped allowlist for photographic scrims/contrast only |
| 9 | Gallery manifest covers every shared control with at least one fixture |
scripts/ui-guard-allowlist.txt is path- and rule-specific and every entry
carries a reason. Never add a directory or a bare filename.
Strings
User-facing copy is resource-backed; per-feature string XML keeps review and translation scoped:
res/values/strings_home.xml,strings_activity.xml,strings_budgets.xml,strings_reports.xml,strings_editor.xml,strings_review.xml,strings_onboarding.xml,strings_components.xml; cross-feature shell, settings and dialog copy stays instrings.xml.- Add a new string to the file that owns the feature, not to a random screen. Plurals live beside their feature strings.
- Debug-only gallery labels live in
src/debug/res/values/strings.xmland never ship. contentDescriptionand other TalkBack copy are strings too; an amount marked unavailable must announce its localized sentence, not bare punctuation.
Checklist
- Reuse first. Compose the control from
ui/components/. If you need a new reusable control, add it there, map it in the gallery manifest and remove any copy at the call sites. - Use the roles. Colour emphasis and surfaces come from
AppSurface/AppTextRole(ADR-0069, ADR-0079) — grouping issurfaceContainerLow, the one full-bleed hero is Home, supporting copy isonSurfaceVariant. Expense amounts are neutral and never error red; theerrorfamily is for a real failure or an over-limit state only, and direction/state never rely on colour alone. - No truncation. A financial amount is never ellipsised, size-reduced,
k/M-abbreviated or scrolled to fit — it wraps or reflows (ADR-0009). - Cover the states. Add or update the control's
GalleryFixtureentries inGalleryInventory.kt(normal / selected / disabled / empty / error, long content, large amount) in the same change. Use only the states the control actually exposes. - Check the matrix. Light + dark, and narrow (320 dp) with large (up to 200 %) text at minimum — see testing.md.
- Update the one doc. If selection or behaviour changed, update
ui.md; if behaviour changed, add an ADR and its
one-line entries (
CONTEXT.md,docs/architecture/overview.md,zensical.toml). Changed user-facing behaviour also updates the relevant user-guide page. - Attach evidence. Settled captures with fixture metadata (API image, pixel size, density, actual dp window, font scale, locale, theme, navigation mode), labelled manual versus automated, plus the test/guard results. Do not re-enable a byte-identical screenshot gate until repeatability is demonstrated, and never fabricate an image when the capture tooling is unavailable.
- Run the guards.
bash scripts/ui-guard.shandbash scripts/docs-guard.shmust pass, plus the relevant unit/instrumented tests.
Related
- docs/architecture/ui.md — the design language, shared components, and the update/reuse checklist
- ADR-0079 — the Warm ledger palette, bundled fonts, type roles, spacing, shapes, motion and dynamic colour
- docs/developer/testing.md — how the design system is verified
- warm-ledger-verification.md — the committed suite and matrix evidence