---
phase: 17
slug: ui-optimization-polish
status: draft
nyquist_compliant: false
wave_0_complete: false
created: 2026-06-18
---

# Phase 17 — Validation Strategy

> Per-phase validation contract for feedback sampling during execution.
> Source: `17-RESEARCH.md` §Validation Architecture. Requirement IDs are TBD for this
> phase (UI/UX polish + branding) — decisions D-01…D-10 (CONTEXT.md) and workstreams
> A–D (UI-SPEC.md) stand in for REQ-IDs until promoted in plan-phase.

---

## Test Infrastructure

| Property | Value |
|----------|-------|
| **E2E Framework** | Playwright 1.60.0 |
| **Unit Framework** | Vitest ^4.1.8 |
| **E2E config file** | `apps/pwa/playwright.config.ts` (profiles: `iphone` 390×844 WebKit, `pixel` 412×915 Chromium, `desktop` 1280×720) |
| **Unit config file** | `apps/pwa/vitest.config.ts` |
| **Quick run command** | `pnpm --filter @familysync/pwa exec playwright test --project=pixel layout.spec.ts` |
| **Full suite command** | `pnpm --filter @familysync/pwa test:e2e` |
| **Build gate** | `pnpm --filter @familysync/pwa build` |
| **Estimated runtime** | ~45–90 seconds (quick: ~15s; full 3-profile suite: ~60–90s) |
| **Browser-verification skill** | `playwright-cli` (desktop/Chromium UI checks the spec suite cannot cover) |

---

## Sampling Rate

- **After every task commit:** Run `pnpm --filter @familysync/pwa exec playwright test --project=pixel layout.spec.ts` (phone profile covers the primary fix geometry)
- **After every plan wave:** Run `pnpm --filter @familysync/pwa test:e2e` (full 3-profile suite) + `pnpm --filter @familysync/pwa build`
- **Before `/gsd-verify-work`:** Full Playwright suite green + playwright-cli visual sweeps passed + **human logo approval received** (Workstream B checkpoint)
- **Max feedback latency:** ~90 seconds

---

## Per-Task Verification Map

> Task IDs are assigned by the planner. This map is workstream/decision-keyed; the executor
> binds each task ID to the matching row's automated command during execution.

| Decision | Workstream | Behavior | Test Type | Automated Command / Method | Automated? | Status |
|----------|-----------|----------|-----------|----------------------------|------------|--------|
| D-01/D-02 | A | FAB bottom edge ≤ BottomTabBar top edge on iphone/pixel | Playwright geometry assertion | **New** assertion in `apps/pwa/e2e/layout.spec.ts` (skipped on desktop) | CI (iphone + pixel) | ⬜ pending |
| D-01 | A | Color-legend chips visible; content scrollable above bar | Playwright + playwright-cli sweep | `layout.spec.ts` full suite + manual scroll | Partial | ⬜ pending |
| D-01 | A | No horizontal overflow after fix (`scrollWidth ≤ clientWidth`) | Playwright Rule 2 | Existing `layout.spec.ts` Rule 2 | CI (all profiles) | ⬜ pending |
| D-01 | A | Tap targets preserved (≥44px / ≥56px) | Playwright Rule 1 | Existing `layout.spec.ts` Rule 1 | CI (all profiles) | ⬜ pending |
| D-03/D-04 | B | All 7 asset files present in `apps/pwa/public/` | Shell existence check | `ls apps/pwa/public/{logo.svg,favicon.svg,favicon.ico,icon-192.png,icon-512.png,icon-maskable-512.png,apple-touch-icon.png}` | CI gate / Wave 0 | ⬜ pending |
| D-04 | B | `favicon.ico` is multi-size & non-trivial (>100 bytes) | Shell | `test $(wc -c < apps/pwa/public/favicon.ico) -gt 100` | CI gate | ⬜ pending |
| D-04 | B | `icon-maskable-512.png` is exactly 512×512 (real safe-zone) | Node + sharp metadata | `node -e "require('sharp')('apps/pwa/public/icon-maskable-512.png').metadata().then(m=>console.log(m.width,m.height))"` | Scripted | ⬜ pending |
| D-04 | B | PWA manifest + index.html icon links resolve (no 404) | Playwright | `page.goto('/')`, devtools manifest / network check | playwright-cli | ⬜ pending |
| D-03 | B | Logo meets warm/rounded/at-home brief | Human review | playwright-cli screenshot + operator approval | **Human checkpoint** | ⬜ pending |
| D-05 | B | BrandSlot swap: no LoginPage layout shift; `<h1>` retains name; img `alt=""` | Playwright + playwright-cli | login route render + a11y name check | playwright-cli | ⬜ pending |
| D-06 | C | No hard-coded hex/px literals in component files | `grep` | `grep -rnE '#[0-9a-fA-F]{3,6}\|[0-9]+px' apps/pwa/src/components/ apps/pwa/src/routes/` (expect token refs only) | CI gate | ⬜ pending |
| D-06 | C | Schedule-X `--sx-color-*` overrides still apply post-restructure | playwright-cli | visual sweep of `/calendar` | playwright-cli | ⬜ pending |
| D-06 | C | TypeScript + Vite build exits 0 | Build | `pnpm --filter @familysync/pwa build` | CI | ⬜ pending |
| D-07 | D | "Sign out" control renders in SettingsSheet | Playwright + playwright-cli | open settings sheet, confirm button + aria-label | playwright-cli | ⬜ pending |
| D-07 | D | Logout calls `fetchLocalLogout()` then navigates to `/login` | Playwright interaction | click Sign out, confirm redirect | playwright-cli | ⬜ pending |
| D-08 | D | "Member added." toast on create success | Playwright interaction | admin: create member, confirm toast (role=status) | playwright-cli (admin) | ⬜ pending |
| D-08 | D | "Password reset." toast on reset success | Playwright interaction | admin: reset password, confirm toast | playwright-cli (admin) | ⬜ pending |
| D-08 | D | Toast auto-dismisses after ~3s | Playwright interaction | wait 3.5s, confirm gone | playwright-cli | ⬜ pending |
| D-09 | D | Sheet centered on desktop (`top/left 50%`, transform) | Playwright geometry | playwright-cli @1280×720 SettingsSheet geometry | playwright-cli (desktop) | ⬜ pending |
| D-09 | D | Sheet renders as bottom-sheet on phone | Playwright geometry | playwright-cli @390×844 geometry | playwright-cli (phone) | ⬜ pending |
| D-10 | D | Admin two-tab strip renders with correct labels | Playwright | `/admin`: confirm "Members & Accounts" + "Settings" | playwright-cli (admin) | ⬜ pending |
| D-10 | D | ArrowLeft/ArrowRight switches tabs (roving tabindex) | Playwright keyboard | focus tab, ArrowRight, confirm active | playwright-cli | ⬜ pending |
| D-10 | D | Admin tab ARIA roles present (`tablist`/`tab`/`aria-selected`/`tabpanel`) | Playwright | **New** assertion in `admin.spec.ts` or `layout.spec.ts` via `getByRole` | CI | ⬜ pending |

