--- phase: 02.1 plan: 07 subsystem: ui-shell tags: [kotlin, compose-multiplatform, empty-state, viewmodel, theme-tokens, accessibility, i18n, polish-copy] requires: [02.1-02, 02.1-04] provides: [EmptyState, PlannerScreen, RecipesScreen, PantryScreen, ShoppingScreen, PlannerViewModel, RecipesViewModel, PantryViewModel, ShoppingViewModel] affects: [] tech-stack: added: [] patterns: [statelfow-method-per-action, mergeDescendants-a11y, RecipeTheme-tokens] key-files: created: - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/empty/EmptyState.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/planner/PlannerScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/planner/PlannerViewModel.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipes/RecipesViewModel.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantryScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/pantry/PantryViewModel.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shopping/ShoppingScreen.kt - composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/shopping/ShoppingViewModel.kt modified: - composeApp/src/commonMain/composeResources/values/strings.xml decisions: - "Used `BasicText` from compose-foundation rather than Material 3 `Text` to keep shell components Material-3-free per UI-SPEC line 31" - "Tab screens render inline title + centered EmptyState; chrome bottom inset is owned by AppShell, not screens" - "All 4 tab VMs ship a marker `isEmpty` field for forward-compatible expansion in feature phases (5/6/8/9)" metrics: duration: ~10m completed: 2026-05-08 requirements: [UI-09] --- # Phase 02.1 Plan 07: Tab Empty States Summary UI-09 anticipatory empty states: a reusable `EmptyState(icon, title, subtitle, modifier, action?)` composable plus four tab screens (Planner / Recipes / Pantry / Shopping) each rendering an inline title and a centered EmptyState with calm Polish copy from UI-SPEC § Copywriting Contract. ## What was built - `EmptyState.kt` — reusable centered Column with 48dp muted icon, display headline, body subline, optional action slot, wrapped in `Modifier.semantics(mergeDescendants = true) {}` so VoiceOver reads the empty state as a single announcement (UI-SPEC line 226). - 4 tab `*Screen.kt` files — each `Box(background = RecipeTheme.colors.background)` containing a `Column` with status-bar inset + `xl` top padding, inline tab title in `RecipeTheme.typography.title`, and a centered `EmptyState` reading the tab-specific icon (from `BottomBarDestination..icon`) and resource strings. - 4 tab `*ViewModel.kt` files — each `ViewModel` exposes a `state: StateFlow<*State>` with a marker `isEmpty: Boolean = true` field; no actions in this phase. - `strings.xml` extended with 8 empty-state keys (Polish copy verbatim from UI-SPEC § Copywriting Contract). ## Tasks & commits | Task | Commit | Description | |------|---------|-------------| | 1 | 1cc4d9d | Add 8 empty-state strings (Polish copy) | | 2 | 98baed9 | Add reusable EmptyState composable | | 3 | fda8d2a | Add 4 tab ViewModels (StateFlow, no actions) | | 4 | c0ca16c | Add 4 tab screens with inline title + EmptyState | ## Verification - `./gradlew :composeApp:compileKotlinIosSimulatorArm64 -q` — exit 0 after each task - `./gradlew :composeApp:linkDebugFrameworkIosSimulatorArm64 -q` — exit 0 (only the pre-existing `bundleId` warning) - `./gradlew :composeApp:generateComposeResClass -q` — exit 0; new `Res.string.empty_*` accessors generated - Material 3 boundary preserved: `grep -rc 'androidx.compose.material3' [9 new files]` returns 0 - Zero hardcoded Polish literals in any *.kt — every string flows through `stringResource(Res.string.*)` ## Spacing accessor names verified `RecipeSpacing` exposes: `xs (4dp)`, `sm (8dp)`, `lg (16dp)`, `xl (24dp)`, `xxl (32dp)`, `xxxl (48dp)`. Per `RecipeSpacing.kt` comment: UI-SPEC's `2xl` / `3xl` are remapped to `xxl` / `xxxl` because Kotlin identifiers cannot start with a digit. This plan uses only `sm`, `lg`, `xl` — all plain identifiers, no backticks needed. ## strings.xml state after this plan - Total keys: **24** - Auth (pre-existing): 7 (`auth_*`) - Shell tabs (plan 02.1-04): 4 (`shell_tab_*`) - Search placeholders (plan 02.1-04): 2 (`search_placeholder_*`) - Search a11y (plan 02.1-04): 3 (`search_open_a11y`, `search_close_a11y`, `search_clear_a11y`) — verified each present exactly once - Empty-state (this plan): 8 (`empty_*_title` × 4 + `empty_*_subtitle` × 4) ## Deviations from Plan None — plan executed exactly as written. ## Self-Check: PASSED - All 9 created files exist (verified via Write tool success) - All 4 task commits present in git log (1cc4d9d, 98baed9, fda8d2a, c0ca16c) - Strings file modified with 8 new keys; total count 24 - iOS K/N compile + link green