--- phase: 17-ui-optimization-polish verified: 2026-06-18T00:00:00Z status: human_needed score: 10/10 decisions delivered in code (1 WARNING-class resize limitation, 4 device-only checks) behavior_unverified: 0 overrides_applied: 0 human_verification: - test: "On a phone (≤767px, e.g. iPhone 14 / Pixel 7) load /calendar and /lists. Confirm the New Event FAB sits fully above the BottomTabBar and the color-legend chips are not clipped behind the bar." expected: "FAB floats above the tab bar (not on the Admin tab); legend chips fully visible; CI overlap guard (layout.spec.ts) corroborates geometry on iphone/pixel profiles." why_human: "Pixel-accurate fixed-chrome overlap + safe-area-inset rendering on a real notched device cannot be fully judged from CSS source; CI guard runs headless WebKit/Chromium only." - test: "Resize a desktop browser window narrower than 768px (or rotate a tablet/foldable across the 767px breakpoint) with the Settings sheet, a credential sheet, or the admin reset sheet OPEN." expected: "Sheet should switch between centered-modal (desktop) and bottom-sheet (phone) geometry. NOTE: 17-REVIEW WR-01 documents that the `phone` snapshot is computed once per render with no matchMedia listener, so the sheet keeps stale geometry until an unrelated re-render. Confirm severity for this household's actual devices." why_human: "Resize-crossing-breakpoint re-render behavior is a runtime interaction; the static branches are correct at mount but do not re-evaluate. Operator decides if this WARNING blocks (real phones are always phones; impact is tablets/foldables/desktop-resize)." - test: "Install the PWA to a device home screen and inspect the app icon (especially Android adaptive/maskable rendering and iOS apple-touch-icon)." expected: "Maskable icon (icon-maskable-512.png) shows the family-house logo inside the safe zone with no clipping; favicon shows in browser tab; apple-touch-icon shows on iOS home screen." why_human: "Maskable safe-zone correctness and home-screen icon rendering are device/installer-specific (iOS-Safari standalone is device-only per CLAUDE.md); cannot be driven headless." - test: "Sign in (local auth, non-dev-bypass) and open Settings; tap 'Sign out'." expected: "Session is cleared and the app routes to /login; signing back in works. (Endpoint + client wiring verified in code; live round-trip confirms cookie clearing end-to-end.)" why_human: "Full logout round-trip (cookie cleared server-side + redirect) is a runtime/auth flow; code path is verified but live confirmation is prudent." --- # Phase 17: UI Optimization & Polish Verification Report **Phase Goal:** A visual-identity & polish pass for the PWA spanning three workstreams — (A) phone-layout polish (no fixed-chrome overlap), (B) branding assets (real logo + complete icon set), (C) theme-token groundwork — plus (D) UAT-surfaced UI fixes (logout, admin toasts, sheet centering, admin nav rework). **Verified:** 2026-06-18 **Status:** human_needed **Re-verification:** No — initial verification ## Goal Achievement Decisions D-01..D-10 stand in for REQ-IDs (no Success Criteria array in ROADMAP; goal is prose + decisions). All 10 decisions have corresponding, substantive, wired code in the codebase. The status is `human_needed` (not `passed`) because device-only checks (phone overlap on real hardware, PWA icon install, logout round-trip) and one WARNING-class resize limitation (17-REVIEW WR-01) require operator confirmation. ### Observable Truths (by Decision) | # | Decision / Truth | Status | Evidence | | ---- | ---------------- | ------ | -------- | | D-01 | Phone FAB no longer overlaps BottomTabBar; legend not occluded | ✓ VERIFIED | FAB `bottom: calc(var(--bottom-chrome-h) + var(--space-6))` (CalendarShell.tsx:470); phone `paddingBottom: var(--bottom-chrome-h)` (App.tsx:164); `--bottom-chrome-h: calc(56px + env(safe-area-inset-bottom,0px))` (tokens.css:69). Matches ROADMAP fix sketch exactly. | | D-02 | Permanent overlap regression guard in CI | ✓ VERIFIED | layout.spec.ts:209-231 "New Event FAB does not overlap BottomTabBar" — asserts `fabBox.bottom ≤ navBox.top`, phone-only (iphone/pixel), skipped on desktop. Shared-token approach chosen (the planner's default lean). | | D-03 | Real FamilySync logo (warm/rounded/at-home) committed as SVG; operator-approved | ✓ VERIFIED | logo.svg (warm peach gradient bg, rounded family-house mark, 512 viewBox). Accent #e8915a + `--brand-logo-border-radius: 0` documented as operator-approved (tokens.css:41,106). | | D-04 | Complete 7-asset icon set; proper separate maskable | ✓ VERIFIED | All 7 files present in public/ (logo.svg, favicon.svg, favicon.ico, icon-192/512, icon-maskable-512, apple-touch-icon) at real sizes (not stubs). Maskable is its own 8627B file, distinct from icon-512 (12056B). Manifest references it with `purpose: 'maskable'` (vite.config.ts:41). | | D-05 | Logo wired into BrandSlot, decorative, no LoginPage layout change | ✓ VERIFIED | BrandSlot.tsx swaps placeholder div for ``; `

