docs(02): UI design contract for auth foundation
Locks scaffold-level visual contract for Phase 2: spacing scale, typography roles, Material 3 color seed, copywriting keys, and auth-gate routing — without committing to Liquid-Glass / Haze (Phase 10) or final font + Polish polish (Phase 11). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
301
.planning/phases/02-authentication-foundation/02-UI-SPEC.md
Normal file
301
.planning/phases/02-authentication-foundation/02-UI-SPEC.md
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
---
|
||||||
|
phase: 2
|
||||||
|
slug: authentication-foundation
|
||||||
|
status: draft
|
||||||
|
shadcn_initialized: false
|
||||||
|
preset: none
|
||||||
|
created: 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 "3–4 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.
|
||||||
Reference in New Issue
Block a user