Skip to content

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

  1. Change the token, not the screen. Palette entries and Warm literals live in Color.kt; named surface/type roles in Roles.kt; the two bundled families and every type role in Type.kt; spacing in Spacing.kt; corners in Shape.kt; durations, offsets and the single easing curve in Motion.kt.
  2. 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 in ui.md and ADR-0079. Screens never read a brand hex or a raw TextStyle.
  3. Add a step only when it repeats. Use Spacing.compact 12 dp for a dense row/card gap, Spacing.hero 20 dp for a hero interior and Spacing.sectionGap 24 dp before a SectionHeader. A one-off card interior may keep a local literal; a screen seam may not.
  4. 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 to tween; never write a new duration — scripts/ui-guard.sh fails a raw tween(<number>). Compose honours animator scale, so scale 0 settles immediately, and money is never interpolated.
  5. Update the contract tests with the token. WarmLedgerContractTest pins the schemes, type roles, tabular features, spacing, shapes and motion; RoleContractTest pins the named roles. A deliberate token change updates both the token and its test in the same change.

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 under ui/components/ must be mapped to a GalleryComponent, or listed in galleryExemptSources with 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 in strings.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.xml and never ship.
  • contentDescription and other TalkBack copy are strings too; an amount marked unavailable must announce its localized sentence, not bare punctuation.

Checklist

  1. 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.
  2. Use the roles. Colour emphasis and surfaces come from AppSurface / AppTextRole (ADR-0069, ADR-0079) — grouping is surfaceContainerLow, the one full-bleed hero is Home, supporting copy is onSurfaceVariant. Expense amounts are neutral and never error red; the error family is for a real failure or an over-limit state only, and direction/state never rely on colour alone.
  3. No truncation. A financial amount is never ellipsised, size-reduced, k/M-abbreviated or scrolled to fit — it wraps or reflows (ADR-0009).
  4. Cover the states. Add or update the control's GalleryFixture entries in GalleryInventory.kt (normal / selected / disabled / empty / error, long content, large amount) in the same change. Use only the states the control actually exposes.
  5. Check the matrix. Light + dark, and narrow (320 dp) with large (up to 200 %) text at minimum — see testing.md.
  6. 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.
  7. 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.
  8. Run the guards. bash scripts/ui-guard.sh and bash scripts/docs-guard.sh must pass, plus the relevant unit/instrumented tests.