16 KiB
Phase 2.1: App Shell, Navigation & Search Foundation - Context
Gathered: 2026-05-08 Status: Ready for planning
## Phase BoundaryReplace 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
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 isPlaner(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
PrzepisyandSpiżarnia(the two tabs that will have searchable content in v1).PlanerandZakupyhave 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 inui/components/. Theactionslot 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
GlassSurfacetoken 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:
GlassSurfaceis 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.Tabsemantics, 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_refs>
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 viakoinViewModel()).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 fallbackCLAUDE.md§ Module structure —composeApp/commonMainpackage layout (app/,navigation/,ui/{theme,components,screens/{recipes,planner,pantry,shopping}})CLAUDE.md§ Non-negotiable conventions — #8 (shared/commonMainlight), #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
@Serializableroutes, 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
</canonical_refs>
<code_context>
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 toLoginScreen/PostLoginPlaceholderScreenbased onAuthSessionstate.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 newui/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.ktmay 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 aSearchViewModelper searchable tab). - Koin module-per-feature —
AuthModule.kt,UserModule.kt. New shell addsNavigationModule.kt(or folds intoAppModule.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" — replacingPostLoginPlaceholderScreenas the destination of the auth gate transition inApp.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/(orui/theme/glass/). The dock, search pill, and floating action buttons all consume it. Future polish chrome (Phase 10) tunes here without touching call sites.
</code_context>
## 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-navis 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.
- 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