Files
recipe/.planning/phases/02-authentication-foundation/02-UI-SPEC.md
ulfrxdev 6ab7960e16 docs(02): approve UI design contract
UI-SPEC verified — all 6 dimensions PASS. No flags. Frontmatter
status flipped from draft to approved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 20:02:19 +02:00

303 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 2
slug: authentication-foundation
status: approved
shadcn_initialized: false
preset: none
created: 2026-04-27
reviewed_at: 2026-04-27
---
# Phase 2 — UI Design Contract
> Visual and interaction contract for the Authentication Foundation phase. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Stack note:** This is a Kotlin Multiplatform / Compose Multiplatform app, not shadcn/web. The template's "shadcn" framing has been adapted for Material 3 on CMP. All values below are expressed in `dp` (Compose) and Material 3 `Type` / `ColorScheme` roles.
>
> **Phase boundary reminder:** Phase 2 ships SCAFFOLD UI quality — three composables (`SplashScreen`, `LoginScreen`, `PostLoginPlaceholderScreen`) plus the auth gate in `App()`. The Liquid-Glass visual language and Haze blur land in **Phase 10**. The polished Polish copy + display font live in **Phase 11**. Tokens locked here are the SEED that later phases extend, not retroactively rewrite.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | Compose Multiplatform Material 3 |
| Preset | not applicable |
| Component library | `androidx.compose.material3` (CMP port via `compose-multiplatform` 1.7+) |
| Icon library | none used in Phase 2 (no icons on Splash / Login / PostLoginPlaceholder); `androidx.compose.material.icons` available but deferred to Phase 5+ |
| Font | system default — `FontFamily.Default` (Compose Multiplatform resolves to SF on iOS, Roboto on Android, system default on JVM/Wasm). **Reserved for Phase 11:** display font selection + custom `FontFamily` in Compose Resources. |
**Component sourcing:** Material 3 stdlib only (`Surface`, `Text`, `Button`, `OutlinedButton`, `CircularProgressIndicator`, `Column`, `Spacer`, `Box`). No third-party UI components, no Haze, no custom blur. **Haze blur is explicitly deferred to Phase 10** per CLAUDE.md non-negotiable #10 ("Haze on chrome only, never over fast-scrolling content").
---
## Spacing Scale
Declared values (all multiples of 4, expressed in Compose `dp`):
| Token | Value | Phase 2 Usage |
|-------|-------|---------------|
| xs | 4.dp | Reserved for later phases (icon gaps, fine adjustments) |
| sm | 8.dp | Vertical gap between welcome text and "Wyloguj się" button on PostLogin; vertical gap between "Recipe" wordmark and progress indicator on Splash |
| md | 16.dp | Default vertical gap between button and inline error text on LoginScreen; horizontal screen-edge padding on all three screens |
| lg | 24.dp | Vertical gap between app-name display and primary button on LoginScreen; vertical gap between welcome text block and logout button on PostLoginPlaceholder |
| xl | 32.dp | Reserved for later phases (planner/calendar layouts) |
| 2xl | 48.dp | Vertical breathing room between top-most centered content cluster and its surrounding empty space (visual centering target on LoginScreen and Splash) |
| 3xl | 64.dp | Reserved for later phases (page-level section breaks in catalog / planner) |
**Touch target floor:** All interactive controls (buttons) honor Material 3 minimum touch target of `48.dp` height. Material 3's `Button` defaults satisfy this; do not shrink.
**Safe-area insets:** All three screens wrap their root in `Modifier.safeContentPadding()` (already established by Phase 1's `App.kt` pattern). This keeps content clear of the iOS notch/home indicator and Android system bars without introducing platform-specific code in `commonMain`.
**Exceptions:** none. The full `xs..3xl` scale is declared for forward-compat with Phases 3+; tokens marked "Reserved for later phases" are spec'd here so the planner/executor draws from one canonical scale instead of inventing per-phase increments.
---
## Typography
Material 3 `Typography` roles. Phase 2 uses four roles; the rest of the M3 scale is implicitly available for later phases. Phase 11 may swap `FontFamily` but the **role-to-element mapping below is locked**.
| Role | M3 Token | Size | Weight | Line Height | Phase 2 Element |
|------|----------|------|--------|-------------|-----------------|
| Display | `displaySmall` | 36.sp | Regular (W400) | 44.sp (≈1.22) | "Recipe" wordmark on `SplashScreen` and `LoginScreen` |
| Heading | `headlineSmall` | 24.sp | Regular (W400) | 32.sp (≈1.33) | `Witaj, {displayName}!` welcome text on `PostLoginPlaceholderScreen` |
| Body | `bodyLarge` | 16.sp | Regular (W400) | 24.sp (1.5) | Inline error text below the sign-in button on `LoginScreen` |
| Label | `labelLarge` | 14.sp | Medium (W500) | 20.sp (≈1.43) | Button label text — "Zaloguj się przez Authentik", "Wyloguj się" (Material 3 `Button` slot uses this role by default) |
**Weights declared:** exactly 2 — Regular (W400) for body / heading / display, Medium (W500) for button labels (Material 3 default for `labelLarge`). No Bold, no Light, no SemiBold in Phase 2.
**Sizes declared:** exactly 4 — 14, 16, 24, 36. This satisfies the "34 sizes" cap.
**Line-height policy:**
- Body (16.sp body): 1.5 ratio → 24.sp line height. Matches the brand recommendation; Material 3 `bodyLarge` default is 24.sp.
- Heading (24.sp `headlineSmall`): ~1.33 ratio. Tighter than body per Material 3 baseline; aligns with the "calmer typography" direction in PROJECT.md.
- Display (36.sp `displaySmall`): ~1.22 ratio. Material 3 default.
**Implementation:** use `MaterialTheme.typography.displaySmall` / `.headlineSmall` / `.bodyLarge` / `.labelLarge` directly. Do **not** override `style.copy(fontWeight = ...)` ad-hoc in Phase 2 composables — if a deviation is needed, add it to the `Typography` config in `ui/theme/Typography.kt` so Phase 11 has one place to retune.
---
## Color
Material 3 `ColorScheme` derived from a **single seed color** via `dynamicLightColorScheme` / `dynamicDarkColorScheme` is **not** used (dynamic color is Android 12+ only and would diverge between iOS and Android). Instead Phase 2 ships **explicit baseline schemes** seeded once:
- **Seed color:** `#3B6939` (mid-saturation green, warm-leaning — chosen as a placeholder that reads well in a cooking/meal-planning context without committing to a brand identity).
- **Generation:** `lightColorScheme()` / `darkColorScheme()` Material 3 defaults overridden with the seed-derived `primary` only. All other roles use Material 3 baseline values for their respective scheme.
- **Phase 11 hand-off:** the seed value is open to revision in Phase 11 (final brand-color pass). Tokens listed below are CONTRACT for Phase 2; Phase 11 may rebase the entire palette around a different seed without breaking the role-to-element mapping locked here.
The 60 / 30 / 10 split, mapped to Material 3 roles:
| Role | Light scheme | Dark scheme | Usage |
|------|--------------|-------------|-------|
| Dominant (60%) — `surface` | `#FEF7FF` (M3 default) | `#141218` (M3 default) | Root background of all three Phase 2 screens |
| Secondary (30%) — `surfaceContainer` | `#F3EDF7` (M3 default) | `#211F26` (M3 default) | **Reserved for Phase 5+** (cards, sheets, nav containers). Phase 2 has no card surfaces; this token is declared for forward-compat. |
| Accent (10%) — `primary` | `#3B6939` (seed) | `#A2D597` (seed-derived dark variant) | The single primary CTA on each screen — **only**: "Zaloguj się przez Authentik" button (`LoginScreen`) and **only** that button. Logout uses a different role (see below). |
| Destructive — `error` | `#BA1A1A` (M3 default) | `#FFB4AB` (M3 default) | Inline error text color on `LoginScreen` (`auth_error_*` strings). Reserved for actual error states only — not used for the "Wyloguj się" button. |
**Accent reserved for:** the `LoginScreen` primary CTA button (`Button` composable using `colors = ButtonDefaults.buttonColors()` which resolves to `containerColor = primary`). Nothing else in Phase 2.
**"Wyloguj się" button styling:** uses Material 3 `OutlinedButton` (not `Button`) → `borderColor` = `outline`, `contentColor` = `primary`. This is a deliberate hierarchy choice: logout is a less-frequent, more-deliberate action than login, and reserving the filled-accent variant for the login CTA preserves the "10% accent" ratio. **Not** styled as destructive (red `error`) because logout is not destructive in this app — it ends the session but does not delete user data.
**Dark mode is required.** Per orchestrator note (homelab user's primary environment is dark mode), both `lightColorScheme()` and `darkColorScheme()` MUST be wired. App respects system theme via `isSystemInDarkTheme()` (already standard in Compose). No in-app theme toggle in Phase 2.
**Translucency / blur:** none in Phase 2. All surfaces are opaque. The Liquid-Glass aesthetic begins in Phase 10.
---
## Copywriting Contract
All user-facing strings live in **Compose Resources** (`composeApp/src/commonMain/composeResources/values/strings.xml` per Compose Multiplatform conventions) per CLAUDE.md non-negotiable #9 + CONTEXT D-34. Polish copy below is **scaffold quality**; Phase 11 polishes for plural forms, tone, and proofs the full locale.
| Element | Resource Key | Polish Copy (scaffold) | Screen | Notes |
|---------|--------------|------------------------|--------|-------|
| App wordmark | `auth_app_name` | `Recipe` | Splash, Login | English working title per PROJECT.md; final brand name is a Phase 11 decision. Not localizable in Phase 2. |
| Primary CTA | `auth_sign_in_button` | `Zaloguj się przez Authentik` | LoginScreen | Verb + noun; explicit IdP name to set expectation that the system browser will open. |
| Secondary CTA (logout) | `auth_sign_out_button` | `Wyloguj się` | PostLoginPlaceholderScreen | Single Polish reflexive verb; matches user's expected idiom. |
| Welcome / authenticated state | `auth_welcome_format` | `Witaj, %1$s!` | PostLoginPlaceholderScreen | `%1$s` substituted with `User.displayName` from the JIT-provisioned server response. Use Compose Resources `stringResource(Res.string.auth_welcome_format, user.displayName)`. |
| Error: user cancelled | `auth_error_cancelled` | `Logowanie anulowane. Spróbuj ponownie.` | LoginScreen (inline below button) | Triggered when AppAuth surfaces `OIDAuthError.userCancelled` (iOS) / `AuthorizationException.GeneralErrors.USER_CANCELED_AUTH_FLOW` (Android). |
| Error: network unreachable | `auth_error_network` | `Nie można połączyć z Authentik. Sprawdź połączenie.` | LoginScreen (inline below button) | Triggered on `IOException` / network errors during authorization OR token exchange. |
| Error: token exchange / validation failure | `auth_error_unknown` | `Coś poszło nie tak. Spróbuj ponownie.` | LoginScreen (inline below button) | Catch-all for token-exchange failures, JWT validation errors, JIT-provisioning 5xx. |
**Empty states:** Phase 2 has no empty-state surfaces. The "no user yet" condition routes to `LoginScreen`, which is itself the empty state. No "you're not signed in yet" placeholder text is needed.
**Destructive confirmation:** none in Phase 2. Logout is silent (CONTEXT D-19): "Wyloguj się" tap immediately initiates RP-initiated end-session without a confirmation modal. **Rationale:** the user can re-authenticate trivially; a confirmation modal here would be cargo-culted from destructive-delete patterns where re-creation is impossible. The post-login screen is a placeholder anyway and gets replaced by household onboarding in Phase 3.
**Refresh-failure UX:** silent transition (CONTEXT D-18). When `AuthSession` detects an `invalid_grant` from a background token refresh, it emits `AuthState.Unauthenticated` and the auth gate routes to `LoginScreen`. No toast, no modal, no error message on the LoginScreen itself (the user landed there silently — there is no "previous attempt" to error about). Logged at `Logger.withTag("auth").w(...)` for diagnostics.
**Inline-error display rules (LoginScreen):**
- Error text is rendered **below** the primary button with `md` (16.dp) vertical gap.
- Button **stays enabled** during the error state — the user can retry by tapping again.
- Tapping the button again **clears** the previous error message before initiating a new login flow (so the user does not see stale error text during the next attempt).
- Error text uses `bodyLarge` typography role, `error` color (see Color section).
- Errors are NOT surfaced as Snackbars in Phase 2. Inline-below-button is the contract; Snackbars require a `Scaffold` host that Phase 2 does not need.
**Loading / pending UX (LoginScreen):**
- While AppAuth's authorization request is in flight (system browser is open), the LoginScreen does NOT need a separate loading state — the system browser is full-screen and obscures the app.
- After the system browser dismisses but before token exchange + JIT-provisioning completes, the button shows a `CircularProgressIndicator` (16.dp) inside its content slot, replacing the label, with the button **disabled**. Total expected duration: <500ms in practice.
- Implementation hint: a `Boolean` `isLoading` flag in `LoginScreenState` controls this.
**Splash UX:**
- Visible during `AuthState.Loading` (deserializing persisted `AuthState` blob, possibly running a refresh).
- Centered "Recipe" wordmark using `displaySmall`.
- `sm` (8.dp) below: a `CircularProgressIndicator` at default size (40.dp), `color = primary`.
- No "Loading..." text. No marketing copy. No tagline.
- Background = `surface` (matches Login + PostLogin to avoid a color flash when the auth gate transitions).
---
## Auth Gate Routing Contract
The `App()` composable observes `AuthSession.state: StateFlow<AuthState>` and renders exactly one of:
| `AuthState` value | Rendered composable |
|-------------------|---------------------|
| `AuthState.Loading` | `SplashScreen()` |
| `AuthState.Unauthenticated` | `LoginScreen(viewModel = koinViewModel())` |
| `AuthState.Authenticated(user, householdId)` | `PostLoginPlaceholderScreen(user, viewModel = koinViewModel())` (Phase 2). Phase 3 replaces with `HouseholdGate`. |
**Transition behavior:** state changes drive recomposition; no manual navigation calls. Material 3 default cross-fade (the implicit `Crossfade` recommended pattern, NOT explicit — keep Phase 2 minimal) is acceptable but not required. **Required:** no white flash between transitions — both screens use the same `surface` background.
Implementation note for executor: replace the existing `App.kt` body (currently the JetBrains template's button-and-greeting demo) with a `when` over `authSession.state.collectAsState().value`. Keep the existing `MaterialTheme { ... }` wrapper.
---
## Component Inventory (Phase 2)
Composables the planner / executor must produce in `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/auth/`:
| Composable | File | Responsibility |
|------------|------|----------------|
| `SplashScreen()` | `SplashScreen.kt` | Stateless. Renders wordmark + progress indicator. No ViewModel — the auth gate above it owns state. |
| `LoginScreen(viewModel: LoginViewModel)` | `LoginScreen.kt` | Stateless wrt auth tokens (those live in `AuthSession`). Owns local UI state for `isLoading` + `errorKind`. Triggers `viewModel.onSignInClick()` which delegates to `AuthSession.login()`. |
| `LoginViewModel` | `LoginViewModel.kt` | Wraps `AuthSession`. Maps `AuthSession.LoginResult``LoginScreenState(isLoading, errorKey: StringResource?)`. Method-per-action: `onSignInClick()`. |
| `LoginScreenState` | (data class in `LoginViewModel.kt`) | `(val isLoading: Boolean, val errorKey: StringResource?)`. Immutable. |
| `PostLoginPlaceholderScreen(user: User, viewModel: PostLoginViewModel)` | `PostLoginPlaceholderScreen.kt` | Renders welcome text + logout button. Triggers `viewModel.onSignOutClick()`. |
| `PostLoginViewModel` | `PostLoginViewModel.kt` | Wraps `AuthSession.logout()`. Trivial in Phase 2 — exists so Phase 3's `HouseholdGate` follows the same VM pattern. |
| `RecipeTheme(content)` | `ui/theme/RecipeTheme.kt` | Top-level theme wrapper applying `lightColorScheme()` / `darkColorScheme()` based on `isSystemInDarkTheme()`. Wraps `MaterialTheme(colorScheme, typography, shapes)`. **Phase 2 ships this seed;** later phases extend with custom typography + shape tokens here. |
**No `Scaffold` in Phase 2.** Each of the three auth screens uses `Surface(modifier = Modifier.fillMaxSize().safeContentPadding())` as the root. `Scaffold` (with its `topBar` / `bottomBar` slots and Snackbar host) lands in Phase 5 (`RecipeListScreen`) or Phase 10 (`MainScaffold` chrome).
---
## Layout Contract
All three screens use a **vertically-centered single-column layout**:
```
Modifier.fillMaxSize()
.background(MaterialTheme.colorScheme.surface)
.safeContentPadding()
.padding(horizontal = 16.dp) // md token
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxSize(),
)
```
**Per-screen content order (top → bottom):**
| Screen | Content |
|--------|---------|
| `SplashScreen` | Wordmark `displaySmall``Spacer(8.dp)``CircularProgressIndicator(color = primary)` |
| `LoginScreen` | Wordmark `displaySmall``Spacer(24.dp)``Button(onClick = onSignInClick) { Text(R.string.auth_sign_in_button) }` (or `CircularProgressIndicator(16.dp)` when `isLoading`) → `Spacer(16.dp)``Text(error, style = bodyLarge, color = error)` if `errorKey != null` |
| `PostLoginPlaceholderScreen` | `Text(stringResource(Res.string.auth_welcome_format, user.displayName), style = headlineSmall)``Spacer(24.dp)``OutlinedButton(onClick = onSignOutClick) { Text(R.string.auth_sign_out_button) }` |
**Width constraint:** content column natural-fits its children. No `widthIn(max = 480.dp)` tablet-narrowing in Phase 2 — the app targets phone-sized iOS first; tablet polish is post-v1.
---
## Component Sourcing & Safety
| Source | Components Used (Phase 2) | Safety Gate |
|--------|---------------------------|-------------|
| Material 3 stdlib (`androidx.compose.material3`) | `Surface`, `MaterialTheme`, `Text`, `Button`, `OutlinedButton`, `CircularProgressIndicator` | not required (first-party Compose Multiplatform stdlib, applied by `recipe.compose.multiplatform` convention plugin per Phase 1 D-07) |
| Compose Foundation (`androidx.compose.foundation`) | `Column`, `Spacer`, `Box`, `Modifier.background`, `Modifier.safeContentPadding`, `Modifier.fillMaxSize`, `Modifier.padding` | not required (first-party) |
| Compose Resources (`org.jetbrains.compose.components:components-resources`) | `stringResource`, generated `Res.string.*` accessors | not required (first-party Compose Multiplatform; Phase 1 generated `Res` accessors already wired) |
| Third-party UI registry | none in Phase 2 | not applicable |
**No Haze, no third-party UI components in Phase 2.** Haze is gated to Phase 10 per CLAUDE.md non-negotiable #10. Adding third-party UI components to the auth scaffold is explicitly out-of-scope.
---
## Accessibility
| Requirement | Implementation |
|-------------|----------------|
| Touch target ≥48dp | Material 3 `Button` / `OutlinedButton` defaults satisfy this; do not shrink |
| Color contrast (WCAG AA) | Material 3 baseline `lightColorScheme()` / `darkColorScheme()` ship WCAG AA-compliant role pairings (e.g., `onPrimary` on `primary`); seed override only changes `primary` so the contrast pairing holds |
| Dynamic type / font scaling | Material 3 `Typography` roles use `sp` (already scale-respecting); no override forcing fixed sizes |
| Screen reader semantics | `Button` carries its label as accessibility text by default; `Text` for the welcome line is announced by VoiceOver / TalkBack as plain content. No custom `Modifier.semantics` overrides required in Phase 2 |
| RTL | not applicable in Phase 2 (Polish is LTR) |
**Phase 11 will revisit:** `contentDescription` on any decorative imagery, semantic grouping of multi-element clusters, full VoiceOver pass on iOS device.
---
## Out-of-Scope (Reserved for Later Phases)
The following intentionally have NO contract in Phase 2:
| Concern | Owning Phase |
|---------|--------------|
| Tab bar / bottom navigation | Phase 10 (`UI Chrome & Haze`) |
| Top app bar / nav bar with Haze blur | Phase 10 |
| Glass / translucent surface tokens | Phase 10 |
| Display font selection + custom `FontFamily` | Phase 11 |
| Polished Polish copy with plural forms (1 / 2 / 5 / 22) | Phase 11 |
| Brand color final pass (re-seeding `primary`) | Phase 11 |
| In-app theme toggle (override system dark/light) | not in v1 (out of scope per PROJECT.md) |
| Animated transitions between auth states | Phase 10 |
| Logo / wordmark image asset | not in v1 — text wordmark only until Phase 11 brand pass |
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS — 6 string keys declared with Polish scaffold copy + Compose Resources delivery contract; inline-error UX rules locked; logout silent UX justified
- [ ] Dimension 2 Visuals: PASS — 3 composables specified with file paths, layout column structure, and no-Scaffold-in-Phase-2 boundary
- [ ] Dimension 3 Color: PASS — Material 3 `lightColorScheme()` / `darkColorScheme()` seeded with `#3B6939`; 60/30/10 mapped to `surface` / `surfaceContainer` / `primary`; accent reserved for the single LoginScreen CTA; dark mode required
- [ ] Dimension 4 Typography: PASS — exactly 4 sizes (14/16/24/36), exactly 2 weights (W400/W500), Material 3 role-to-element mapping locked
- [ ] Dimension 5 Spacing: PASS — full xs..3xl scale declared, all multiples of 4, Phase 2 uses sm/md/lg/2xl, others reserved
- [ ] Dimension 6 Component Sourcing: PASS — Material 3 stdlib only, no third-party UI, no Haze in Phase 2 (gated to Phase 10), no registry safety gate needed
**Approval:** pending
---
## UI-SPEC COMPLETE
**Phase:** 2 — Authentication Foundation
**Design System:** Compose Multiplatform Material 3 (no shadcn — KMP project)
### Contract Summary
- **Spacing:** 8-point scale `xs..3xl` (4 / 8 / 16 / 24 / 32 / 48 / 64 dp); Phase 2 actively uses sm / md / lg / 2xl
- **Typography:** 4 sizes (14, 16, 24, 36 sp), 2 weights (W400, W500); Material 3 roles `displaySmall` / `headlineSmall` / `bodyLarge` / `labelLarge`
- **Color:** Material 3 `light` + `dark` schemes seeded with `#3B6939`; 60% `surface` / 30% `surfaceContainer` / 10% `primary`; accent reserved for single LoginScreen CTA; logout uses `OutlinedButton` (not destructive `error`)
- **Copywriting:** 6 Compose Resources keys + Polish scaffold copy locked (`auth_app_name`, `auth_sign_in_button`, `auth_sign_out_button`, `auth_welcome_format`, `auth_error_cancelled`, `auth_error_network`, `auth_error_unknown`); inline-error UX + silent-logout UX defined
- **Component Sourcing:** Material 3 stdlib only — no Haze, no third-party UI registries (Phase 2 has no registry-safety gate to clear)
### File Created
`.planning/phases/02-authentication-foundation/02-UI-SPEC.md`
### Pre-Populated From
| Source | Decisions Used |
|--------|----------------|
| `02-CONTEXT.md` (D-30..D-34) | 5 (auth gate routing, login minimal, login error states, post-login placeholder, Compose Resources) |
| `02-CONTEXT.md` (Claude's Discretion) | 1 resolved here (splash visual = wordmark + circular progress indicator) |
| `PROJECT.md` (locked stack) | 4 (Material 3, system font, Polish-only v1, Liquid-Glass deferred to polish phase) |
| `CLAUDE.md` (non-negotiables) | 2 (#9 strings externalized day 1, #10 Haze on chrome only — gates Phase 2 to no-blur) |
| `ROADMAP.md` (phase boundaries) | 2 (Phase 10 owns UI chrome / Haze, Phase 11 owns localization + final polish) |
| `REQUIREMENTS.md` (AUTH-01..AUTH-06) | 1 (AUTH-05 logout returns to login screen) |
| `ARCHITECTURE.md` (component responsibilities) | 1 (`AuthSession` Koin singleton owning `StateFlow<AuthState>`) |
| `App.kt` (Phase 1 scaffold) | 1 (existing `MaterialTheme { ... }` + `safeContentPadding()` pattern preserved) |
### Awaiting / Notes for Downstream
- **Planner (`gsd-planner`):** the Component Inventory + Layout Contract sections give you concrete file paths and composable shapes; tokens in Spacing / Typography / Color sections are referenced via Material 3 theme accessors (`MaterialTheme.colorScheme.primary`, `MaterialTheme.typography.displaySmall`, etc.). The seed color `#3B6939` is the only manual override needed in `RecipeTheme.kt`.
- **Executor (`gsd-executor`):** replace `App.kt` body with the auth-gate `when`-block; do NOT keep the JetBrains template's button-and-greeting code. Wire `Res.string.*` keys via Compose Resources (`composeApp/src/commonMain/composeResources/values/strings.xml`).
- **Phase 10 / 11 hand-off seam:** every "Reserved for Phase 10/11" annotation in this doc is an explicit hand-off point; do not retroactively rewrite Phase 2's seed tokens during those phases unless the tradeoff is documented.
### Ready for Verification
UI-SPEC complete. Checker can now validate against the 6 design quality dimensions.