FamilySync

` retained as page title; tokens drive size/radius. Seam contract honored. | | D-06 | tokens.css restructured to themeable layer; Schedule-X overrides intact; no new hardcoded colors | ⚠️ PARTIAL | Combined `:root, [data-theme="light"]` selector (tokens.css:16-17); `--sx-color-*` overrides remain inside the block after the theme-default import (tokens.css:127+). Groundwork-only (no dark palette/toggle) per scope. **Caveat:** sheet-centering work (17-05/06) added hardcoded px literals (`12px`, `480px`, boxShadow rgba) in component files — but these match pre-existing patterns and the color invariant (hex in tokens.css) holds. | | D-07 | Reachable Sign out clears session + routes to /login; best-effort on API failure | ✓ VERIFIED | SettingsSheet.tsx:522-554 always-visible "Sign out" control → `handleSignOut` (137-146): try `fetchLocalLogout()`, catch swallows error, then unconditional `navigate('/login')`. | | D-08 | Admin create-member + reset-password success toasts, ~3s auto-dismiss | ✓ VERIFIED | AdminPage.tsx: toast state + 3000ms `setTimeout` auto-dismiss (62-69); "Member added." (261), "Password reset." (1090); `role="status"` live region (1030). | | D-09 | Dialogs/sheets centered on desktop, bottom-sheet on phone | ⚠️ PARTIAL | SettingsSheet (204-233), CredentialSheet (178+), AdminPage reset sheet (1419-1443) all have phone (`bottom:0`) vs desktop (`translate(-50%,-50%)`, maxWidth 480px) branches. **Caveat:** branch is a one-shot render snapshot (17-REVIEW WR-01) — does not re-evaluate on resize across 767px. Correct for real phones; stale on tablet/foldable/desktop-resize. | | D-10 | Two-tab ARIA strip with roving tabindex + ArrowLeft/Right keyboard nav | ✓ VERIFIED | AdminPage.tsx: `role="tablist"` (321), `role="tab"`+`aria-selected`+roving `tabIndex` 0/-1 (331-358), `handleTabKeyDown` ArrowRight/Left with focus management (205-220), `role="tabpanel"` ×2. CI-tested in admin.spec.ts (ArrowRight/Left switch + default selection). | **Score:** 10/10 decisions delivered in code (8 fully VERIFIED, 2 PARTIAL with documented WARNING-class caveats). 0 behavior-unverified, 0 FAILED, 0 BLOCKER. ### Required Artifacts | Artifact | Expected | Status | Details | | -------- | -------- | ------ | ------- | | `apps/pwa/src/styles/tokens.css` | Themeable layer + `--bottom-chrome-h` | ✓ VERIFIED | Combined selector + token defined; sx overrides intact | | `apps/pwa/public/logo.svg` + 6 icons | Brand mark + complete set | ✓ VERIFIED | All 7 real files; maskable separate | | `apps/pwa/pwa-assets.config.ts` + package.json | generator config + script | ✓ VERIFIED | Both exist; reads logo.svg | | `apps/pwa/e2e/layout.spec.ts` | FAB↔BottomTabBar overlap guard | ✓ VERIFIED | D-01 regression test present | | `apps/pwa/src/components/BrandSlot.tsx` | logo img swapped in | ✓ VERIFIED | Decorative img, h1 retained | | `apps/pwa/index.html` | favicon links + theme-color | ✓ VERIFIED | favicon.svg + .ico + apple-touch + #e8915a | | `apps/pwa/src/components/SettingsSheet.tsx` | Sign out + centering | ✓ VERIFIED | handleSignOut + desktop branch | | `apps/pwa/src/components/CredentialSheet.tsx` | centering branch | ✓ VERIFIED | phone/desktop branch present | | `apps/pwa/src/routes/AdminPage.tsx` | toasts + tablist + reset centering | ✓ VERIFIED | All present | | `apps/pwa/e2e/admin.spec.ts` | tab ARIA + keyboard + toast tests | ✓ VERIFIED | 9 tab assertions + toast structure test | ### Key Link Verification | From | To | Via | Status | Details | | ---- | -- | --- | ------ | ------- | | tokens.css | @schedule-x/theme-default | sx overrides after import | ✓ WIRED | tool-verified | | pwa-assets.config.ts | public/logo.svg | images: ['public/logo.svg'] | ✓ WIRED | tool-verified | | CalendarShell.tsx | tokens.css | FAB reads `var(--bottom-chrome-h)` | ✓ WIRED | **Manually verified** (CalendarShell.tsx:470) — gsd-tools reported false-negative due to over-escaped regex `var\\(--bottom-chrome-h\\)`; pattern is present and correct. | | App.tsx | tokens.css | phone paddingBottom reads token | ✓ WIRED | **Manually verified** (App.tsx:164) — same false-negative; pattern present. | | vite.config.ts | icon-maskable-512.png | manifest `purpose: maskable` | ✓ WIRED | tool-verified | | BrandSlot.tsx | public/logo.svg | img src /logo.svg | ✓ WIRED | tool-verified | | SettingsSheet.tsx | api/client.ts | handleSignOut calls fetchLocalLogout | ✓ WIRED | tool-verified | | AdminPage.tsx | SyncStateToast.tsx | reuse toast visual pattern | ✓ WIRED (pattern) | gsd-tools reported "target not referenced" — the plan `via` says reuse the *visual pattern*, not import. Toast uses `role="status"` + 3s auto-dismiss as specified (AdminPage.tsx:1030,66-69). Intent satisfied. | ### Behavioral Spot-Checks | Behavior | Command | Result | Status | | -------- | ------- | ------ | ------ | | Overlap guard test exists | grep "New Event FAB does not overlap BottomTabBar" layout.spec.ts | 1 match | ✓ PASS | | Admin tab tests exist | grep "getByRole('tab'" admin.spec.ts | 9 matches | ✓ PASS | | Tab keyboard switching exercised in CI | ArrowRight/ArrowLeft → aria-selected assertions | present (admin.spec.ts:134-151) | ✓ PASS (behavior-dependent truth has CI coverage) | | All 7 branding assets present + real sizes | ls public/ + file | logo.svg, favicon.svg/.ico, icon-192/512, maskable-512, apple-touch all >900B PNG/SVG | ✓ PASS | Production build / typecheck / eslint (0 warnings) / 266 unit tests already passing per phase context — relied upon, not re-run. ### Requirements Coverage No REQ-IDs; D-01..D-10 serve as requirements. Coverage: D-01/D-02→17-03 ✓, D-03/D-04→17-02 ✓, D-04/D-05→17-04 ✓, D-06→17-01 ✓(⚠), D-07/D-09→17-05 ✓(⚠), D-08/D-09/D-10→17-06 ✓. All decisions mapped to a plan and delivered. ### Anti-Patterns Found | File | Line | Pattern | Severity | Impact | | ---- | ---- | ------- | -------- | ------ | | SettingsSheet/CredentialSheet/AdminPage | sheet branches | Hardcoded px (`12px`, `480px`, boxShadow rgba) in component files | ℹ️ Info | Layout primitives, not colors; matches pre-existing patterns; color invariant (hex→tokens.css) intact | | SettingsSheet.tsx | various | `phone = matchMedia(...)` one-shot, no listener | ⚠️ Warning | 17-REVIEW WR-01 — stale sheet/FAB geometry on resize across 767px (tablet/foldable/desktop-resize). Not a blocker for real phones. | | AdminPage / dialogs | — | Hardcoded z-index literals (300/301/302/303) | ℹ️ Info | 17-REVIEW WR — duplicated magic numbers, no current layering bug | No TBD/FIXME/XXX debt markers found in phase-modified files. No BLOCKER-class issues (17-REVIEW: 0 critical). ### Human Verification Required See frontmatter `human_verification` — 4 items: (1) phone overlap on real device, (2) **resize-across-breakpoint sheet geometry [WR-01 severity call]**, (3) PWA maskable/home-screen icon install, (4) logout round-trip. Items 1 and 3 are genuinely device-only (per CLAUDE.md iOS-Safari/install exception). Items 1, 2, 4 could alternatively be spot-checked via playwright-cli on Chromium if desired. ### Gaps Summary No goal-blocking gaps. Every decision D-01..D-10 is delivered with substantive, wired code, and the two automated CI guards the phase promised (FAB overlap in layout.spec.ts, admin tab ARIA/keyboard in admin.spec.ts) exist and assert real geometry/state transitions. The phase achieves its goal in the codebase. Two WARNING-class caveats (both pre-documented in 17-REVIEW, both WARNING not BLOCKER) and four human/device confirmations keep the verdict at `human_needed` rather than `passed`: - **WR-01 (resize snapshot)** is the one substantive behavioral caveat — sheet/FAB geometry does not re-evaluate when the viewport crosses 767px after mount. For the two-person household's actual phones this is invisible (a phone is always a phone); it only manifests on tablet rotation or desktop-window narrowing. Operator should confirm this is acceptable for the milestone or fold the `useIsPhone()` fix (already sketched in 17-REVIEW) into a follow-up. - The hardcoded-px additions in sheet branches are a minor invariant softening (D-06 is primarily a *color* invariant, which holds), recorded as Info. --- _Verified: 2026-06-18_ _Verifier: Claude (gsd-verifier)_