Files

7.4 KiB
Raw Permalink Blame History

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 05 ui-shell
kotlin
compose-multiplatform
shell
dock
viewmodel
glass
accessibility
navigation
02.1-03
02.1-04
02.1-06
ShellViewModel + ShellState (StateFlow + method-per-action)
AppShell() — authenticated root composable
DockBar() — collapsible 4-tab Liquid-glass dock
FloatingSearchButton() — 44dp circular glass button
Empty placeholder app target now has the destination composable for the post-auth shell (plan 02.1-08 wires it in).
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)
created modified
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
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.
completed duration
2026-05-08 ~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.