Files

6.2 KiB

phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, decisions, metrics
phase plan subsystem tags requires provides affects tech-stack key-files decisions metrics
02.1 06 ui-search
kotlin
compose-multiplatform
search
viewmodel
glass
accessibility
phase-5-extension-hook
02.1-03
02.1-04
RecipesSearchViewModel (open/close/onQueryChange/clear)
PantrySearchViewModel (open/close/onQueryChange/clear)
SearchState data class
SearchSource placeholder interface
SearchPill composable (44dp inline pill on GlassSurface)
02.1-05
02.1-08
added patterns
RESEARCH § Pattern 4: per-tab Search VM with SearchState(isOpen, query)
Phase 5/8 extension hook: nullable SearchSource constructor parameter
BasicTextField as renderless TextField primitive (Compose Unstyled fallback)
created modified
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModel.kt
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModel.kt
composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/search/SearchPill.kt
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt
Used BasicTextField from compose-foundation rather than Compose Unstyled TextField — BasicTextField is already on the classpath, is renderless (no Material 3 chrome), and provides equivalent IME/a11y plumbing. Compose Unstyled was the originally specified primitive but adds no value here.
SearchState and SearchSource live in ui.screens.recipes package; PantrySearchViewModel imports them. Single source of truth prevents drift between the two VMs.
SearchPill's clear and close icons both use Icons.Outlined.Close glyph; UI-SPEC accessibility distinguishes via contentDescription only. Distinct glyphs deferred to Phase 10 polish.
tasks-completed files-created files-modified completed-date
3 3 2 2026-05-08

Phase 02.1 Plan 06: Search Foundation Summary

Per-tab Search ViewModels (Recipes + Pantry) with locked SearchState shape and SearchPill composable rendering a 44dp inline GlassSurface pill — search affordance functional before catalog data exists (UI-10).

What Was Built

  • SearchState(isOpen, query) data class + SearchSource placeholder interface in ui.screens.recipes.
  • RecipesSearchViewModel and PantrySearchViewModel: identical 4-action API (open, close, onQueryChange, clear). close() clears query (D-08); clear() preserves isOpen (D-07). Both accept nullable searchSource: SearchSource? = null for Phase 5/8 dependency injection without VM refactor.
  • SearchPill: 44dp-height pill on GlassSurface(cornerRadius = 22.dp), leading search icon + BasicTextField query input + conditional clear button (visible only when query.isNotEmpty()) + always-visible close button. A11y descriptions resolved from search_clear_a11y / search_close_a11y.
  • Replaced @Ignore stubs in RecipesSearchViewModelTest (5 cases — V-05 + V-06 + edge cases) and PantrySearchViewModelTest (3 cases — V-07 parity).

Output Spec Answers

  • Compose Unstyled TextField vs BasicTextField: Used BasicTextField from compose-foundation. It is renderless, already on the classpath, and provides the IME/a11y plumbing the pill needs. Compose Unstyled TextField would have added a dependency surface for no gain in this phase.
  • Resource keys: search_clear_a11y and search_close_a11y were both present in composeResources/values/strings.xml from plan 02.1-04 before SearchPill compilation (verified via grep -c returning 2).
  • SearchSource placement: Declared in ui.screens.recipes as planned. PantrySearchViewModel imports it (alongside SearchState) to keep a single canonical shape.
  • AppShell handoff (02.1-05): AppShell from plan 02.1-05 was already shipped before this plan; on inspection it stubs the search affordance internally. AppShell will be rewired to consume this plan's SearchPill + per-tab Search ViewModels in plan 02.1-08 (ShellModule wiring) — that's the natural integration point because Koin registration of the new VMs happens there. No regression: SearchPill + VMs are pure additions; nothing in AppShell breaks.

Verification

  • ./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q → exit 0.
  • ./gradlew :composeApp:iosSimulatorArm64Test --tests "...RecipesSearchViewModelTest" --tests "...PantrySearchViewModelTest" -q → exit 0; all 8 cases pass.
  • Material 3 boundary: 0 androidx.compose.material3 imports across the 3 new commonMain files.
  • Liquid / Haze imports: 0 across the new search package and search VMs.

Deviations from Plan

None substantive. Two minor cosmetic deviations:

  1. The plan's example code referenced an internal helper named BasicTextWithStyle defined to call BasicText. Renamed to PlaceholderText and imported BasicText directly at top-level for cleaner reading — semantics unchanged.
  2. The plan's import list included KeyboardOptions, KeyboardCapitalization, and ImeAction, but the spec'd implementation does not actually use them (no keyboardOptions = ... argument is set on BasicTextField). Omitted to keep the import list honest. If future work configures the keyboard explicitly, those imports come back.

Self-Check: PASSED

Verified files and commits exist:

  • composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModel.kt — FOUND
  • composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModel.kt — FOUND
  • composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/search/SearchPill.kt — FOUND
  • composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt — FOUND (no @Ignore)
  • composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt — FOUND (no @Ignore)

Commits:

  • d40aeef feat(02.1-06): add per-tab search ViewModels
  • 9c193d7 feat(02.1-06): add SearchPill inline search input
  • b8100cb test(02.1-06): assert search VM state-machine semantics