Files

90 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 02.1
plan: 05
subsystem: ui-shell
tags: [kotlin, compose-multiplatform, shell, dock, viewmodel, glass, accessibility, navigation]
requires:
- 02.1-03 # GlassSurface + GlassBackdropSource
- 02.1-04 # BottomBarDestination + RootNavHost + navigateToTab + a11y string keys
- 02.1-06 # SearchPill + RecipesSearchViewModel + PantrySearchViewModel
provides:
- "ShellViewModel + ShellState (StateFlow + method-per-action)"
- "AppShell() — authenticated root composable"
- "DockBar() — collapsible 4-tab Liquid-glass dock"
- "FloatingSearchButton() — 44dp circular glass button"
affects:
- "Empty placeholder app target now has the destination composable for the post-auth shell (plan 02.1-08 wires it in)."
tech-stack:
added: []
patterns:
- "VM + StateFlow + method-per-action (mirrors LoginViewModel)"
- "animateContentSize + AnimatedContent single-block animation at 250ms FastOutSlowInEasing"
- "Type-safe NavBackStackEntry → BottomBarDestination derivation via hasRoute(*Graph::class)"
key-files:
created:
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shell/ShellViewModel.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShell.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/dock/DockBar.kt
- composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/dock/FloatingSearchButton.kt
modified: []
decisions:
- "ShellViewModel holds activeTab + searchOpen only; query state lives in per-tab Search VMs (RecipesSearchViewModel, PantrySearchViewModel) so Phase 5's extension hook stays connected to the UI."
- "DockBar uses Row + Modifier.semantics{role=Role.Tab; selected; contentDescription} (the UI-SPEC-line-180 'TabGroup-equivalent fallback') instead of Compose Unstyled's TabGroup primitive — the renderless TabGroup did not match the desired per-cell semantics shape; PATTERNS.md § DockBar line 326 explicitly accepts this path."
- "Active tab derivation uses type-safe hasRoute(*Graph::class) on the destination hierarchy — no string-route fallback was needed."
- "ShellViewModel ↔ NavHost sync uses a LaunchedEffect(activeTab) instead of an inline if-state-check, to avoid composition-side-effect pitfalls."
metrics:
completed: 2026-05-08
duration: ~25 minutes
---
# Phase 02.1 Plan 05: App Shell Composables Summary
Built the four core authenticated-shell composables — `ShellViewModel`, `AppShell`, `DockBar`, `FloatingSearchButton` — wiring RootNavHost (02.1-04) inside a GlassBackdropSource (02.1-03) and overlaying a bottom chrome column with the SearchPill (02.1-06), DockBar, and FloatingSearchButton.
## What Was Built
1. **ShellViewModel + ShellState** — pure synchronous state machine with three method-per-action signatures (`openSearch`, `closeSearch`, `onTabChanged`). State is `(activeTab, searchOpen)` only — no query field; per-tab query state lives in `RecipesSearchViewModel` / `PantrySearchViewModel`. Mirrors LoginViewModel's StateFlow shape.
2. **DockBar** — Liquid-glass capsule rendering 4 tabs (icon + label always shown, D-02) when expanded (28dp corner, 56dp tall) and collapsing to a single circular icon-only toggle on the active tab when search opens (22dp corner, 44dp tall, D-05). The collapse is one coordinated motion: `animateContentSize` on the GlassSurface modifier plus `AnimatedContent` with a fade `togetherWith` transition, both at 250ms FastOutSlowInEasing per UI-SPEC line 198. Each tab cell exposes `Role.Tab + selected + contentDescription` semantics; cells satisfy ≥44dp touch targets via `defaultMinSize(44dp, 44dp)`.
3. **FloatingSearchButton** — 44dp `GlassSurface(cornerRadius = 22.dp)` with `Icons.Outlined.Search` tinted `RecipeTheme.colors.content`. Carries `search_open_a11y` contentDescription. Visibility (only when `!searchOpen && activeTab.hasSearch`) is gated by AppShell, not the button itself.
4. **AppShell** — authenticated root composable. Wraps `RootNavHost` in `GlassBackdropSource` so Liquid/Haze backends sample the body through the shared `LocalGlassBackdropState`. Bottom chrome is a `Column` aligned `BottomCenter` with `windowInsetsPadding(WindowInsets.navigationBars) + imePadding()` only — no `safeContentPadding()` per Pitfall F. Conditionally renders a `SearchPill` wired to the active tab's SearchViewModel (Recipes or Pantry — both paths covered) above the always-present `DockBar`. The `FloatingSearchButton` is overlaid at `BottomEnd`. Active-tab tracking derives from `NavBackStackEntry.destination.hierarchy` via type-safe `hasRoute(*Graph::class)`; a `LaunchedEffect(activeTab)` keeps `ShellViewModel.activeTab` in sync for back-button and deep-link cases. Tab selection navigates via `navigateToTab(dest.graphRoute)` and notifies `vm.onTabChanged(dest)`.
## Plan Output Questions Answered
- **Both Recipes and Pantry SearchViewModel paths covered?** Yes — `AppShell` has explicit `when (activeTab)` branches for both `BottomBarDestination.Recipes` and `BottomBarDestination.Pantry` for SearchPill rendering and FloatingSearchButton onClick. `Planner` and `Shopping` are no-ops because `hasSearch = false` already gates the surfaces.
- **Compose Unstyled TabGroup vs Row + semantics?** Used the `Row + Modifier.semantics { role = Role.Tab; selected; contentDescription }` fallback per UI-SPEC line 180 / PATTERNS.md § DockBar line 326. The renderless TabGroup did not offer a cleaner per-cell shape than direct semantics modifiers.
- **`hasRoute(*Graph::class)` worked?** Yes — nav-compose 2.9.2 exposes `NavDestination.Companion.hasRoute` and `NavDestination.Companion.hierarchy`. No string-route fallback was needed; iOS K/N compile + linkDebugFrameworkIosSimulatorArm64 both green.
- **Touch targets:** Code-level confirmation — DockTabCell uses `defaultMinSize(minWidth = 44.dp, minHeight = 44.dp)`, CollapsedDockToggle is `size(44.dp)`, FloatingSearchButton is `size(44.dp)`. Visual sim confirmation deferred to plan 02.1-08's manual smoke (V-09 / V-11 in VALIDATION.md).
## Deviations from Plan
None — plan executed exactly as written. The plan's "Implementation note 1" pre-flagged an invalid `clickableNoRipple` sketch and recommended the `MutableInteractionSource + clickable(indication = null)` pattern, which is what the final code uses inline at each click site. No autoFix Rules 13 needed.
## Verification
- `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` → exits 0 (silent).
- `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` → exits 0 (only an unrelated bundle-ID warning).
- Material 3 boundary preserved: zero `androidx.compose.material3` imports in any of the 4 new files.
- Direct Liquid / Haze imports zero in `ui/screens/shell/` and `ui/components/dock/`.
- `safeContentPadding()` not present in AppShell.
## Commits
| Task | Commit | Files |
| --- | --- | --- |
| 1 — ShellViewModel | `5e0aaf9` | ShellViewModel.kt |
| 2 — DockBar + FloatingSearchButton | `78bb90d` | DockBar.kt, FloatingSearchButton.kt |
| 3 — AppShell | `fb4301e` | AppShell.kt |
## Self-Check: PASSED
- Files exist:
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shell/ShellViewModel.kt
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShell.kt
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/dock/DockBar.kt
- FOUND: composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/dock/FloatingSearchButton.kt
- Commits exist: FOUND 5e0aaf9, 78bb90d, fb4301e.
- iOS compile + link both green.