Implement main app navigation
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
---
|
||||
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 1–3 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.
|
||||
Reference in New Issue
Block a user