Files

348 lines
26 KiB
Markdown
Raw Permalink 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.1
slug: app-shell-navigation-search-foundation
status: draft
shadcn_initialized: false
preset: not applicable
created: 2026-05-08
revised: 2026-05-08
---
# Phase 2.1 — UI Design Contract
> Visual and interaction contract for the App Shell, Navigation & Search Foundation. Generated by gsd-ui-researcher, verified by gsd-ui-checker.
>
> **Stack note:** This is a Kotlin Multiplatform + Compose Multiplatform mobile project (iOS-primary, Android secondary). shadcn is not applicable — the design system is built on Composables / Compose Unstyled primitives + a local `RecipeTheme` token scaffold + a `GlassSurface` primitive backed by Liquid → Haze → flat fallback chain.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (Compose Multiplatform; shadcn is web-only) |
| Preset | not applicable |
| Component library | Composables / Compose Unstyled (renderless primitives, locally restyled by Recipe components) |
| Icon library | Compose Material Icons Outlined (`androidx.compose.material:material-icons-extended`) — Material Icons stays even though the visual layer leaves Material 3; outlined variants align with the calm Liquid-Glass aesthetic |
| Font | System default (`FontFamily.Default`) for v1; SF Pro on iOS / Roboto on Android via platform default. No custom font shipped this phase. Phase 10 may revisit. |
| Glass primitive | `GlassSurface` composable in `ui/components/glass/`, layered over Liquid (`io.github.fletchmckee.liquid:liquid`) → Haze (`dev.chrisbanes.haze:haze`) → flat translucent fallback |
| Theme entry | `dev.ulfrx.recipe.ui.theme.RecipeTheme { content }` providing a `LocalRecipeColors`, `LocalRecipeTypography`, `LocalRecipeSpacing`, `LocalRecipeShapes`, `LocalRecipeGlass` `CompositionLocal` set |
**Material 3 boundary:** Material 3 stays only as legacy auth-screen scaffolding (`PostLoginPlaceholderScreen`, login). New code in `ui/screens/{planner,recipes,pantry,shopping}` and `ui/components/` MUST NOT introduce `androidx.compose.material3.*` imports. Use `RecipeTheme` tokens.
---
## Spacing Scale
Declared values (all multiples of 4, all within the standard set {4, 8, 16, 24, 32, 48, 64}):
| Token | Value | Usage |
|-------|-------|-------|
| `xs` | 4dp | Icon-to-label gap inside dock pill; chip internal padding |
| `sm` | 8dp | Compact inline spacing; gap between dock and floating search/action button; floating dock vertical offset above the bottom safe-area; dock vertical padding; inter-tab gap inside dock; empty-state icon-to-headline gap |
| `lg` | 16dp | Default screen content padding; empty-state headline-to-subline gap; search pill horizontal padding |
| `xl` | 24dp | Section padding; horizontal screen edge inset for empty-state body |
| `2xl` | 32dp | Layout-level gaps; vertical breathing room above empty-state block |
| `3xl` | 48dp | Large vertical separators (e.g. between top safe-area and an empty-state's icon when centered visually rather than mathematically) |
**Revision note (revision 1, 2026-05-08):** CONTEXT D-14 originally locked the scale as `4/8/12/16/24/32`. The 12dp step (`md`) was retired during UI-SPEC verification because no usage in this phase required 12dp specifically — every prior 12dp reference was remapped to 8dp (tighter chrome read more like a native iOS dock cluster). The scale extends upward with `2xl` (32dp) and `3xl` (48dp) so empty-state vertical rhythm has expressive headroom. Re-introduce a 12dp token in a later phase if a real geometric need surfaces in execution; the rest of the system can absorb that without churn.
**Exceptions:**
- iOS safe-area insets are added on top of these tokens via `WindowInsets.safeContent` — never hardcode status-bar or home-indicator padding.
- Touch target minimum: 44dp on iOS, 48dp on Android. Dock tab cells and the floating search button MUST satisfy this even if visual padding is smaller — use a transparent expansion via `Modifier.minimumInteractiveComponentSize()` or equivalent.
- Dock geometry: 56dp expanded height, 44dp collapsed height. These are absolute pixel values driven by touch-target ergonomics, not spacing-scale tokens.
---
## Typography
Four named text styles, two weights (Regular 400, Semibold 600). Use system default font family; let the platform pick SF Pro / Roboto.
| Role | Size | Weight | Line Height | Letter Spacing | Usage |
|------|------|--------|-------------|----------------|-------|
| `display` | 28sp | 600 (Semibold) | 1.2 (≈34sp) | -0.2sp | Empty-state headline (the calm, anticipatory line) |
| `title` | 20sp | 600 (Semibold) | 1.2 (≈24sp) | 0sp | Inline tab title at top of each screen body (no top app bar — D-04) |
| `body` | 16sp | 400 (Regular) | 1.5 (≈24sp) | 0sp | Empty-state subline; search input value text; default screen body copy |
| `label` | 13sp | 600 (Semibold) | 1.2 (≈16sp) | 0.1sp | Dock tab labels (always shown, both active + inactive — D-02); chip text |
**Scale enforcement:** No raw `TextStyle(fontSize = ...)` in screen code. All text styles come from `RecipeTheme.typography.{display,title,body,label}`. The `title` role is the only header style this phase ships — there is no `headline` / `h1..h6` cascade because there's no top app bar (D-04) and screens don't yet have multi-level content hierarchy.
**Polish-language readiness:**
- All four roles must render Polish diacritics (ą, ć, ę, ł, ń, ó, ś, ź, ż) without clipping. Line-height ratios above (1.2 / 1.5) leave headroom for `ą` and `Ż` accents.
- Long Polish tab labels constrain the `label` role: `Spiżarnia` is the longest (9 chars including diacritic). Dock label cells must accommodate this without truncation at default font scale; with system font scaling at 1.3× the dock may compress label visibility (active-only) — this is acceptable in v1 and revisited in Phase 10.
---
## Color
Light + dark schemes are both defined this phase (CONTEXT D-15) and follow the system setting. The mockup palette is reference, not ported. Tokens are exposed as semantic roles (CONTEXT D-14), never raw hex in screen code.
### Semantic roles (60/30/10 + supporting)
| Role | Light value | Dark value | Usage (60/30/10 mapping) |
|------|-------------|-----------|--------------------------|
| `background` | `#F7F5F1` (warm off-white) | `#0F1113` (near-black warm) | **Dominant 60%** — full-screen background behind every tab |
| `surface` | `#FFFFFF` | `#1A1D21` | **Secondary 30%** — solid card / sheet / search-pill substrate when glass is unavailable (flat fallback) |
| `surfaceGlass` | `#FFFFFF @ 60% alpha` | `#1A1D21 @ 55% alpha` | Tint layer composited inside `GlassSurface` (dock, search pill, floating action button); the Liquid/Haze blur reads through this |
| `content` | `#0F1113` | `#F1EFEA` | Primary text on `background` and `surface` |
| `contentMuted` | `#6B6E73` | `#9AA0A6` | Empty-state subline, inactive tab label, secondary captions |
| `accent` | `#D97757` (warm terracotta) | `#E48A6E` | **Accent 10%** — see "Accent reserved for" below |
| `separator` | `#E5E1DA` | `#2A2D31` | Hairline dividers (1dp); inter-tab separators inside dock if used |
| `borderCard` | `#E5E1DA @ 60% alpha` | `#FFFFFF @ 8% alpha` | Outline on glass surfaces (dock, search pill) for depth in light mode and edge clarity in dark mode |
| `destructive` | `#C0392B` | `#E57368` | Reserved — no destructive actions exist in this phase, but the token is declared so feature phases (sign-out confirmation, plan-entry deletion) inherit it |
### Accent reserved for
The `accent` color (warm terracotta, 10% of pixel real estate target) is used **only** for:
1. **Active dock tab** — the wider, emphasized active tab cell uses `accent` at full opacity for its icon + label color, on a `surfaceGlass` substrate. Inactive tabs use `contentMuted`.
2. **Search input caret + selection highlight** — the cursor in the open search pill, and any text-selection range.
Accent is NOT used for:
- Dividers, borders, separators
- Empty-state icons (those use `contentMuted` per D-10 — calm, low-saturation)
- The dock substrate itself (that is `surfaceGlass`, not `accent`)
- Standard body text
This list is exhaustive for this phase. Future phases extend it — primary CTA buttons (Phase 5+), shopping-list checked items (Phase 9), etc.
### 60/30/10 audit (this phase only)
- 60% `background` — yes; the four tab screens are predominantly empty (empty states), so the warm off-white / near-black background dominates.
- 30% `surface` / `surfaceGlass` — yes; the dock pill, the floating search button, and the search pill are the only substantial non-background surfaces in the shell.
- 10% `accent` — yes; only the active tab and the search caret carry accent. Quantitatively below 10%, which is correct for a calm shell.
---
## Copywriting Contract
All strings go through Compose Resources (`composeResources/values/strings.xml` or per-locale equivalents). No literal Polish strings in `.kt` files. Resource keys are namespaced by feature: `shell_*`, `empty_*`, `search_*`. Polish copy is the v1 ship language; the resource catalog is multi-locale-ready for Phase 11.
### Tab labels (CONTEXT D-03 — order: Planer, Przepisy, Spiżarnia, Zakupy)
| Resource key | Polish copy | English placeholder (not shipped) |
|--------------|-------------|-----------------------------------|
| `shell_tab_planner` | `Planer` | Planner |
| `shell_tab_recipes` | `Przepisy` | Recipes |
| `shell_tab_pantry` | `Spiżarnia` | Pantry |
| `shell_tab_shopping` | `Zakupy` | Shopping |
### Empty states (CONTEXT D-10, D-11 — anticipatory tone, icon + headline + subline, no CTA)
| Tab | Icon (Material Outlined) | Headline (display) | Subline (body) |
|-----|--------------------------|--------------------|----------------|
| Planer | `Icons.Outlined.CalendarMonth` | `Twój plan tygodnia czeka` | `Wkrótce zobaczysz tu zaplanowane posiłki.` |
| Przepisy | `Icons.Outlined.MenuBook` | `Tu pojawi się Twoja książka kucharska` | `Po dodaniu pierwszych przepisów zobaczysz je w tym miejscu.` |
| Spiżarnia | `Icons.Outlined.Inventory2` | `Spiżarnia jest jeszcze pusta` | `Wkrótce zobaczysz tu wszystko, co masz pod ręką.` |
| Zakupy | `Icons.Outlined.ShoppingCart` | `Lista zakupów czeka na Twój plan` | `Gdy zaplanujesz tydzień, zobaczysz tu, czego brakuje.` |
Resource keys: `empty_planner_title` / `empty_planner_subtitle`, `empty_recipes_title` / `empty_recipes_subtitle`, `empty_pantry_title` / `empty_pantry_subtitle`, `empty_shopping_title` / `empty_shopping_subtitle`.
**Tone rules:**
- Forward-looking: "Wkrótce", "Po dodaniu", "Gdy zaplanujesz" — signal the feature is real, not broken.
- No "Brak danych", no chatty onboarding ("Witaj!"), no exclamation marks.
- Subline ends with a period. Headline does not.
- No CTA buttons (CONTEXT D-12). The `EmptyState` composable's `action` slot is reserved unused this phase (D-13).
**Phase 11 caveat:** copy may be tuned during the localization pass. Resource keys above are the contract; copy strings are best-current.
### Search affordance (CONTEXT D-06 through D-09)
| Resource key | Polish copy | Purpose |
|--------------|-------------|---------|
| `search_open_a11y` | `Otwórz wyszukiwanie` | Content description for the floating search-icon button (icon-only) |
| `search_close_a11y` | `Zamknij wyszukiwanie` | Content description for the collapsed dock toggle when search is open (D-05) |
| `search_clear_a11y` | `Wyczyść` | Content description for the clear button inside the search pill (visible when query is non-empty) |
| `search_placeholder_recipes` | `Szukaj przepisów…` | Search pill placeholder on Przepisy tab |
| `search_placeholder_pantry` | `Szukaj w spiżarni…` | Search pill placeholder on Spiżarnia tab |
Search body content: **none** (CONTEXT D-07). No "no results" copy this phase. Phase 5 wires real result rendering. Empty `SearchSurface` body renders an empty `Box` matched to `background`.
### Error / sign-out (out of scope for this phase but tokens reserved)
This phase introduces no error surfaces (auth errors are Phase 2 territory; sync errors are Phase 4+) and no destructive actions. The `destructive` color and a future `confirm_signout_*` resource family are NOT defined here — they ship with their owning phase.
### CTA / primary action
This phase has **no primary CTA button**. The shell is navigation chrome and empty surfaces. The `accent` color contract above declares accent reservation; the first real primary CTA ships in Phase 5 (recipe browse).
---
## Component Inventory (this phase)
Composables introduced in `composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/`:
| Composable | Path | Built on | Visual contract |
|-----------|------|----------|-----------------|
| `RecipeTheme` | `ui/theme/RecipeTheme.kt` | CompositionLocal scaffold | Provides `RecipeColors`, `RecipeTypography`, `RecipeSpacing`, `RecipeShapes`, `RecipeGlass` to descendants |
| `GlassSurface` | `ui/components/glass/GlassSurface.kt` | Liquid → Haze → flat | Single primitive consumed by dock, search pill, floating buttons. Same token API across all three backends (color, opacity, radius). Compile-time backend selection per target; debug-build runtime toggle (CONTEXT D-16, D-17) |
| `AppShell` | `ui/screens/shell/AppShell.kt` | Compose Unstyled `Scaffold`-equivalent | Auth-gated root: hosts root NavHost + the bottom dock + the floating search/action surface. Renders `background` color edge-to-edge under safe-area insets. |
| `DockBar` | `ui/components/dock/DockBar.kt` | Compose Unstyled `TabGroup`-equivalent + GlassSurface | Floating bottom pill, 4 tabs (icon + label always — D-02), active tab wider with `accent` foreground; collapses to single circular icon-only toggle when `searchOpen == true` (D-05). Capsule shape: full-pill (height/2 corner radius). Height: 56dp; collapsed height: 44dp. |
| `FloatingSearchButton` | `ui/components/dock/FloatingSearchButton.kt` | Compose Unstyled `Button` + GlassSurface | 44dp circular glass button, search icon (`Icons.Outlined.Search`) tinted `content`. Adjacent to dock with `sm` (8dp) gap. Visible only on Przepisy + Spiżarnia tabs (D-06). Hidden when `searchOpen == true`. |
| `SearchPill` | `ui/components/search/SearchPill.kt` | Compose Unstyled `TextField` (renderless) + GlassSurface | Inline bottom search pill (D-09). Capsule shape. Holds: leading search icon, text input (placeholder per tab), trailing clear button (visible when query non-empty). Substrate: `surfaceGlass`. Body content behind it stays visible. Height: 44dp. |
| `EmptyState` | `ui/components/empty/EmptyState.kt` | Plain Compose | Reusable `EmptyState(icon: ImageVector, title: String, subtitle: String, action: (@Composable () -> Unit)? = null)` — D-13. Vertical center on screen. Icon 48dp tinted `contentMuted`. Spacing: icon → 8dp (`sm`) → headline (`display`) → 16dp (`lg`) → subline (`body`, color `contentMuted`). `action` slot is below subline at 24dp (`xl`) gap when present; unused this phase. |
| `Screen scaffolds` | `ui/screens/{planner,recipes,pantry,shopping}/{Tab}Screen.kt` | `RecipeTheme` + `EmptyState` | Each: inline tab title at top in `title` style + `lg` padding, then centered `EmptyState`. Background: `RecipeColors.background`. |
**Renderless primitive boundary:** Where Compose Unstyled provides a renderless primitive (button, text field, tab group), Recipe components MUST consume it and apply local styling, not implement the gesture/a11y semantics from scratch. This is the explicit project decision (PROJECT.md § Components: Composables / Compose Unstyled).
---
## Interaction Contracts
### Dock state machine (CONTEXT D-05)
States:
- `Expanded` — default. 4-tab pill, all icons + labels visible, active tab wider with `accent` foreground.
- `Collapsed` — when `searchOpen == true`. Single circular cell showing only the active tab's icon, no label, height 44dp (vs 56dp expanded).
Transition: **single coordinated animation** (not two independent ones — explicit user intent in CONTEXT specifics). Suggested duration: 250ms with a standard easing (e.g. `FastOutSlowInEasing`); planner picks final curves and Phase 10 tunes on real device.
Tapping the collapsed dock = `setSearchOpen(false)` = re-expand + close search.
### Search affordance (CONTEXT D-06 through D-09)
- Visible only on `Przepisy` + `Spiżarnia` tabs.
- `FloatingSearchButton` tap → `searchOpen = true``SearchPill` slides up / fades in, `DockBar` collapses, `FloatingSearchButton` hides. Coordinated with the dock-collapse animation as one motion.
- Closing: tap collapsed dock OR system back gesture → `searchOpen = false` AND `query = ""` (D-08). Re-opening starts blank.
- Query state lives in the per-tab `SearchViewModel` (one for Recipes, one for Pantry); no persistence across close, tab-switch, or app launch.
- Body of search surface: **renders nothing** this phase (D-07). The `SearchPill` overlays the existing tab body; the body remains visible behind it.
### Tab navigation (UI-03 / CONTEXT D-03)
- Default landing tab on first sign-in: `Planer` (D-03 — departs from REQ listing order, which research confirmed non-binding).
- Tab order in dock (left→right): Planer / Przepisy / Spiżarnia / Zakupy.
- Each tab owns an independent nested `NavHost` (CONTEXT D-03 + research ARCHITECTURE recommendation), so future detail screens preserve back stacks per tab.
- Tab switch preserves the destination tab's back stack; selecting an already-active tab pops to its root (standard mobile pattern).
- No tab-bar hide-on-scroll behavior this phase (deferred — CONTEXT § Deferred).
### Accessibility
- Each dock tab cell: `Modifier.semantics { role = Role.Tab; selected = isActive; contentDescription = "$tabLabel${if (isActive) ", aktywna" else ""}" }`.
- `FloatingSearchButton`: `contentDescription = stringResource(Res.string.search_open_a11y)`.
- Collapsed dock toggle: `contentDescription = stringResource(Res.string.search_close_a11y)`.
- Search pill clear button: `contentDescription = stringResource(Res.string.search_clear_a11y)`; visible only when query is non-empty.
- Touch targets: dock tab cells and the floating search button MUST be ≥ 44dp on iOS, ≥ 48dp on Android.
- Focus order when search opens: search input field receives focus on open; soft keyboard appears; the collapsed dock toggle is in the tab order after the clear button.
- Empty-state regions: `Modifier.semantics(mergeDescendants = true) { ... }` so VoiceOver reads the headline + subline as one announcement, not two.
---
## Glass / Liquid contract
`GlassSurface` is the only entry point to glass effects this phase. Direct calls to Liquid or Haze APIs from screen code are forbidden — those only live inside `GlassSurface`'s internal backend selection.
### Backend selection
| Backend | When engaged | Notes |
|---------|--------------|-------|
| Liquid | Default on iOS + Android where Liquid 1.1.x compiles cleanly for the target | Pixel-sampling refractive approximation; matches PROJECT decision and CLAUDE.md convention #10 |
| Haze | Compile-time fallback if Liquid does not ship for a target, OR runtime debug-toggle override | Plain blur; no refraction |
| Flat | Compile-time fallback if neither Liquid nor Haze is available, OR debug-toggle override | Solid translucent surface using `surfaceGlass` token; no blur |
Selection mechanism (CONTEXT D-17):
- **Compile-time per target:** the build picks the backend at build time. No runtime branch in production binaries.
- **Runtime debug toggle (debug builds only):** stored via `multiplatform-settings`, surfaced through a hidden settings entry or build flag. Lets the developer switch backends on-device for visual comparison.
### Surface parameters
The dock, search pill, and floating search button all consume the same token API:
| Parameter | Value | Notes |
|-----------|-------|-------|
| Tint color | `surfaceGlass` (light: white@60%, dark: dark@55%) | Composited inside the glass effect |
| Corner radius | 28dp for the dock pill (full-pill at 56dp height); 22dp for the collapsed dock toggle (full-pill at 44dp); 22dp for the search pill (full-pill at 44dp); 22dp for the floating search button (full-circle at 44dp) | All chrome elements are pill / circle, never rectangular |
| Border | 1dp `borderCard` outline | Provides edge clarity especially in dark mode |
| Elevation / shadow | Soft drop shadow: y-offset 8dp, blur 24dp, opacity 12% in light mode; opacity 0% (no shadow, just border) in dark mode | Applied via `Modifier.shadow()` outside the glass clip |
| Blur radius (Liquid + Haze) | Initial value: 24dp. Phase 10 tunes on real device. Planner may pick library-specific equivalent. |
| Refraction (Liquid only) | Library default initially; tune in Phase 10. |
**Chrome-only constraint (CLAUDE.md #10 + PITFALLS Pitfall 5):** Glass surfaces are applied to dock, search pill, and floating search button only. NEVER over scrolling content. The empty-state area, tab body, and any future list rows are flat — no `GlassSurface` wraps them.
### Fallback test plan (informational)
Each backend must render visually distinct but functionally identical chrome. Acceptance: switching the debug toggle between Liquid / Haze / flat keeps the dock, search pill, and floating button in the same geometry, with the same content positioning, only the substrate effect changes.
---
## Layout & Safe Area
- Root container: full-screen, edge-to-edge. `WindowInsets.statusBars` is consumed by tab body content (top inset added to the inline tab title's top padding). `WindowInsets.navigationBars` + iOS home-indicator inset are consumed by the dock's bottom offset.
- The dock floats `sm` (8dp) above the bottom safe-area inset. The search pill and floating search button sit at the same vertical baseline as the dock when active.
- iOS keyboard avoidance: when the search input has focus, the search pill animates above the soft keyboard via `imeAnimationSource` / `imePadding()`. The dock's collapsed toggle rides up with it (single coordinated motion).
- No top app bar (D-04). The inline tab title sits at the top of each screen body with `xl` (24dp) top padding above the status-bar inset, then `lg` (16dp) below before screen content (or before the empty-state vertical centering region).
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none — not applicable (Compose Multiplatform stack) | not required |
| Compose Unstyled (`composables.com`) | renderless primitives (Button, TextField, TabGroup-equivalent) — locally restyled by Recipe components | not required (first-party renderless library; no third-party code lifted into the project) |
| Liquid (`io.github.fletchmckee.liquid:liquid`) | consumed as a Gradle dependency, not as copied source | dependency review passed — date 2026-05-08; no source code lifted |
| Haze (`dev.chrisbanes.haze:haze`) | consumed as a Gradle dependency, not as copied source | dependency review passed — date 2026-05-08; no source code lifted |
No third-party shadcn registries declared. No source-code blocks vended into the repo. Standard Gradle dependency review applies.
---
## Out-of-Scope Boundaries (this UI-SPEC)
These intentionally have no contract here and are owned by later phases:
- Recipe list rendering, grid spec, card style — Phase 5
- Real planner grid, day cells, slot cells — Phase 6
- Pantry inventory rows, category headers — Phase 8
- Shopping list rows, checked-state styling, category groupings — Phase 9
- Theme polish (final color palette tuning, custom font) — Phase 10
- Animation curves and durations beyond the dock-collapse 250ms default — Phase 10 tunes on real device
- Real-device Liquid parameter tuning (refraction strength, specular highlights) — Phase 10
- Polish copy final pass — Phase 11
- Profile / settings / sign-out chrome placement — Phase 3 onward (no top bar exists yet — D-04)
---
## Pre-Population Audit
| Field | Source |
|-------|--------|
| Tab order, default landing | CONTEXT D-03 |
| Tab labels (Polish) | CONTEXT D-03 + REQUIREMENTS UI-03 |
| Dock shape, label visibility | CONTEXT D-01, D-02 |
| Top app bar absence | CONTEXT D-04 |
| Dock-collapse-on-search transition | CONTEXT D-05 + user verbatim |
| Search affordance scope (which tabs) | CONTEXT D-06 |
| Search behavior this phase | CONTEXT D-07, D-08, D-09 |
| Empty-state pattern + tone + no CTA | CONTEXT D-10, D-11, D-12 |
| `EmptyState` composable signature | CONTEXT D-13 |
| Theme scaffold scope | CONTEXT D-14 |
| Light + dark schemes | CONTEXT D-15 |
| GlassSurface fallback chain | CONTEXT D-16 |
| Compile-time + debug toggle | CONTEXT D-17 |
| Compose Unstyled foundation | PROJECT.md Key Decisions + CLAUDE.md tech stack |
| Liquid first / Haze fallback | PROJECT.md + CLAUDE.md #10 |
| Strings externalized | CLAUDE.md #9 + REQUIREMENTS UI-01 |
| Material 3 boundary | PROJECT.md + CONTEXT discretion default |
| Material Icons Outlined | CONTEXT discretion default |
| Spacing scale 4/8/16/24/32/48 | CONTEXT D-14 (12dp step retired during UI-SPEC verification — see Spacing § Revision note) |
| Typography 4 styles, 2 weights | gsd-ui-researcher recommendation aligned with CONTEXT D-14 named scale |
| Color hex values | gsd-ui-researcher recommendation (mockup is reference, not ported — CONTEXT D-15) |
| Empty-state copy strings | gsd-ui-researcher recommendation; subject to Phase 11 copy pass |
| Touch target minimums | iOS HIG / Material guidelines + accessibility default |
| 250ms transition duration | gsd-ui-researcher reasonable default; CONTEXT discretion + Phase 10 tunes |
No user questions asked this round — CONTEXT.md, PROJECT.md, REQUIREMENTS.md, and CLAUDE.md collectively answered every load-bearing decision. Discretionary defaults (color hex values, typography sizes, copy strings, animation duration) are recorded above and revisitable in Phase 10/11.
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending