# Phase 2.1: App Shell, Navigation & Search Foundation - Context
**Gathered:** 2026-05-08
**Status:** Ready for planning
## Phase Boundary
Replace the post-login placeholder with the real app shell before household and domain data lands. Deliver four persistent top-level destinations (Planer, Przepisy, Spiżarnia, Zakupy) with independent per-tab back-stack boundaries, a Liquid-glass floating pill dock as the primary chrome, deliberate anticipatory empty states for every tab, and a functional search affordance (open/close + query echo only this phase) on Przepisy and Spiżarnia. Also introduce the first shared visual foundation built on Composables / Compose Unstyled + Liquid instead of expanding around Material 3 — including a full theme token scaffold (colors, typography, spacing, glass-surface) and a layered Liquid → Haze → flat fallback chain.
**Out of scope for this phase** (carried by later phases):
- Real search results or catalog data (Phase 5)
- Household onboarding / membership (Phase 3)
- SyncEngine wiring (Phase 4)
- Per-screen feature content beyond empty states (Phases 5–9)
- Real-device Liquid tuning + cross-screen polish (Phase 10)
- Full Polish copy pass and i18n delivery (Phase 11) — but all strings introduced in this phase MUST go through resource lookup, not hardcoded literals
## Implementation Decisions
### Tab bar shape & chrome placement
- **D-01:** Bottom-anchored floating pill dock implemented as a Liquid-glass capsule, centered above the safe-area inset. No edge-to-edge bottom bar.
- **D-02:** All four tabs render icon + label at all times (active and inactive). Active tab is wider and visually emphasized; inactive tabs remain readable, not icon-only.
- **D-03:** Tab order — `Planer` / `Przepisy` / `Spiżarnia` / `Zakupy`. Default landing tab on first sign-in is `Planer` (matches the "my week is planned" core value; departs from the literal UI-03 listing order, which research confirmed is non-binding).
- **D-04:** No top app bar in v1. Tab title (where useful) lives inline at the top of each screen body. All chrome is bottom-anchored — one surface to design well.
- **D-05:** When search is opened (on tabs that have search — see D-06), the dock collapses to a single circular button showing only the active tab's icon (no label, slightly reduced height). Tapping that collapsed button closes the search and re-expands the dock. The transition is a single coordinated animation, not two independent ones. This matches the Apple-app pattern the user explicitly endorsed.
### Search affordance behavior
- **D-06:** Search button is per-tab and only present on `Przepisy` and `Spiżarnia` (the two tabs that will have searchable content in v1). `Planer` and `Zakupy` have no search button and no search surface. The button renders as a separate floating circular icon adjacent to the dock (not inside it), matching the mockup.
- **D-07:** This phase delivers open/close, query input echo, and clear/close actions only. The body of the search surface renders nothing (no placeholder list, no empty-state body) — Phase 5 wires real result rendering for Przepisy, and the corresponding pantry phase wires Spiżarnia. UI-10 is satisfied by demonstrating the affordance is functional, not by faking content.
- **D-08:** Closing the search clears the query. Reopening starts blank. No persistence across close, tab-switch, or app launch.
- **D-09:** Search is an inline bottom pill, not a full-screen sheet. The search input expands across the bottom chrome row alongside the collapsed dock toggle (D-05). Body content stays visible behind it.
### Empty state design language
- **D-10:** Visual treatment is icon + headline + subline. Icon is tab-themed (calendar for Planer, book for Przepisy, warehouse for Spiżarnia, cart for Zakupy), rendered in a calm, low-saturation theme color. No bespoke illustrations in this phase.
- **D-11:** Tone is anticipatory in Polish — copy signals the feature is real but waiting (e.g. "Wkrótce zobaczysz tu swój plan tygodnia"). Avoid neutral "Brak danych" and avoid chatty onboarding copy.
- **D-12:** No CTA buttons in empty states this phase. Households and catalog don't exist yet, so any CTA would either no-op or navigate to another empty screen. CTAs are added in feature phases as actions become real.
- **D-13:** Single reusable `EmptyState(icon, title, subtitle, action?)` composable in `ui/components/`. The `action` slot is optional and unused this phase but reserved so feature phases can add CTAs without a new component.
### Theme tokens + Liquid fallback
- **D-14:** Full theme scaffold this phase — semantic color roles (background, surface, surfaceGlass, content, contentMuted, accent, separator, borderCard), a typography scale with named text styles (display/title/body/caption), a spacing scale (4/8/12/16/24/32), and a `GlassSurface` token primitive consumed by the dock, search pill, and search/filter buttons. Phase 5 inherits cleanly; Phase 10 tunes on real hardware.
- **D-15:** Both light and dark color schemes are defined and follow the system setting. UI-05 fully lands in Phase 5 but the foundation must be correct now so Phase 5 doesn't retrofit. The mockup's CSS palette (`--app-bg-rgb`, `--card-rgb`, `--sunken-rgb`, etc.) is a useful reference but is NOT directly ported — the visual rebuild owns its own palette.
- **D-16:** `GlassSurface` is a layered primitive with a Liquid → Haze → flat translucent fallback chain. All three paths consume the same token API (color + opacity + radius). Liquid is the preferred path for chrome/buttons; Haze is the secondary blur path; the flat path is a solid translucent surface using theme tokens for the worst case.
- **D-17:** Fallback engagement is compile-time per-target plus a runtime debug toggle. Compile-time: if Liquid does not compile or ship for a given target, the build picks the fallback at build time (no runtime guards in production binaries). Runtime: a debug-build-only toggle (via `multiplatform-settings`, surfaced through a hidden settings entry or build flag) lets the user switch GlassSurface between Liquid / Haze / flat to compare on-device. No automatic perf detection in v1 — Phase 10 may revisit.
### Claude's Discretion
- Exact Liquid library API usage and effect parameters (radius, blur amount, refraction strength) — to be researched against the Liquid library's current docs by gsd-phase-researcher
- Nav graph topology: single root NavHost vs nested NavHosts per tab. Recommendation in research SUMMARY.md is nested per tab for independent back stacks; planner should default to that unless research surfaces a CMP-specific blocker
- Whether to migrate the Phase 2 Material 3 auth screens to the new component foundation now or leave them as legacy until a later phase. Default: leave auth screens as-is; do not expand Material 3 into new code
- Specific empty-state copy strings (subject to Phase 11 copy pass; placeholders this phase must still go through resource lookup)
- Icon source — Compose Material Icons vs a calmer custom icon set. Default to Material Icons Outlined for v1 unless research surfaces a clearly better option that fits the Liquid aesthetic
- Animation curves and durations for the search-open dock collapse (D-05) — should feel iOS-native; planner can pick a reasonable default and Phase 10 tunes
- Accessibility specifics: tab bar `Role.Tab` semantics, search button label, focus order between collapsed dock and search input — pick reasonable defaults aligned with iOS VoiceOver expectations
- Whether to expose the runtime fallback toggle (D-17) as an in-app debug-build affordance or as a build flag only
## Canonical References
**Downstream agents MUST read these before planning or implementing.**
### Project source of truth
- `.planning/PROJECT.md` — Locked tech decisions; especially § Key Decisions (Components: Composables/Compose Unstyled; Glass: Liquid first, Haze fallback; Real app shell before household/domain work; Polish-only strings, i18n-ready)
- `.planning/REQUIREMENTS.md` § UI foundation — UI-01, UI-03, UI-04, UI-05, UI-09, UI-10 (UI-03 / UI-04 / UI-09 / UI-10 are the requirements this phase closes; UI-01 must be honored for any new strings; UI-05 lands in Phase 5 but tokens are scaffolded here)
- `.planning/ROADMAP.md` § Phase 2.1 — Goal, success criteria, requirements mapping
### Architecture & pitfalls research
- `.planning/research/SUMMARY.md` — Executive synthesis; especially § Architecture Approach (nested NavHosts per tab for independent back stacks, Koin scoping to NavBackStackEntry via `koinViewModel()`)
- `.planning/research/ARCHITECTURE.md` — Component structure (UI + Navigation layer), build-order reasoning
- `.planning/research/PITFALLS.md` — iOS infra hygiene (Pitfall 5: Liquid/Haze on chrome only, never over scrolling content; single ComposeUIViewController instance)
### Repository conventions
- `CLAUDE.md` § Tech stack (locked) — JetBrains Navigation Compose, Koin scoping, Compose Unstyled foundation, Liquid first / Haze fallback
- `CLAUDE.md` § Module structure — `composeApp/commonMain` package layout (`app/`, `navigation/`, `ui/{theme,components,screens/{recipes,planner,pantry,shopping}}`)
- `CLAUDE.md` § Non-negotiable conventions — #8 (`shared/commonMain` light), #9 (strings externalized day 1), #10 (Liquid/glass on chrome only)
### Functional reference (visual NOT carried forward; structural pattern IS)
- `~/dev/repo/recipe-mockup/js/ui/bottomNav.js` — Reference implementation of the floating pill dock: the active-tab-expand pattern, the collapse-to-single-button transition when search opens, tab order rationale (Planer first), tab-specific action button slots adjacent to the dock. Mine the structural pattern; do NOT port the CSS or animation timings literally
- `~/dev/repo/recipe-mockup/js/ui/recipeSearchField.js` — Reference for the inline search pill shape, placeholder/clear/filter slot semantics
- `~/dev/repo/recipe-mockup/index.html` — CSS for the bottom dock states (`is-collapsed-tab`, `is-nav-menu-open`, `is-inline-search-open`) is the reference for state machine transitions, not visual styling
### External library docs (for gsd-phase-researcher)
- JetBrains Navigation Compose: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-navigation.html — type-safe `@Serializable` routes, nested NavHost setup
- Koin Compose ViewModel: https://insert-koin.io/docs/reference/koin-compose/compose/ — `koinViewModel()` scoping with NavBackStackEntry
- Liquid (fletchmckee): https://github.com/fletchmckee/liquid — modifier-node pixel-sampling API for Compose Multiplatform; check current artifact ID and KMP target matrix
- Haze (chrisbanes): https://github.com/chrisbanes/haze — fallback blur primitive; check CMP/iOS support
## Existing Code Insights
### Reusable Assets
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/App.kt` — Current root composable; will host the new shell after auth gate. Currently routes to `LoginScreen` / `PostLoginPlaceholderScreen` based on `AuthSession` state.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/theme/RecipeTheme.kt` — Theme entry point exists but is minimal. This phase expands it into the full token scaffold (D-14, D-15).
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/di/AppModule.kt` — Koin app module; new screen ViewModels register here (or in a new `ui/UiModule.kt`).
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/PostLoginPlaceholderScreen.kt` — The placeholder this phase replaces. Should be retired (or reduced to a degenerate "Authenticating…" sliver) once the shell exists; `PostLoginViewModel.kt` may continue to drive the bridge.
- `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/auth/AuthSession.kt` — State machine the shell observes to decide whether to render auth flow or shell. No changes expected here; the shell sits downstream.
### Established Patterns
- ViewModel + StateFlow + method-per-action — every Phase 2 screen follows this; new shell screens MUST follow it (`PlannerViewModel`, `RecipesViewModel`, `PantryViewModel`, `ShoppingViewModel`, plus a `SearchViewModel` per searchable tab).
- Koin module-per-feature — `AuthModule.kt`, `UserModule.kt`. New shell adds `NavigationModule.kt` (or folds into `AppModule.kt`) and one ViewModel module per tab area.
- Strings externalized via Compose Resources — Phase 2 already established this; new shell must NOT introduce hardcoded literals (UI-01 / convention #9).
- Material 3 used in auth screens only — do NOT extend Material 3 into shell code; build new components on Compose Unstyled (PROJECT.md decision).
- iOS Kotlin/Native binary flags already set (`objcDisposeOnMain=false`, `gc=cms`) per Phase 1.
### Integration Points
- Auth gate: shell renders only when `AuthSession.state == Authenticated`. The shell becomes the new "authenticated root" — replacing `PostLoginPlaceholderScreen` as the destination of the auth gate transition in `App.kt`.
- Navigation: introduces `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/navigation/` package — root NavHost + per-tab nested NavHosts + serializable route definitions. Phase 3 (households) will hook onboarding into this graph; Phase 5 (catalog) will populate the Recipes nested graph.
- Theme tokens: every later phase reads these. Get the API right now — colors as semantic roles, not raw hex; typography as named styles, not raw `TextStyle`; spacing as named ints, not magic numbers.
- Search ViewModel surface: this phase delivers the open/close/query state machine for Recipes + Pantry search. Phase 5 plugs results in by injecting a search-results-source dependency into the same ViewModel — design the API for that injection point now.
- GlassSurface primitive: lives in `ui/components/` (or `ui/theme/glass/`). The dock, search pill, and floating action buttons all consume it. Future polish chrome (Phase 10) tunes here without touching call sites.
## Specific Ideas
- "When search bar is shown then from the menu only active button is visible and without label but then the whole is a little bit smaller in height" — verbatim user intent for the dock-collapse-on-search transition (D-05). The transition is a single coordinated motion, not two independent ones.
- "I've seen it in some Apple apps and I like it" — re: dock collapsing into a single button when search opens. Reference point is iOS native apps (Mail, Notes, Settings) where the bottom chrome morphs as the search context activates. The Liquid library's pixel-sampling capabilities are the right tool to make this feel native rather than mechanical.
- "All tabs show labels" — explicit departure from a typical iOS tab bar where inactive labels can be hidden. The user wants every tab readable at all times; the active tab differentiates by width and emphasis, not by being the only labeled one.
- The mockup's `app-bottom-nav` is the structural reference — a floating capsule with adjacent floating circular action buttons, not a flat edge-to-edge nav bar. Visual styling is being rebuilt; the floating-pill geometry and the "search open collapses the dock" state machine are what's being preserved.
## Deferred Ideas
- Per-tab dock collapse to a single button on certain tabs/scroll states (independent of search) — mockup has this for some views; defer to Phase 10 if real-device feel demands it. Not in scope here; this phase only collapses the dock for the search-open transition.
- Profile / settings entry point in chrome — no top bar this phase (D-04) means there's no obvious slot. Households/profile UI lands in Phase 3; revisit chrome placement then.
- Cross-tab CTAs in empty states (e.g. "Browse recipes" on empty Planer) — deferred until target tabs have content (Phase 5+).
- Custom illustrations for empty states — deferred; icon-based v1 (D-10).
- Material 3 migration of Phase 2 auth screens — leave as legacy; revisit when Phase 10 polishes chrome or when a phase touches login flow visually.
- Runtime perf detection that auto-downgrades GlassSurface — deferred to Phase 10. Compile-time + debug toggle is enough for v1 (D-17).
- Persisting search query across sessions — explicitly rejected (D-08). Per-tab session-level persistence is also out of scope.
- Real-device Liquid tuning (refraction strength, specular highlights, animation curves) — that's Phase 10's job; this phase ships a working approximation with sensible defaults.
- Localization (full Polish copy pass) — Phase 11. Strings introduced this phase go through resource lookup but the catalog of copy is not finalized.
---
*Phase: 02.1-app-shell-navigation-search-foundation*
*Context gathered: 2026-05-08*