---
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
