# Hope Voice iOS — UI/UX Audit & Redesign Specification - Task: 20260831-105824-plan-code-ui-audit - Agent: coder (UI/UX research pass) - Model: litellm-lan/qwen3.8-27b - Date: 2026-08-31 - Status: complete (research + specification only; no product code changed) --- ## 1. Scope & Method This document audits the current iOS UI in `/Users/shane/dev/voice-ios` against the canonical Hope visual language found in the reference projects, and specifies a coherent redesign. All findings cite file paths and line numbers so an implementer can act without this conversation's context. Sources inspected: | Path | What it provided | |---|---| | `hope-voice-api/web/style.css` (1607 lines) | Full design-token system (`:root`, lines 1–26), LED-wall avatar (lines 316–429), brand glyph (lines 125–166), message bubbles (lines 702–764), buttons/controls (line 431+), task status dots (lines 1120–1150), keyframes (`think-pulse` L427, `message-in` L709, `status-pulse` L1133) | | `hope-voice-api/web/app.js` | LED-wall build/render implementation (lines 265–341): 21×21 grid, radial falloff, per-dot phase ripple, audio-reactive sizing, state palettes (line 389 sets `data-state`) | | `hope-voice-api/web/index.html` | ARIA roles: `led-wall` is `aria-hidden="true"` (L61), `orb-labels` is `aria-live="polite"` (L62), transcript `aria-live="polite"` (L102) | | `hope-voice-api/web/favicon.svg` | Square Hope glyph: 64×64 rounded rect (`rx=16`) dark plate `#08111f`, iris circle + tilted orbit ellipse + warm dot — the canonical "square Hope" mark | | `hope_companion/scripts/gen-icons.mjs` | SDF-generated app icon: 1024² grid, rounded-rect plate (`sdRoundedRect … 512,512, r=228`) with indigo→violet diagonal gradient (43,46,122)→(124,58,237), white mark of two nodes + arc + status ring | | `hope_companion/resources/icon.png` | 1024×1024 RGBA output of the above generator (verified via `sips`: hasAlpha, RGB) | | `voice-ios/Sources/**` (all SwiftUI sources) | Current implementation audited below | | `voice-ios/project.yml`, `Resources/Assets.xcassets/AppIcon.appiconset/Contents.json` | Build config; app icon set is **empty** (no image assigned) | ### The "square Hope element" — resolved The square Hope visual element is the **LED-wall avatar**: a 320×320 px square container (`border-radius: 26px`) holding a 21×21 dot matrix that blooms with audio. Evidence: `style.css` L331–346 (`.led-wall { width: 320px; height: 320px; … grid-template-columns: repeat(21,1fr); border-radius: 26px }`), L349–359 (glass panel backing), L361–372 (`.led-dot`), L376–392 (state color palette), and `app.js` L265–341 (build + per-frame render). It is the single most prominent Hope visual in the web UI and sits at the center of the voice console. Two related but distinct marks also exist and are relevant to the iOS redesign: 1. **Favicon glyph** (`favicon.svg`): 64×64 rounded square, dark plate `#08111f`, iris circle `#8ba7ff` with tilted orbit ellipse `#526cae` and warm accent dot `#ffb38e`. This is the compact "Hope" identity mark. 2. **Companion app icon** (`hope_companion/resources/icon.png`, generated by `gen-icons.mjs`): 1024² rounded square (corner radius 228/1024 ≈ 22.3%) with an indigo→violet diagonal gradient and a white two-node + arc + ring mark. The current iOS app uses **none** of these: `OrbView.swift` renders a plain 120 pt `Circle()` filled with a system-color gradient (lines 45–47), and the app icon set is empty. This is the central brand gap this spec addresses. --- ## 2. Asset Provenance | Asset | Source | Format | Notes | |---|---|---|---| | LED-wall avatar | `hope-voice-api/web/style.css` L316–429 + `app.js` L265–341 | CSS grid + JS per-frame render | Not an image asset; must be re-implemented in SwiftUI (Canvas/TimelineView) | | Square Hope glyph (favicon) | `hope-voice-api/web/favicon.svg` | SVG 64×64 | Portable to iOS as a vector asset or SF Symbol-style shape | | Companion icon | `hope_companion/resources/icon.png` | PNG 1024² RGBA, generated by `scripts/gen-icons.mjs` (SDF) | Candidate base for the iOS AppIcon; generator is dependency-free Node | | Design tokens | `style.css` `:root` L1–26 | CSS custom properties | Full palette + easing below (§5) | | State palettes | `style.css` L376–392 | CSS `data-state` selectors | idle/connecting/thinking/listening/speaking colors | No binary image assets currently exist in `voice-ios/Resources/Assets.xcassets` beyond an empty `AppIcon.appiconset`. The redesign should add: (a) an `AppIcon` derived from the companion mark, (b) a `HopeGlyph` vector asset from the favicon, and (c) no raster assets for the LED wall (procedural rendering only). --- ## 3. Current State Audit (voice-ios) ### 3.1 Screen inventory | Screen | File | Navigation | Notes | |---|---|---|---| | Conversation (root) | `Sources/Features/Conversation/ConversationView.swift` | `NavigationStack`, `.navigationTitle("Hope Voice")` | Single screen; everything else is pushed or sheeted from the toolbar | | Orb + status header | `ConversationView.statusHeader` L63–125, `OrbView.swift` | — | 120 pt circle, system colors | | Transcript list | `OrbView.swift` (TranscriptListView) L64–146 | Inline in root VStack | Plain rows, role name + timestamp, no bubbles/avatars | | Composer | `ConversationView.composer` L161–191 | — | Start button / ProgressView / TextField+Send+End | | Settings | `Sources/Features/Settings/SettingsView.swift` | Pushed from toolbar gear | Form: Server URL, Voice, Model, Text mode, Connection test; voice/model pickers as sheets | | Connection test | `Sources/Features/Settings/ConnectionTestView.swift` | Pushed from Settings | Token endpoint probe | | Tasks panel | `Sources/Features/Tasks/TaskPanelView.swift` | Pushed from toolbar tray icon (badge) | Cards with status dot, steps, result/error blocks; DEBUG "Sample" button | | Media panel | `Sources/Features/Media/MediaPanelView.swift` | Pushed from toolbar music icon | Now-playing card, transport, seek, volume, pending jobs, choices, search, library, add-stream | ### 3.2 Navigation & hierarchy findings - **N1 — Flat toolbar with three unrelated destinations.** Tasks, Media, and Settings are all `.topBarTrailing` pushes (ConversationView L193–229). There is no tab bar or grouped navigation; the web UI keeps these as side panels/sheets. Acceptable on iOS, but the icon set (`tray.full`, `music.note`, `gearshape`) carries no brand and the order puts Settings last while it is the most-used config surface. - **N2 — No persistent identity.** The web header shows the brand glyph + "Hope Voice" + presence dot + profile chip (index.html L18–39). iOS shows only a plain nav title; no Hope mark anywhere in the UI. - **N3 — Session lifecycle is not visually distinct.** Idle, connecting, waitingAgent, ready, and failed all render in the same layout with only text changes (ConversationView L148–163, L248–263). The web UI has a dedicated console-intro state (style.css L290–314) before connecting. ### 3.3 Component & state findings - **C1 — OrbView is not the Hope element.** `OrbView.swift` L44–56: a `Circle()` with `.blue/.green/.orange/.red` system gradients and a uniform `scaleEffect` pulse. It matches none of the web LED-wall behavior (radial bloom, per-dot phase ripple, audio-reactive dot sizing) and uses colors outside the Hope palette (§5). - **C2 — State palette mismatch.** OrbView maps listening→green, speaking/thinking→orange (L17–24), while the web uses cool blue for listening (`#6f8ff7`), warm amber for thinking/connecting (`#d99a6c`), and light periwinkle for speaking (`#9dc2ff`) (style.css L376–392). The iOS colors contradict the brand's "cool while it listens, warm toward the center as it speaks" intent. - **C3 — No audio-reactive rendering.** `ConversationStateModel` exposes smoothed `micLevel`/`agentLevel` (ConversationState.swift L18–19) but nothing consumes them for visuals except a 4 pt mic meter bar (ConversationView L127–138). The web drives the entire avatar from these levels every frame (app.js L300–341). - **C4 — Transcript rows are not chat bubbles.** `TranscriptListView.transcriptRow` (OrbView.swift L111–137) renders full-width stacked cards with a role-name caption. The web uses aligned bubbles: user right-aligned `#1b3150` with bottom-right corner cut, agent left-aligned `#14243a` with bottom-left cut, 27 px avatar dot for the agent, partials at opacity 0.58 (style.css L714–756). Partial/final distinction on iOS is only an opacity change on plain text (L117, L130). - **C5 — No message entrance motion.** Web rows animate in with `message-in` (translateY 5px + fade, style.css L709–712); iOS rows appear instantly. - **C6 — Thinking chip is plain.** A capsule with SF Symbol "ellipsis" (ConversationView L68–74) vs the web's amber pulsing dot + text (`think-pulse`, style.css L409–429). - **C7 — Task status dots lack motion.** `TaskPanelView` uses static colored circles; the web's running dot pulses (`status-pulse`, style.css L1125–1135). - **C8 — Composer states are abrupt.** Idle shows a Start button, connecting shows a bare `ProgressView` (L169–170), ready shows a disabled TextField in voice mode with no explanation beyond placeholder text. The web's composer is always present and the mic meter sits directly above it (index.html L85–107). - **C9 — Empty app icon.** `Resources/Assets.xcassets/AppIcon.appiconset/Contents.json` has no image entries; the app ships with the default iOS placeholder. - **C10 — No Dynamic Type / reduced-motion handling anywhere.** No `@Environment(\.dynamicTypeSize)`, no `accessibilityReduceMotion` checks in any view. - **C11 — Accessibility is label-only.** Phase 8 added labels/values (OrbView L58–60, toolbar buttons L211/219/227), but the LED wall equivalent on web is `aria-hidden` with a separate `aria-live="polite"` status region (index.html L61–62). iOS's OrbView combines label+value on one element, which is fine, but transcript updates are not announced (`TranscriptListView` has no `accessibilityLiveRegion` equivalent — SwiftUI: needs `.accessibilityLabel` on rows + possibly UIAccessibility notifications for new finals). ### 3.4 What is already good (keep) - State machine port is faithful: EMA 0.6/0.4, thresholds 0.035/0.02, thinking chip after 2 s (ConversationState.swift L12–89) — matches app.js exactly. - Stale-room guards, audio interruption handling, error banners (Phase 8). - Media panel structure (now-playing → transport → seek → volume → jobs → choices → search → library → add-stream) is a sensible vertical flow for iPhone. - Task card information design (status dot + description + role-styled steps + result/ error blocks) mirrors the web well. --- ## 4. Navigation & Hierarchy Specification ### Target structure ``` Root: ConversationScreen (NavigationStack) ├── Header (inline, not nav-title-only) │ ├── HopeGlyph (28×28, favicon mark) + "Hope Voice" title │ └── Toolbar: [Tasks (badge)] [Media] [Settings] ├── PresenceStage (replaces statusHeader) │ ├── HopeLedWall (160–200 pt square, procedural) │ ├── StateLabel (aria-live equivalent) │ ├── ThinkingChip / BargeInHint │ ├── MicMeter (voice mode) │ └── Voice·Model caption + Agent caption ├── TranscriptList (bubbles, §6.3) └── ComposerBar (always visible; content by session state, §6.4) Pushed: TaskPanelView, MediaPanelView, SettingsView (unchanged destinations) Sheets: VoicePickerView, ModelPickerView (unchanged) ``` Changes vs current: 1. **Move mode toggles out of the header.** The mic/text-mode toggles currently live in the status header (ConversationView L93–105), making it tall and busy. Move them into the ComposerBar as a segmented control (Voice | Text) — matching the web's `mode-switch` group (index.html L26–34). The header keeps at most: wall, label, chip, meter, one caption line. 2. **Add an intro/landing state.** When `.idle`, render a compact console-intro block (brand glyph large + "Hope is ready when you are" + Start button) instead of the full transcript layout — mirrors web `.console-intro` (style.css L290–314). This makes the first-run experience branded rather than blank. 3. **Keep three toolbar items** but reorder: Tasks, Media, Settings is fine; ensure the tasks badge uses `--danger`/red only for *failed* and iris-blue for *active* (see §6.5). --- ## 5. Visual Language & Tokens ### 5.1 Color tokens (from style.css L1–26, ported to SwiftUI) ```swift enum HopeColor { static let canvas = Color(hex: 0x08111F) // page background static let canvasDeep = Color(hex: 0x050B14) static let surface = Color(hex: 0x0D1929) static let surfaceRaised = Color(hex: 0x132238) static let surfaceSoft = Color(hex: 0x172941) static let line = Color(hex: 0xB7CCEB, opacity: 0.13) static let lineStrong = Color(hex: 0xB7CCEB, opacity: 0.22) static let ink = Color(hex: 0xF4F7FB) static let inkSoft = Color(hex: 0xC5D1E2) static let inkMuted = Color(hex: 0x8395AD) static let iris = Color(hex: 0x8BA7FF) // primary accent static let irisStrong = Color(hex: 0x6F8FF7) static let irisDeep = Color(hex: 0x3C5FC9) static let warm = Color(hex: 0xFFB38E) // secondary accent static let mint = Color(hex: 0x76D9B2) // success static let danger = Color(hex: 0xFF806F) // error static let userBubble = Color(hex: 0x1B3150) static let agentBubble = Color(hex: 0x14243A) static let focus = Color(hex: 0xB5C5FF) } ``` LED-wall state colors (style.css L376–392): | State | Web color | SwiftUI | |---|---|---| | idle | `#56739E` | `Color(hex: 0x56739E)` | | connecting / thinking | `#D99A6C` | `Color(hex: 0xD99A6C)` | | listening | `#6F8FF7` | `HopeColor.irisStrong` | | speaking | `#9DC2FF` + drop-shadow `rgba(111,143,247,0.35)` @14px | `Color(hex: 0x9DC2FF)` + `.shadow(color: .iris.opacity(0.35), radius: 14)` | **Rule:** replace all system `.blue/.green/.orange/.red` in OrbView, mic meter, chips, and banners with the tokens above. Semantic mapping: success=mint, warning/danger= danger (not system red), thinking=warm, listening=irisStrong, speaking=#9DC2FF. ### 5.2 Shape & surface tokens - Corner radii: panels/cards 14 pt (matches current TaskCardView), LED-wall plate 26 pt at 320 px web → scale to ~13–18 pt at 160–200 pt iOS size (keep the 26/320 ≈ 8% ratio). Bubbles 12 pt with one corner cut to 3 pt (user: bottom-right; agent: bottom-left) — style.css L732–751. - Backing surfaces: LED-wall glass panel = radial gradient `rgba(111,143,247,0.10)` center → transparent 62%, over `rgba(6,12,24,0.4)`, with a 1 pt border `rgba(139,167,255,0.14)` (style.css L349–359). - Easing: `cubic-bezier(0.22, 1, 0.36, 1)` (`--ease-out`) for all state transitions; dot-level changes use 90 ms linear (style.css L371); wall filter transition 220 ms ease (L345). ### 5.3 Typography Keep SwiftUI defaults but align scale with web: status label = `.headline` (web 0.83rem semibold), captions `.caption`/`.caption2`, transcript body `.body` at line-height 1.55 (web L737–738). No custom font is used by the web (system stack) — do not introduce one. --- ## 6. Screen & Component Specifications ### 6.1 HopeLedWall (replaces OrbView) **Geometry:** square container, size `min(200, availableWidth - 32)` pt, corner radius ≈ 8% of side. Inside: a 21×21 dot grid with 2 pt gap and 12 pt padding (web L337–342). At 180 pt the dots are ~5 pt base size — same as web's `--sz: 5px` (L362). **Implementation:** SwiftUI `Canvas` inside a `TimelineView(.animation)` (or `.periodic(from:by: 1/30)` to cap cost; web renders every rAF but iOS should cap at 30 fps for battery). For each of the 441 dots compute, porting app.js L298–341 verbatim: ``` rad = sqrt(dx² + dy²) // 0 center → ~1.41 corner; web normalizes by (COLS/2) phase = hash(r,c) * 2π // deterministic per-dot phase (app.js L290 formula) level = speaking ? agentLevel : listening ? micLevel : 0 // from ConversationStateModel idle: vis = 0.16 - rad*0.08 ; size = 1 signal: lvl = min(1, level*1.3) falloff = exp(-rad² * (2.0 - lvl*1.1)) ripple = 0.62 + 0.38*sin(t*0.32 + phase) vis = falloff * (0.35 + lvl) * ripple * (speaking ? 1 : 0.55) size = 1 + (1 - rad) * 1.4 * lvl no-signal: breathe = 0.5 + 0.5*sin(t*0.12 + phase) vis = 0.14 - rad*0.06 + 0.04*breathe ; size = 1 + (1-rad)*0.3 ``` Dot color = state palette (§5.1); opacity clamped [0.02, 1]; dot diameter = `2*size` pt base 5 pt → up to ~9 pt at center on strong signal. The wall itself gets the glass backing (§5.2) and, in speaking state, a soft outer glow (shadow radius 14, iris 35%). **Performance notes:** - Precompute `rad` and `phase` once into a `[Dot]` array; per-frame work is only the trig/exp math on 441 entries — trivial for Canvas. - Pause the TimelineView when phase == .idle (static matrix) or scene backgrounded. - Respect `accessibilityReduceMotion`: render the static idle matrix + state color only, no ripple/breathe; keep opacity changes but drop size animation. **Accessibility:** mark the wall `.accessibilityHidden(true)` and expose a separate status element with `.accessibilityLabel("Conversation status")` + `.accessibilityValue(stateText)` — exactly mirroring web's `aria-hidden` wall + `aria-live="polite"` label region (index.html L61–62). Announce phase changes via `UIAccessibility.post(notification: .announcement, …)` only on meaningful transitions (speaking start/end), not every tick. ### 6.2 PresenceStage layout (replaces statusHeader) ``` VStack(spacing: 12) { HopeLedWall(state:, level:) // §6.1 StateLabel(text: statusText) // .headline, inkSoft ThinkingChip() // when isThinkingChipVisible BargeInHint() // when textMode && ready MicMeter(level:) // voice mode only; 4 pt capsule, iris fill CaptionLine(voice · model) // .caption, inkMuted — one line, not two } ``` - Thinking chip: amber dot (5 pt, `warm`) with `think-pulse` equivalent (opacity 1→0.28, scale 1→0.75, 1.2 s ease-in-out infinite) + "Thinking…" caption — replaces the current ellipsis capsule (ConversationView L68–74). - Mic meter: fill color `irisStrong` (not system green), width = level × width × 2.5 (keep existing scaling, ConversationView L134). - Error banners keep their current placement but use `danger` token at 0.1 opacity background; reconnecting banner uses `warm`. ### 6.3 TranscriptList (bubble redesign) Port web bubble semantics (style.css L714–756): - **User row:** right-aligned, max width 82%, background `userBubble` (#1B3150), text #E8EFFF, corner radius 12 with bottom-right 3 pt. No avatar dot. - **Agent row:** left-aligned, max width 82%, background `agentBubble` (#14243A), text `inkSoft`, 27 px→24 pt avatar dot (iris ring + iris center, style.css L714–730), corner radius 12 with bottom-left 3 pt. - **Partial rows:** opacity 0.58 on the whole row (L753–756) — keep current behavior but apply to bubble background too. - **System rows:** centered, transparent, `inkMuted`, caption size (L702–707). - **Entrance animation:** `message-in` equivalent — opacity 0→1 + y +5→0 over ~180 ms ease-out on insert only (L709–712). Use `.transition(.opacity.combined(with: .offset(y: 5)))` with `withAnimation(.easeOut(duration: 0.18))` in the existing scroll-to-bottom path (OrbView.swift L102–106). - **Timestamps:** keep per-row timestamps but style them as web does — hidden by default, revealed on row focus/long-press (`message-row::after`, L758–764). Simplest iOS: show timestamp in caption2 `inkMuted` under the bubble (current behavior) — acceptable; long-press-to-reveal is a later-phase nicety. - **Accessibility:** each row gets `.accessibilityElement(children: .combine)` with label "\(roleName): \(text)" so VoiceOver reads one element per message; post an announcement on new *final* agent messages only. ### 6.4 ComposerBar (always visible) Replace the current state-swapped HStack (ConversationView L161–191) with a persistent bar: | Session state | Content | |---|---| | idle | Intro card above; bar shows prominent "Start conversation" button (iris fill, white text, 42 pt min height — web `.btn` metrics L444–453) | | connecting / waitingAgent | Bar disabled, shows ProgressView + "Connecting…" caption | | ready, voice mode | Segmented [Voice | Text] control; TextField hidden (placeholder area shows "Barge in anytime — speak or switch to text"); End button (danger outline) | | ready, text mode | Same segmented control; TextField enabled (rounded, surfaceRaised bg); Send button (iris) disabled when empty; End button | The Voice/Text segmented control replaces both current toggles (L95–104) and the Settings "Text mode" toggle stays in sync via `SettingsStore.textMode`. ### 6.5 TaskPanelView adjustments - Status dot: running = iris with `status-pulse` (opacity 1→0.3, 1.4 s infinite); completed = mint; failed = danger; pending = inkMuted (style.css L1120–1131). - Step role colors already mirror web (thinking=blue→iris, tool_call=indigo, etc.) — keep, but swap system blues for `iris`/`irisDeep`. - Toolbar badge on the tray icon: iris-blue background for active count; reserve red (`danger`) for a failed-task indicator if one is added later. Current red-always badge (ConversationView L206) reads as "error" — change to `iris`. ### 6.6 MediaPanelView adjustments - Keep layout; swap system colors: now-playing card bg `surfaceRaised`, transport accent iris, error banner danger/0.1, seek slider tint iris. - Now-playing art placeholder: use HopeGlyph at low opacity instead of a blank box. ### 6.7 Settings & pickers - Form sections are fine; add the HopeGlyph + "Hope Voice" header row at top for brand continuity (mirrors web header, index.html L18–39). - Connection test result colors: success mint, failure danger. --- ## 7. Motion Specification | Element | Trigger | Animation | Source | |---|---|---|---| | LED wall dots | every frame (≤30 fps) while active | opacity + size per §6.1 formulas; 90 ms linear feel via per-frame lerp | app.js L300–341, style.css L371 | | Wall state color | phase change | cross-fade 220 ms ease (filter transition) | style.css L345, L376–392 | | Speaking glow | entering .speaking | shadow radius 0→14 iris/35% over 220 ms | style.css L389–392 | | Thinking chip dot | visible | `think-pulse`: opacity 1→0.28, scale 1→0.75, 1.2 s ease-in-out ∞ | style.css L415–429 | | Message row insert | new event | fade + translateY(5→0), 180 ms ease-out | style.css L709–712 | | Task running dot | status running | `status-pulse`: opacity 1→0.3, 1.4 s ∞ | style.css L1125–1135 | | Buttons | tap/press | transform scale or color 180 ms `--ease-out` | style.css L453 | | State label text | phase change | no animation (instant) — keep text updates jank-free | — | **Reduce Motion:** when `accessibilityReduceMotion` is on: wall renders static per-state matrix (no ripple/breathe/size), thinking chip loses pulse (static dot), message rows appear without offset (fade only or instant), task dots static. All state *information* remains visible — motion is decorative. --- ## 8. Accessibility Specification 1. **Status announcements:** separate live status element (label "Conversation status", value = state text) distinct from the hidden wall (§6.1). Announce only on phase transitions to speaking/listening/failed, throttled to ≥2 s apart. 2. **Transcript:** combined elements per row (§6.3); announce new final agent messages; partials are not announced (they churn too fast — matches web `aria-live="polite"` on the container which coalesces). 3. **Reduce Motion** handled per §7. 4. **Dynamic Type:** all text uses semantic fonts (`.headline`, `.body`, `.caption`) — already true; verify bubble max-widths don't clip at accessibility sizes by allowing `maxWidth: 82%` to grow to 92% above `.accessibility1`. Wall size is fixed (decorative). 5. **Contrast:** token palette on canvas (#08111F): ink #F4F7FB ≈ 16:1, inkSoft #C5D1E2 ≈ 10:1, inkMuted #8395AD ≈ 4.6:1 (passes AA for large text; captions using it are ≥ caption size — flag as a watch-item), iris #8BA7FF on canvas ≈ 6.5:1. All pass WCAG AA except muted-on-canvas at small sizes, which should be reserved for secondary metadata only (timestamps, agent name). 6. **Touch targets:** all interactive controls ≥44 pt (web `.btn` min-height 42 px; iOS standard). Composer buttons and toolbar icons already comply via SF Symbol sizing — verify segmented control height ≥44. 7. **VoiceOver order:** header status → transcript (oldest→newest) → composer, matching visual order; the wall is hidden from VO (§6.1). --- ## 9. App Icon & Branding Assets 1. **AppIcon:** generate from `hope_companion/scripts/gen-icons.mjs` output (`resources/icon.png`, 1024²) — it already encodes the square Hope plate + mark and is dependency-free to regenerate. Add all required iOS sizes (60/58/40/29 @1x–3x, marketing 1024) to `AppIcon.appiconset`. Fallback if regeneration is undesirable: use the existing PNG directly; it has alpha and correct dimensions (verified via sips). 2. **HopeGlyph asset:** port `favicon.svg` into the asset catalog as a vector PDF/SVG or implement as a small SwiftUI shape view (rounded rect + circle + rotated ellipse + dot) for use in the header, empty states, and media placeholder. The SVG is 6 lines — trivial to hand-port with `Path`. 3. **No raster assets** for the LED wall — procedural only (§6.1). --- ## 10. Phased Implementation Plan ### Phase A — Brand foundation (low risk, high impact) - [ ] Add `HopeColor` token enum + `Color(hex:)` helper; replace system colors in OrbView, mic meter, chips, banners, task dots, media panel. - [ ] Add AppIcon (all sizes) from companion generator output. - [ ] Add HopeGlyph shape view; use in header row of Conversation and Settings. - **Exit:** app visually reads as "Hope" at a glance; zero system-green/orange/red left in conversation surfaces. ### Phase B — LED wall avatar (the core element) - [ ] Implement `HopeLedWall` (Canvas + TimelineView ≤30 fps) with the exact formulas from §6.1, driven by `ConversationStateModel.micLevel/agentLevel`. - [ ] Glass backing + speaking glow; state palette per §5.1. - [ ] Pause rendering when idle/backgrounded; reduce-motion fallback (§7). - [ ] Replace OrbView usage in ConversationView; keep the old file for one release as a `#if`-guarded fallback, then delete. - **Exit:** wall blooms with agent audio (visible in sim via text-mode TTS-driven state — note: sim mic is dead per PLAN.md Phase 2, so listening-side reactivity must be verified on device; speaking-side can be verified in simulator). ### Phase C — Transcript bubbles & composer - [ ] Bubble redesign per §6.3 (alignment, colors, corner cuts, avatar dot, entrance animation, combined VO elements). - [ ] Persistent ComposerBar with Voice/Text segmented control per §6.4; remove toggles from header. - [ ] Intro/landing state for `.idle` per §4.3. - **Exit:** transcript visually matches web bubbles; mode switch is one tap in the bar. ### Phase D — Polish pass - [ ] Thinking chip pulse, task running-dot pulse, button press easing (§7). - [ ] Toolbar badge color → iris (§6.5); media panel token swap (§6.6). - [ ] Dynamic Type audit at `.accessibility3`; contrast fixes for muted captions if needed. - **Exit:** side-by-side with web UI on same deployment shows consistent language; no motion under reduce-motion; VO walkthrough clean. Sequencing rationale: A is pure token/asset work (no behavior change). B is the signature element and isolates the only new rendering code. C changes information layout (transcript/composer) but reuses existing data flow. D is small deltas. Each phase is independently reviewable and revertible. --- ## 11. Acceptance Criteria (for the redesign implementation) 1. **Brand:** app icon set populated; HopeGlyph visible in conversation + settings headers; no system palette colors on conversation surfaces (grep for `.blue`, `.green`, `.orange`, `.red` in `Sources/Features` returns only token-enum internals). 2. **LED wall:** renders 21×21 grid; dot opacity/size respond to `agentLevel` while speaking (verifiable in simulator via TTS) and to `micLevel` while listening (device-only); idle shows static dim matrix; state colors match §5.1 table exactly. 3. **Performance:** wall animation holds 30 fps on iPhone 15-class hardware with no main-thread stalls (Instruments template: Time Profiler + SwiftUI view body count); TimelineView paused when `.idle` or backgrounded. 4. **Transcript:** user rows right-aligned #1B3150, agent rows left-aligned #14243A with avatar dot; partials at 0.58 opacity; new rows animate in (fade+5 pt rise); VO reads one element per message. 5. **Composer:** always visible; Voice/Text segmented control switches mode and syncs with `SettingsStore.textMode`; Start/End states per §6.4 table. 6. **Accessibility:** reduce-motion yields static wall + no pulses; status announcements fire on speaking/listening/failed transitions only; all interactive targets ≥44 pt. 7. **Build:** zero warnings; simulator build/run clean (XcodeBuildMCP); existing connect/disconnect/reconnect matrix from PLAN.md Phase 8 still passes. 8. **No regressions:** media reconciliation, task events, TLS pinning, persistence all unchanged (these phases touch only presentation layer). --- ## 12. Risks & Open Questions - **R1 — Sim mic limitation.** Listening-side wall reactivity cannot be verified in the simulator (CoreAudio -4010, PLAN.md Phase 2). Device testing required for that half of §6.1; speaking-side is verifiable in sim via agent TTS. - **R2 — Canvas performance on older devices.** 441 dots × per-frame trig at 30 fps is cheap on A15+ but should be profiled on the oldest supported device (iOS 17 floor = A14-class). Mitigation: drop to 15 fps or 17×17 grid if profiling shows headroom loss. - **R3 — Color(hex:) helper.** No such helper exists in the codebase; it is a new ~10-line utility. Trivial but must be added before Phase A color swaps. - **R4 — Web parity scope.** This spec deliberately does not port web-only features (keyboard shortcuts, drag-to-seek waveform) — consistent with PLAN.md's accepted gaps. - **Open question:** should the LED wall size scale with device width (e.g., larger on iPad-class or landscape)? Spec assumes iPhone portrait only per UPDATE_PATH.md scope; recommend fixed 180 pt for Phase B and revisit later.