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.