Skip to content

ADR-0064: One shared search field

  • Status: Accepted
  • Date: 2026-10-17

Context

The app had two search fields that shared nothing. The Activity screen built a Surface(shape = extraLarge, color = surfaceContainer) pill with a leading Search icon, an inline placeholder, a 48 dp minimum height and a clear action; the Settings index built an OutlinedTextField whose label floated above the value (it was the placeholder), reading taller and emptier, with no clear action (issue #172). Same purpose, different shape, colour, iconography and affordance.

Decision

ui/components/SearchField.kt is the app's one search field: a rounded surfaceContainer pill (MaterialTheme.shapes.extraLarge), a leading Icons.Default.Search, a single-line inline placeholder (never a floating label), a 48 dp minimum height and a clear (Icons.Default.Clear) action once there is text. It owns the whole decoration box; callers pass value, onValueChange, the placeholder copy, a stable modifier (with a screen test tag) and an optional accessible contentDescription. Both the Activity screen (activity_search) and the Settings index (settings_search) render it, so the two cannot drift; local filtering semantics stay in each ViewModel. Screens never build a second copy (ADR-0065).

Consequences

Both search fields look and behave identically. Editing the pill once updates both places. The Settings field drops its floating label, so it is the same height as the Activity field and gains the clear action.

Verification

SearchFieldTest asserts the shared affordances (leading icon, inline placeholder, no clear when empty, clear once typed and emits empty when cleared, 48 dp minimum). SettingsIndexTest and TransactionsContentTest cover the two call sites' filtering; ActivityControlsLayoutTest keeps the Activity field's placeholder and touch-target geometry green.