--- phase: 02.1 plan: 06 subsystem: ui-search tags: [kotlin, compose-multiplatform, search, viewmodel, glass, accessibility, phase-5-extension-hook] requires: - 02.1-03 # GlassSurface - 02.1-04 # search_clear_a11y / search_close_a11y resource keys provides: - RecipesSearchViewModel (open/close/onQueryChange/clear) - PantrySearchViewModel (open/close/onQueryChange/clear) - SearchState data class - SearchSource placeholder interface - SearchPill composable (44dp inline pill on GlassSurface) affects: - 02.1-05 # AppShell consumes SearchPill + Search VMs - 02.1-08 # ShellModule registers VMs in Koin tech-stack: 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)" key-files: created: - 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 modified: - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt - composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt decisions: - "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." metrics: tasks-completed: 3 files-created: 3 files-modified: 2 completed-date: 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