Implement main app navigation

This commit is contained in:
2026-05-08 14:03:26 +02:00
parent f7e866a08d
commit 794e27c554
90 changed files with 11725 additions and 187 deletions

View File

@@ -0,0 +1,390 @@
---
phase: 02.1
plan: 01
type: execute
wave: 0
depends_on: []
files_modified:
- gradle/libs.versions.toml
- composeApp/build.gradle.kts
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt
autonomous: true
requirements: [UI-03, UI-04, UI-09, UI-10]
tags: [kotlin, kmp, compose-multiplatform, gradle, navigation, liquid, haze, compose-unstyled, wave-0]
must_haves:
truths:
- "navigation-compose 2.9.2, compose-unstyled 1.49.9, liquid 1.1.1, haze 1.6.10 resolve cleanly for iosArm64 and iosSimulatorArm64"
- "Material Icons Outlined (CalendarMonth, MenuBook, Inventory2, ShoppingCart, Search) compile from accessible package"
- "Wave 0 test stub files exist with @Test functions in @Ignore state for V-01..V-07 anchors"
artifacts:
- path: "gradle/libs.versions.toml"
provides: "version catalog entries: navigation-compose, compose-unstyled, liquid, haze, compose-material-icons-extended (if needed)"
contains: "navigation-compose = \"2.9.2\""
- path: "composeApp/build.gradle.kts"
provides: "commonMain dependencies wired"
contains: "libs.navigation.compose"
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt"
provides: "V-01 test stub"
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt"
provides: "V-02 test stub"
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt"
provides: "V-03 test stub"
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt"
provides: "V-04 test stub"
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt"
provides: "V-05/V-06 test stubs"
- path: "composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt"
provides: "V-07 test stub"
key_links:
- from: "composeApp/build.gradle.kts"
to: "gradle/libs.versions.toml"
via: "libs.navigation.compose / libs.compose.unstyled / libs.liquid / libs.haze references"
pattern: "libs\\.(navigation\\.compose|compose\\.unstyled|liquid|haze)"
---
<objective>
Wave 0 — verify the three load-bearing assumptions (A1: Liquid iOS klibs resolve; A2: Material Icons Outlined available; A3: nav-compose 2.9.2 K/N back-stack save/restore), add the four new dependencies to the version catalog and `composeApp` build, and land the six commonTest stub files referenced by VALIDATION.md so V-01..V-07 anchors have target locations from day 1.
Purpose: De-risk the rest of the phase. If A1 fails, the GlassSurface backend default flips to Haze before any UI code is written; if A2 fails, `material-icons-extended` is added before screens reference icons.
Output: Updated `libs.versions.toml`, updated `composeApp/build.gradle.kts`, six failing-but-compiling test stubs.
</objective>
<execution_context>
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
@$HOME/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-CONTEXT.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-VALIDATION.md
@.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md
@gradle/libs.versions.toml
@composeApp/build.gradle.kts
<interfaces>
Existing test analog (LoginViewModelTest.kt — pattern shape only):
```kotlin
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest
class XxxTest {
@Test
fun behaviorName() = runTest {
// arrange
// act
// assert
}
}
```
Existing libs.versions.toml relevant entries (already present):
- composeMultiplatform = "1.10.3"
- material3 = "1.10.0-alpha05"
- multiplatformSettings = "1.3.0"
- compose-components-resources, compose-foundation, compose-runtime, compose-ui already wired.
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Add nav-compose / compose-unstyled / liquid / haze (and material-icons-extended if needed) to version catalog and composeApp build</name>
<files>gradle/libs.versions.toml, composeApp/build.gradle.kts</files>
<read_first>
- gradle/libs.versions.toml (current state — append-only edits; preserve all existing entries verbatim)
- composeApp/build.gradle.kts (current state — locate the `commonMain.dependencies { ... }` block)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Standard Stack (lines 117-178; locked coordinates and versions)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Pitfall D (Material Icons availability — lines 461-465)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § `gradle/libs.versions.toml` (modified)
</read_first>
<action>
Edit `gradle/libs.versions.toml`:
1. Append to `[versions]` block (after the existing `multiplatformSettings = "1.3.0"` line, preserving alphabetical/grouping conventions seen in the file):
```toml
navigation-compose = "2.9.2"
compose-unstyled = "1.49.9"
liquid = "1.1.1"
haze = "1.6.10"
```
2. Append to `[libraries]` block (after the existing Phase 2 client block, separated by a comment header `# Phase 2.1 — App shell foundation (UI-03, UI-04, UI-09, UI-10)`):
```toml
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
compose-unstyled = { module = "com.composables:composeunstyled", version.ref = "compose-unstyled" }
liquid = { module = "io.github.fletchmckee.liquid:liquid", version.ref = "liquid" }
haze = { module = "dev.chrisbanes.haze:haze", version.ref = "haze" }
```
Edit `composeApp/build.gradle.kts`:
3. Inside the `commonMain.dependencies { ... }` block (locate by grep), append (after existing `implementation(libs.multiplatform.settings)` line, or after the last existing `implementation(...)` in commonMain):
```kotlin
implementation(libs.navigation.compose)
implementation(libs.compose.unstyled)
implementation(libs.liquid)
implementation(libs.haze)
```
4. Verify A2 — Material Icons Outlined availability. Run a quick Gradle resolution probe:
```bash
./gradlew :composeApp:dependencies --configuration commonMainImplementation 2>&1 | grep -E "(material-icons-extended|material3)" | head -20
```
The four icons referenced in UI-SPEC (`Icons.Outlined.CalendarMonth`, `MenuBook`, `Inventory2`, `ShoppingCart`) are NOT in the baseline icon set. They live in `material-icons-extended`. Add to catalog (per RESEARCH § Pitfall D):
```toml
# in [versions]:
compose-material-icons-extended = "1.7.3"
# in [libraries]:
compose-material-icons-extended = { module = "org.jetbrains.compose.material:material-icons-extended", version.ref = "compose-material-icons-extended" }
```
And in `composeApp/build.gradle.kts` `commonMain.dependencies`:
```kotlin
implementation(libs.compose.material.icons.extended)
```
Use the kebab-style alias-to-Kotlin-camel-case convention already in use (e.g. `multiplatform-settings` → `libs.multiplatform.settings`).
Do NOT modify any existing entries. Preserve all comments. Append only.
</action>
<verify>
<automated>count="$(./gradlew :composeApp:dependencies --configuration iosSimulatorArm64MainResolvableDependenciesMetadata 2>&1 | grep -E "(navigation-compose:2\\.9\\.2|composeunstyled:1\\.49\\.9|liquid:1\\.1\\.1|haze:1\\.6\\.10|material-icons-extended)" | wc -l | tr -d ' ')"; test "$count" -ge 5</automated>
</verify>
<acceptance_criteria>
- `grep -c '^navigation-compose = ' gradle/libs.versions.toml` returns 1 (the version entry)
- `grep -c 'navigation-compose:navigation-compose' gradle/libs.versions.toml` returns 1
- `grep -c 'composables:composeunstyled' gradle/libs.versions.toml` returns 1
- `grep -c 'fletchmckee.liquid:liquid' gradle/libs.versions.toml` returns 1
- `grep -c 'chrisbanes.haze:haze' gradle/libs.versions.toml` returns 1
- `grep -c 'material-icons-extended' gradle/libs.versions.toml` returns at least 1 (version + library = 2)
- `grep -c 'libs.navigation.compose' composeApp/build.gradle.kts` returns at least 1
- `grep -c 'libs.compose.unstyled' composeApp/build.gradle.kts` returns at least 1
- `grep -c 'libs.liquid' composeApp/build.gradle.kts` returns at least 1
- `grep -c 'libs.haze' composeApp/build.gradle.kts` returns at least 1
- `./gradlew :composeApp:help -q` exits 0 (catalog parses without error)
- `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` exits 0 (A1 + A3 verified by successful K/N link with new dependencies on classpath)
- All pre-existing `[versions]` and `[libraries]` keys are still present (`grep -c '^kotlin = ' gradle/libs.versions.toml` returns 1; `grep -c '^lokksmith-compose' gradle/libs.versions.toml` returns 1)
</acceptance_criteria>
<done>Version catalog declares the four new libraries (plus material-icons-extended) at the exact pinned versions; composeApp/build.gradle.kts wires them into commonMain; the iOS simulator framework links cleanly, proving A1 (Liquid iOS klibs resolve) and A3 (nav-compose 2.9.2 K/N classpath OK).</done>
</task>
<task type="auto">
<name>Task 2: Land six failing test stubs for V-01..V-07 anchors</name>
<files>
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt,
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt,
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt,
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt,
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt,
composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt
</files>
<read_first>
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/LoginViewModelTest.kt (analog — `runTest`/`@Test`/`assertEquals` skeleton)
- composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/auth/AuthSessionTest.kt (analog — state-flow gate test shape)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-VALIDATION.md § Wave 0 Requirements (locked file paths and anchor coverage)
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-RESEARCH.md § Validation Architecture lines 715-755
- .planning/phases/02.1-app-shell-navigation-search-foundation/02.1-PATTERNS.md § Test files (new) lines 386-415
</read_first>
<action>
Create six commonTest files. Each contains compiling test scaffolds that reference yet-to-be-created production types via `@Ignore`d test bodies (so the test compiles but does not yet pass — Wave 0 produces the targets, later waves implement and un-ignore). Use `kotlin.test` (`org.junit.*` is forbidden; `kotlin.test` only — matches Phase 2 convention).
File 1 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt`:
```kotlin
package dev.ulfrx.recipe.navigation
import kotlin.test.Ignore
import kotlin.test.Test
/**
* V-01 — UI-03 — `navigateToTab()` extension applies
* popUpTo(graph.findStartDestination().id) { saveState = true }; launchSingleTop = true; restoreState = true.
* Implemented in plan 02.1-04 (RootNavHost / Routes).
*/
class NavigationTest {
@Test
@Ignore
fun navigateToTab_appliesPopUpToWithSaveState() {
// TODO(02.1-04): assert NavOptionsBuilder lambda flips popUpToId+saveState=true,
// launchSingleTop=true, restoreState=true. Use TestNavHostController if available
// in CMP commonTest; else capture a fake NavOptionsBuilder.
}
}
```
File 2 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt`:
```kotlin
package dev.ulfrx.recipe.ui.components.glass
import kotlin.test.Ignore
import kotlin.test.Test
/**
* V-02 — UI-04 — `resolveGlassBackend(...)` returns Liquid for iOS source-set defaults
* with no debug override. Implemented in plan 02.1-03 (GlassSurface).
*/
class GlassBackendTest {
@Test
@Ignore
fun resolveGlassBackend_iosDefault_returnsLiquid() {
// TODO(02.1-03): assert resolveGlassBackend(settings = MapSettings(), isDebug = false,
// default = GlassBackend.Liquid) == GlassBackend.Liquid
}
}
```
File 3 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt`:
```kotlin
package dev.ulfrx.recipe.ui.components.glass
import kotlin.test.Ignore
import kotlin.test.Test
/**
* V-03 — UI-04 — debug-build runtime override via multiplatform-settings honors
* "debug.glass_backend" key with values "liquid" / "haze" / "flat".
* Implemented in plan 02.1-03 (GlassSurface).
*/
class GlassBackendOverrideTest {
@Test
@Ignore
fun resolveGlassBackend_debugBuildHonorsSettingsOverride() {
// TODO(02.1-03): use com.russhwolf.settings.MapSettings, set
// "debug.glass_backend" = "haze", isDebug = true, assert returns Haze.
}
@Test
@Ignore
fun resolveGlassBackend_productionBuildIgnoresSettingsOverride() {
// TODO(02.1-03): same map but isDebug = false → returns the compile-time default.
}
}
```
File 4 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt`:
```kotlin
package dev.ulfrx.recipe.ui.screens.shell
import kotlin.test.Ignore
import kotlin.test.Test
/**
* V-04 — UI-09 — App.kt's Authenticated + currentUser != null branch resolves to AppShell,
* not PostLoginPlaceholderScreen. Implemented in plan 02.1-08 (App.kt wire-up).
*
* Style: mirror AuthSessionTest.kt — runTest + state-flow assertion + Koin test container.
*/
class AppShellGateTest {
@Test
@Ignore
fun authenticatedWithUser_routesToAppShell_notPlaceholder() {
// TODO(02.1-08): drive AuthSession through Authenticated state with a non-null currentUser
// and assert the App() composable selects the AppShell branch (via a probe-flag injected
// into the composition or via a refactored RootRouter pure function).
}
}
```
File 5 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt`:
```kotlin
package dev.ulfrx.recipe.ui.screens.recipes
import kotlin.test.Ignore
import kotlin.test.Test
/**
* V-05/V-06 — UI-10 — RecipesSearchViewModel state machine semantics.
* Implemented in plan 02.1-07 (Search foundation).
*/
class RecipesSearchViewModelTest {
@Test
@Ignore
fun openThenQueryChangeThenClose_clearsQueryAndResetsIsOpen() {
// V-05: TODO(02.1-07) — open() → onQueryChange("foo") → close() leaves
// state = SearchState(isOpen = false, query = "")
}
@Test
@Ignore
fun clear_resetsQueryButKeepsIsOpenTrue() {
// V-06: TODO(02.1-07) — open() → onQueryChange("foo") → clear() leaves
// state = SearchState(isOpen = true, query = "")
}
}
```
File 6 — `composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt`:
```kotlin
package dev.ulfrx.recipe.ui.screens.pantry
import kotlin.test.Ignore
import kotlin.test.Test
/**
* V-07 — UI-10 — PantrySearchViewModel parity with RecipesSearchViewModel
* (open/close/clear semantics). Implemented in plan 02.1-07 (Search foundation).
*/
class PantrySearchViewModelTest {
@Test
@Ignore
fun openThenQueryChangeThenClose_clearsQueryAndResetsIsOpen() {
// V-07: TODO(02.1-07) — same semantics as RecipesSearchViewModelTest.
}
@Test
@Ignore
fun clear_resetsQueryButKeepsIsOpenTrue() {
// V-07: TODO(02.1-07).
}
}
```
All six files use `kotlin.test.Test` + `kotlin.test.Ignore` only — no library types referenced (so they compile without depending on yet-to-be-created production code).
</action>
<verify>
<automated>./gradlew :composeApp:compileTestKotlinIosSimulatorArm64 -q</automated>
</verify>
<acceptance_criteria>
- `find composeApp/src/commonTest -name '*.kt' -path '*/navigation/NavigationTest.kt' -o -path '*/glass/GlassBackendTest.kt' -o -path '*/glass/GlassBackendOverrideTest.kt' -o -path '*/shell/AppShellGateTest.kt' -o -path '*/recipes/RecipesSearchViewModelTest.kt' -o -path '*/pantry/PantrySearchViewModelTest.kt' | wc -l` returns 6
- `grep -l 'V-01' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/navigation/NavigationTest.kt` matches
- `grep -l 'V-02' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendTest.kt` matches
- `grep -l 'V-03' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/components/glass/GlassBackendOverrideTest.kt` matches
- `grep -l 'V-04' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` matches
- `grep -l 'V-05' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt` matches
- `grep -l 'V-07' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantrySearchViewModelTest.kt` matches
- `grep -c '@Ignore' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesSearchViewModelTest.kt` returns 2
- `./gradlew :composeApp:commonTest -q` exits 0 (no failures because all tests are `@Ignore`d)
- No file imports `androidx.compose.material3` (Material 3 boundary): `grep -c 'material3' composeApp/src/commonTest/kotlin/dev/ulfrx/recipe/ui/screens/shell/AppShellGateTest.kt` returns 0
</acceptance_criteria>
<done>Six test files exist under `commonTest/`, each compiles, each contains @Ignore'd @Test functions referencing the V-anchor it covers, commonTest run is green (no real assertions yet — production code lands in subsequent waves).</done>
</task>
</tasks>
<verification>
- Catalog parses: `./gradlew :composeApp:help -q` exits 0
- iOS framework links: `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` exits 0 (proves A1 + A3)
- commonTest compiles + green: `./gradlew :composeApp:commonTest -q` exits 0
- All 6 test files exist at the exact paths listed in VALIDATION.md § Wave 0 Requirements
</verification>
<success_criteria>
1. nav-compose 2.9.2 + compose-unstyled 1.49.9 + liquid 1.1.1 + haze 1.6.10 + material-icons-extended 1.7.3 declared in `gradle/libs.versions.toml` and wired into `composeApp/build.gradle.kts` commonMain.
2. iOS simulator K/N framework links successfully (assumptions A1 and A3 confirmed).
3. Material Icons Outlined for the five icons used by this phase (CalendarMonth, MenuBook, Inventory2, ShoppingCart, Search) are reachable through the new `compose-material-icons-extended` artifact (assumption A2 resolved via preemptive add per RESEARCH § Open Question 2 recommendation).
4. Six commonTest stub files exist at the exact paths specified in VALIDATION.md § Wave 0 Requirements; all contain @Ignore'd @Test functions referencing their V-anchor IDs.
5. `./gradlew :composeApp:commonTest` exits green.
</success_criteria>
<output>
After completion, create `.planning/phases/02.1-app-shell-navigation-search-foundation/02.1-01-SUMMARY.md` per `$HOME/.claude/get-shit-done/templates/summary.md`. Record the exact resolved versions of nav-compose, compose-unstyled, liquid, haze, and material-icons-extended (from `./gradlew dependencies` output) so subsequent plans can reference verified coordinates.
</output>