*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*

---

## Wave 0 Requirements

- [ ] **Overlap regression assertion** — add the FAB↔BottomTabBar geometry test to `apps/pwa/e2e/layout.spec.ts` (Workstream A; runs on iphone + pixel, skipped on desktop). Contract in UI-SPEC §"Regression guard".
- [ ] **Admin tab ARIA assertion** — add `role="tablist"`/`role="tab"`/`aria-selected`/`role="tabpanel"` checks to `apps/pwa/e2e/admin.spec.ts` (or append to `layout.spec.ts`) (Workstream D-10).
- [ ] **Asset existence check** — Wave 0 npm script or inline CI step asserting all 7 `apps/pwa/public/` assets exist with valid format (ICO size, maskable dimensions).
- [ ] **`@vite-pwa/assets-generator` devDependency** — install in `apps/pwa` (pulls `sharp` + `sharp-ico`; the only new package) before Workstream B asset derivation runs.

*Existing infrastructure (Playwright 3-profile config + `layout.spec.ts` Rules 1–4 + Vitest) covers the bulk of the structural assertions.*

---

## Manual-Only Verifications

| Behavior | Decision | Why Manual | Test Instructions |
|----------|----------|------------|-------------------|
| Logo / icon set meets warm/rounded/at-home brief | D-03 | Aesthetic judgment; AI-generated art needs operator sign-off | playwright-cli screenshot of BrandSlot on `/login` + favicon in tab; operator approves at checkpoint before wiring is committed |
| Brand accent selection (cool-blue vs warm rose/amber) | UI-SPEC Q1 | Subjective brand decision | Present both variants (one-line token swap); operator selects at logo checkpoint |
| Maskable adaptive-icon render on a real device | D-04 | iOS/Android home-screen mask is device-only (cannot be driven by playwright-cli) | Install PWA on a device, confirm logo within safe-zone (optional spot-check; format validated automatically) |
| Schedule-X calendar colors visually unchanged post-restructure | D-06 | Visual regression of third-party calendar theme | playwright-cli sweep of `/calendar` before/after the tokens.css restructure |

---

## Validation Sign-Off

- [ ] All tasks have an automated verify command or a Wave 0 dependency
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
- [ ] Wave 0 covers all MISSING references (overlap assertion, admin ARIA assertion, asset-existence check, assets-generator install)
- [ ] No watch-mode flags (`playwright test` / `vitest run` are one-shot)
- [ ] Feedback latency < 90s
- [ ] `nyquist_compliant: true` set in frontmatter

**Approval:** pending
