---
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.<Tab>.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
