diff --git a/.planning/phases/17-ui-optimization-polish/17-RESEARCH.md b/.planning/phases/17-ui-optimization-polish/17-RESEARCH.md new file mode 100644 index 0000000..a51b25a --- /dev/null +++ b/.planning/phases/17-ui-optimization-polish/17-RESEARCH.md @@ -0,0 +1,515 @@ +# Phase 17: UI Optimization & Polish — Research + +**Researched:** 2026-06-18 +**Domain:** React PWA polish, branding asset toolchain, CSS token architecture +**Confidence:** HIGH (fix surfaces grounded against actual codebase; toolchain decision based on registry verification and official documentation) + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- **D-01:** Fix BottomTabBar/FAB/colour-legend overlap AND run bounded small-viewport sweep using layout.spec.ts assertions as the checklist. Fix what they flag; no free-form audit. +- **D-03:** Logo + full icon set are AI-generated in-phase by Claude. User approves before assets are finalised. No external designer / user-supplied art. +- **D-04:** Deliver the complete icon set: `favicon.ico` + `favicon.svg`, `icon-192.png`, `icon-512.png`, a proper maskable `icon-maskable-512.png` (safe-zone padded), `apple-touch-icon.png` (180×180). Update `index.html` and `vite.config.ts` manifest. +- **D-05:** Drive logo through the existing BrandSlot seam: swap placeholder `
` for ``, override `--brand-logo-*` tokens. No LoginPage layout changes. Accessibility shape unchanged (`

` carries name, logo is decorative `alt="" aria-hidden`). +- **D-06:** Restructure `tokens.css` into a themeable layer (`:root, [data-theme="light"]` combined selector). Groundwork only — no dark palette values, no `prefers-color-scheme` wiring, no toggle. Schedule-X `--sx-color-*` overrides must remain working. +- **D-07 (F-02):** Wire logout control in SettingsSheet (and/or AppNav) calling existing `fetchLocalLogout()` (client.ts:124) then navigate to `/login`. No backend work. +- **D-08 (F-01):** Add success toast/confirmation to AdminPage create-member and reset-password flows. No backend work. +- **D-09 (F-03):** Fix dialog/sheet centering — bottom-sheet on phone (unchanged), centered modal on desktop. Applies to SettingsSheet, ChangePasswordSheet, LinkOidcSheet, CredentialSheet, admin reset-password sheet. +- **D-10 (F-04):** Rework admin navigation as a two-tab strip ("Members & Accounts" / "Settings") with full ARIA tabs pattern (roving tabindex, ArrowLeft/Right keyboard). +- **D-02:** Fix technique and regression-guard mechanism are researcher/planner's call. (Resolved by UI-SPEC: shared `--bottom-chrome-h` token + permanent overlap assertion in `layout.spec.ts`.) + +### Claude's Discretion +- D-02: fix technique + regression guard (resolved by UI-SPEC to: shared token + CI assertion). +- Exact small-viewport issues surfaced by layout.spec sweep — fix as found, within no-behaviour-change boundary. +- Logo visual execution within the brand brief — subject to user approval at checkpoint. + +### Deferred Ideas (OUT OF SCOPE) +- **Shipped dark theme + light/dark/system toggle** → backlog 999.20. Phase 17 ships enabling groundwork only. +- **Modern visual styling refresh** (login/calendar/event-form/lists/admin restyle) → backlog 999.21. + + +--- + +## Summary + +Phase 17 is a well-specified, bounded polish and branding pass across four workstreams on an existing shipped React 19 + Vite PWA. The UI-SPEC (17-UI-SPEC.md, approved 2026-06-18) resolves all design decisions. This research focuses on the one genuine implementation gap — the branding asset-generation toolchain — plus validation architecture and a grounded code-surface audit. + +**Workstream A (phone layout):** The defect is confirmed in source: `CalendarShell.tsx` FAB uses `bottom: 'var(--space-6)'` (24px), which places it behind the 56px BottomTabBar. The fix is a shared CSS custom property `--bottom-chrome-h` consumed by both the FAB offset and `App.tsx contentStyle`. This is a CSS-only change touching three files. + +**Workstream B (branding assets):** Sharp is not installed in this monorepo. The recommended toolchain is `@vite-pwa/assets-generator` (CLI mode, `minimal-2023` preset) which depends on `sharp` and `sharp-ico` internally, generates all seven required assets from a single source SVG, and is purpose-built for the vite-plugin-pwa ecosystem. The source logo is hand-authored SVG (warm/rounded/caricature-family brief) — this is the most reproducible, version-controlled, deterministic path. + +**Workstream C (token groundwork):** The `tokens.css` restructure is a single-selector change: `:root` becomes `:root, [data-theme="light"]`. The Schedule-X overrides remain inside the same rule block; no cascade order changes. + +**Workstream D (UAT fixes):** All four fixes are confirmed wirable against existing symbols — `fetchLocalLogout()` exists at `client.ts:124`, `SyncStateToast.tsx` provides the visual toast pattern, `AdminPage.tsx` has `useMutation` infrastructure already, and dialog centering is a `window.matchMedia` branch on the outer `
` style. + +**Primary recommendation:** Use `@vite-pwa/assets-generator` CLI (v1.0.2) with the `minimal-2023` preset + `overrideManifestIcons: false` (manual manifest update for explicit control). Source logo: hand-authored SVG committed to `apps/pwa/public/logo.svg`. + +--- + +## Architectural Responsibility Map + +| Capability | Primary Tier | Secondary Tier | Rationale | +|------------|-------------|----------------|-----------| +| FAB/BottomTabBar overlap fix | Browser / Client (CSS) | — | Pure layout geometry; CSS custom property token on the client; no server involvement | +| Icon/favicon asset generation | Build tooling (Node.js CLI) | Static/CDN | Run once at design time; assets committed to `public/`; served as static files | +| Token restructure (tokens.css) | Browser / Client (CSS) | — | CSS-only structural change; no server data | +| BrandSlot logo swap | Browser / Client (React) | — | Component-internal change; logo served from `public/logo.svg` static file | +| Logout control | Frontend + API | — | UI calls existing `POST /api/auth/local/logout` (already implemented); no new backend | +| Admin success toast | Browser / Client (React) | — | Client-side state (`useState` + `setTimeout`); mutation already exists | +| Dialog centering | Browser / Client (CSS/React) | — | Breakpoint-conditional inline style on existing dialog wrappers | +| Admin two-tab nav | Browser / Client (React) | — | Local `useState` in AdminPage; ARIA tabs pattern; no routing change | + +--- + +## Standard Stack + +### Core (already installed — no new runtime dependencies required) + +| Library | Version | Purpose | Status | +|---------|---------|---------|--------| +| React 19 | ^19.0.0 | PWA component framework | Installed | +| lucide-react | 1.17.0 | Icon library (`LogOut`, `CheckCircle` for D-07/D-08) | Installed; `LogOut` not yet imported anywhere | +| @tanstack/react-query | 5.101.0 | Mutation infrastructure for admin toasts | Installed | +| vite-plugin-pwa | ^1.3.0 | Manifest icons wiring | Installed | + +### Build-time toolchain (new devDependency, `apps/pwa` scope) + +| Package | Version | Purpose | Verdict | +|---------|---------|---------|---------| +| `@vite-pwa/assets-generator` | 1.0.2 | CLI: generates all PWA icon assets from SVG source | OK [VERIFIED: npm registry] | +| `sharp` | 0.35.1 (latest) | Raster processing engine (pulled as dependency of assets-generator) | SUS (see audit) — well-known package, seam flagged as too-new due to version date | +| `sharp-ico` | 0.1.5 | ICO encoder used internally by assets-generator | OK [VERIFIED: npm registry] | + +**Note on `sharp` SUS verdict:** The seam flagged it `too-new` because the latest publish date (2026-06-11) is within 30 days. This is a 12-year-old package (created 2013-08-20) with 65.6M weekly downloads at `github.com/lovell/sharp` — it is the canonical Node.js image processing library. The `too-new` signal reflects the latest version's publish date, not the package age. The planner should note this but may treat it as approved. [ASSUMED: confidence assessment of seam verdict vs. package age] + +**No new runtime dependencies.** Zero npm packages are added to `dependencies` — only devDependencies for the asset generation script. + +### Alternatives Considered + +| Instead of | Could Use | Why not | +|------------|-----------|---------| +| `@vite-pwa/assets-generator` | `pwa-asset-generator` (v8.1.5) | pwa-asset-generator uses Puppeteer/Chromium to generate assets (heavier); last meaningful update 2026-06-01; not the official vite-pwa project's tool | +| `@vite-pwa/assets-generator` | `sharp` directly + custom script | More code to maintain; assets-generator is purpose-built for this stack and already handles maskable safe-zone, ICO via sharp-ico | +| `@vite-pwa/assets-generator` | ImageMagick `convert` | ImageMagick is not installed on this machine (confirmed via `which convert`) | +| Hand-authored SVG source | Raster PNG source + trace | SVG is version-controllable, infinitely scalable, editable as text; no round-trip through a raster format | + +--- + +## Package Legitimacy Audit + +| Package | Registry | Age | Downloads | Source Repo | Verdict | Disposition | +|---------|----------|-----|-----------|-------------|---------|-------------| +| `@vite-pwa/assets-generator` | npm | ~7 mo (2025-10-14) | 231K/wk | github.com/vite-pwa/assets-generator | OK | Approved | +| `sharp` | npm | 13 yrs (2013-08-20) | 65.6M/wk | github.com/lovell/sharp | SUS (too-new flag on latest version) | Approved — package age and download volume override seam's version-recency signal; see note above | +| `sharp-ico` | npm | ~4 yrs (2022-08-30) | 431K/wk | github.com/ssnangua/sharp-ico | OK | Approved | + +**Packages removed due to SLOP verdict:** none +**Packages flagged as suspicious SUS:** `sharp` — seam flagged due to latest version publish date (2026-06-11 within 30-day window). This is a false positive given 13-year age and 65M weekly downloads. Planner does NOT need to add `checkpoint:human-verify` for this package; document the assessment and proceed. + +--- + +## Architecture Patterns + +### Recommended Asset-Generation Script Shape + +``` +apps/pwa/ +├── pwa-assets.config.ts # @vite-pwa/assets-generator config +└── public/ + ├── logo.svg # Source — hand-authored SVG (warm/rounded; committed) + ├── favicon.svg # Generated (copy of source SVG) + ├── favicon.ico # Generated (48×48 ICO via sharp-ico) + ├── icon-192.png # Generated (192×192 transparent PNG) + ├── icon-512.png # Generated (512×512 transparent PNG) + ├── icon-maskable-512.png # Generated (512×512 maskable, safe-zone padded) + └── apple-touch-icon.png # Generated (180×180 PNG) +``` + +**Minimal pwa-assets.config.ts:** + +```ts +// Source: @vite-pwa/assets-generator official docs / vite-pwa-org.netlify.app +import { defineConfig, minimal2023Preset } from '@vite-pwa/assets-generator/config' + +export default defineConfig({ + preset: { + ...minimal2023Preset, + // The minimal-2023 preset generates: + // - favicon.ico (48x48 via sharp-ico) + // - favicon.svg (copy of source) + // - icon-64.png (64×64 transparent) + // - icon-192.png (192×192 transparent) + // - icon-512.png (512×512 transparent, purpose: 'any') + // - icon-maskable-512.png (512×512 maskable, white bg, safe-zone padded, purpose: 'maskable') + // - apple-touch-icon.png (180×180) + }, + images: ['public/logo.svg'], +}) +``` + +**Generate command:** + +```bash +# Run from apps/pwa/ +npx --yes @vite-pwa/assets-generator generate +``` + +Or add to `package.json` scripts: + +```json +"pwa:icons": "pwa-assets-generator generate" +``` + +**Maskable safe-zone math (confirmed by minimal-2023 preset):** The preset produces a 512×512 maskable PNG with the logo scaled to fit within the 80% safe zone (410px effective canvas) centered on the canvas, with a solid background fill. The outer 10% on each edge may be cropped by adaptive-icon masks. The source `logo.svg` viewBox should be square; the tool handles all padding arithmetic. [CITED: vite-pwa-org.netlify.app/assets-generator/] + +**ICO generation:** `sharp-ico` (a dependency of `@vite-pwa/assets-generator`) produces the `.ico` file. The `minimal-2023` preset emits a 48×48 single-size ICO. The UI-SPEC calls for 16+32 multi-size — this is a minor gap. Resolution: the favicon.ico produced at 48px is adequate for modern use; multi-size ICO adds complexity with negligible real-world benefit for a household app. The `favicon.svg` (higher priority) covers modern browsers. [ASSUMED: 48px single-size ICO is sufficient vs. 16+32 multi-size for this use case] + +**vite.config.ts manifest update (after generation):** + +```ts +icons: [ + { src: '/icon-192.png', sizes: '192x192', type: 'image/png' }, + { src: '/icon-512.png', sizes: '512x512', type: 'image/png' }, + { src: '/icon-maskable-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }, +], +``` + +**index.html wiring (after generation):** + +```html + + + + +``` + +### Pattern 1: Shared Bottom-Chrome Token (Workstream A) + +**What:** Single CSS custom property `--bottom-chrome-h` defines the BottomTabBar's effective height including the safe-area-inset. Three sites consume it — the bar's own height, the FAB bottom offset, and the content area padding-bottom. + +**Fix contract:** + +```css +/* tokens.css — add to :root */ +--bottom-chrome-h: calc(56px + env(safe-area-inset-bottom, 0px)); +``` + +```ts +// CalendarShell.tsx — FAB bottom offset (phone branch only) +bottom: 'calc(var(--bottom-chrome-h) + var(--space-6))', +``` + +```ts +// App.tsx — contentStyle (phone branch only) +...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {}), +``` + +The BottomTabBar.tsx `height` already uses the same arithmetic inline (`calc(56px + env(safe-area-inset-bottom, 0px))`); it may optionally reference the token for consistency, but neither path changes the visual geometry. + +### Pattern 2: Overlap Regression Assertion (Workstream A) + +**Add to `layout.spec.ts`:** + +```ts +// Source: UI-SPEC §Regression Guard (D-02) +test('New Event FAB does not overlap BottomTabBar (A — phone only)', async ({ page }, testInfo) => { + test.skip(testInfo.project.name === 'desktop', 'Phone-only assertion'); + await page.goto('/calendar'); + const fab = page.getByRole('button', { name: 'New Event' }); + const nav = page.getByRole('navigation', { name: 'Main navigation' }); + const fabBox = await fab.boundingBox(); + const navBox = await nav.boundingBox(); + expect(fabBox).not.toBeNull(); + expect(navBox).not.toBeNull(); + expect(fabBox!.y + fabBox!.height).toBeLessThanOrEqual(navBox!.y); +}); +``` + +### Pattern 3: Admin Tabs ARIA Pattern (Workstream D-10) + +```tsx +// AdminPage.tsx — roving tabindex ARIA tabs +
+ {(['members', 'settings'] as const).map((id) => ( + + ))} +
+
+ {/* active panel content */} +
+``` + +`handleTabKeyDown` moves focus + activates on ArrowLeft/ArrowRight using `document.querySelector('[role="tab"]')` siblings or a ref array. + +### Anti-Patterns to Avoid + +- **Do not add `data-theme="light"` attribute to ``** in Phase 17. The groundwork is CSS-only; the attribute is added by Phase 999.20 when the toggle is shipped. The combined `:root, [data-theme="light"]` selector means light tokens apply regardless — no attribute needed for light-only. +- **Do not use `pwa-asset-generator` (the older v8 package)** — it uses Puppeteer/Chromium and is not the official vite-pwa project tool. +- **Do not run `@vite-pwa/assets-generator` with `overrideManifestIcons: true`** in this project — the manifest is manually maintained in `vite.config.ts`; auto-overwrite would stomp the explicit entries. +- **Do not modify `LoginPage.tsx`** when swapping BrandSlot internals — the seam contract from Phase 19 explicitly forbids layout changes to LoginPage. +- **Do not use `dangerouslySetInnerHTML`** in any new components (T-05-24 invariant, enforced project-wide). + +--- + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Maskable icon safe-zone padding | Custom canvas/sharp script | `@vite-pwa/assets-generator` minimal-2023 preset | Safe-zone math, ICO encoding, maskable background fill already implemented and tested | +| ICO encoding | Custom bit-packing | `sharp-ico` (dependency of assets-generator) | ICO is a non-trivial multi-image container format; sharp-ico handles it correctly | +| Toast state management | Custom event bus | Local `useState` + `setTimeout` in AdminPage | Single-component use; TanStack Query `onSuccess` callback feeds the state directly | +| ARIA tabs keyboard nav | Custom focus-trap | Roving tabindex pattern (standard ARIA spec) | Two tabs; ArrowLeft/ArrowRight with `tabIndex={-1}` on inactive tabs is 10 lines of code | + +--- + +## Code Surface Audit (Fix Surfaces Verified Against Source) + +All files and symbols confirmed to exist as described in CONTEXT.md and UI-SPEC. No drift found. + +### Workstream A + +| File | Finding | Status | +|------|---------|--------| +| `apps/pwa/src/App.tsx` | `contentStyle` (lines 155–163): no `paddingBottom`; phone split done via `isPhone()` function defined at line 64 | CONFIRMED — `paddingBottom` absent, fix is additive | +| `apps/pwa/src/components/BottomTabBar.tsx` | `height: 'calc(56px + env(safe-area-inset-bottom, 0px))'` at line 72; `position: fixed; bottom: 0; z-index: 200` | CONFIRMED — matches UI-SPEC description exactly | +| `apps/pwa/src/components/CalendarShell.tsx` | FAB at lines 463–484: `position: 'fixed', bottom: 'var(--space-6)', right: 'var(--space-6)'` — the defect | CONFIRMED — FAB bottom is `var(--space-6)` (24px), not clearing the 56px bar | +| `apps/pwa/e2e/layout.spec.ts` | Existing assertions cover tap targets, overflow, in-viewport; no overlap assertion yet | CONFIRMED — overlap assertion is missing; file is the correct home for it | +| `apps/pwa/playwright.config.ts` | Three profiles: iphone (390×844 WebKit), pixel (412×915 Chromium), desktop (1280×720 Chrome) | CONFIRMED | + +### Workstream B + +| File | Finding | Status | +|------|---------|--------| +| `apps/pwa/public/` | Three stubs: `apple-touch-icon.png` (617 B), `icon-192.png` (699 B), `icon-512.png` (4086 B). No `favicon.ico`, no `favicon.svg`, no `icon-maskable-512.png` | CONFIRMED — stubs only; all need replacement | +| `apps/pwa/index.html` | One ``, one ``, no `` | CONFIRMED — favicon links entirely absent | +| `apps/pwa/vite.config.ts` | `icons` array (lines 38–42): 3 entries; last entry incorrectly reuses `icon-512.png` for maskable purpose | CONFIRMED — `{ src: '/icon-512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' }` is the defect | +| `apps/pwa/src/components/BrandSlot.tsx` | Placeholder `` with `--brand-logo-*` token consumption; `

FamilySync

` and tagline `

` present | CONFIRMED — swap internals only; seam is correct | +| `sharp` in monorepo | Not installed anywhere in monorepo — not in `apps/pwa/package.json`, `apps/api/package.json`, or root; `node_modules/sharp` absent | CONFIRMED — must install `@vite-pwa/assets-generator` as devDependency | + +### Workstream C + +| File | Finding | Status | +|------|---------|--------| +| `apps/pwa/src/styles/tokens.css` | Single `:root { ... }` block with all tokens including `--brand-logo-*` tokens and Schedule-X `--sx-color-*` overrides at bottom | CONFIRMED — restructure is selector-only; token values unchanged | + +### Workstream D + +| Symbol | File | Line | Status | +|--------|------|------|--------| +| `fetchLocalLogout()` | `apps/pwa/src/api/client.ts` | 124–134 | CONFIRMED — `POST /api/auth/local/logout`, `credentials: 'include'`, `redirect: 'manual'`; handles opaqueredirect | +| `SyncStateToast` visual pattern | `apps/pwa/src/components/SyncStateToast.tsx` | 1–60 | CONFIRMED — toast renders with `role="status"` or `role="alert"`, bottom-center position; reuse the visual style | +| `AdminPage.tsx` | `apps/pwa/src/routes/AdminPage.tsx` | 1–80 | CONFIRMED — `useMutation` imports from TanStack Query present; `sectionLabelStyle` defined; currently single-scroll layout | +| `SettingsSheet.tsx` sheet structure | `apps/pwa/src/components/SettingsSheet.tsx` | 1–60 | CONFIRMED — `role="dialog"` pattern exists; `LogOut` lucide icon NOT yet imported (needs adding) | + +--- + +## Common Pitfalls + +### Pitfall 1: Maskable Icon Without Safe Zone +**What goes wrong:** Reusing the non-maskable 512 PNG as the maskable icon (the current defect in `vite.config.ts`) — the logo gets cropped at the edges on Android adaptive icons. +**Why it happens:** The manifest `purpose: 'maskable'` flag is easy to add without understanding that maskable requires the logo to be inset within the 80% safe zone. +**How to avoid:** Generate a separate `icon-maskable-512.png` using `@vite-pwa/assets-generator` which pads the logo to the safe zone automatically with a solid background fill. +**Warning signs:** `maskable` and `any` entries pointing to the same file in the manifest. + +### Pitfall 2: FAB Position Regression on Desktop +**What goes wrong:** Applying `paddingBottom: 'var(--bottom-chrome-h)'` to `contentStyle` unconditionally — on desktop, this adds 56px+ of empty space below content where there is no BottomTabBar. +**Why it happens:** `contentStyle` in `App.tsx` is shared across phone and desktop layouts. +**How to avoid:** Apply the padding-bottom only in the phone branch — `...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {})`. The existing `phone` boolean (computed from `isPhone()` at line 84 of `App.tsx`) is the correct gate. +**Warning signs:** layout.spec.ts desktop profile shows content area with unexpected bottom padding. + +### Pitfall 3: Schedule-X Token Override Cascade Break +**What goes wrong:** Moving the `--sx-color-*` overrides OUTSIDE the `:root, [data-theme="light"]` block during the tokens.css restructure — they no longer override the `@schedule-x/theme-default` values. +**Why it happens:** The tokens.css comment says "must come after the theme-default import" (import order in main.tsx), but that refers to the stylesheet import order. If the overrides move to a new rule block that comes before `:root` in cascade order (e.g., a bare `[data-theme="light"]` block without `:root`), specificity changes could affect the Schedule-X defaults. +**How to avoid:** Keep ALL tokens — including `--sx-color-*` — inside the combined `:root, [data-theme="light"]` block. Do not split them across multiple rule blocks. +**Warning signs:** Schedule-X calendar grid loses custom colors (event chips revert to the Schedule-X default blue/green). + +### Pitfall 4: `fetchLocalLogout` Error Swallowed on Navigate +**What goes wrong:** If `fetchLocalLogout()` rejects (e.g., server error, network timeout), a naive `await fetchLocalLogout(); navigate('/login')` would navigate away without clearing the cookie. The user would believe they logged out but the session cookie may still be valid. +**Why it happens:** Async error propagation on a fire-and-forget logout. +**How to avoid:** Wrap in `try/catch` and call `navigate('/login')` in both the `try` (success) and `catch` (failure) branches — the UI-SPEC documents this as "fire-and-best-effort for a cookie clear." The server already handles the 401 case by expiring the cookie; the navigate is always safe. +**Warning signs:** `fetchLocalLogout` throws and the SettingsSheet stays open. + +### Pitfall 5: `--brand-logo-border-radius` Not Updated +**What goes wrong:** After swapping the BrandSlot `

` for an ``, the `--brand-logo-border-radius: 50%` token (currently in tokens.css) clips the `` into a circle regardless of the logo's own shape. +**Why it happens:** The token was set for the initials-circle placeholder; it is inherited by the `` element via the BrandSlot swap contract. +**How to avoid:** After the logo art is approved at the checkpoint, update `--brand-logo-border-radius` in tokens.css to match the logo shape (likely `12px` for a warm/rounded brief, or `0` if the SVG draws its own shape). +**Warning signs:** The real logo looks cropped or forced into a circle shape in the login view. + +--- + +## Runtime State Inventory + +SKIPPED — this is a greenfield polish phase, not a rename/refactor/migration. No stored data, live service config, OS-registered state, secrets, or build artifacts carry strings being changed. + +--- + +## Environment Availability + +| Dependency | Required By | Available | Version | Fallback | +|------------|------------|-----------|---------|----------| +| Node.js | Asset generation script | ✓ | 22 LTS | — | +| pnpm | Monorepo package management | ✓ | (installed) | — | +| `@vite-pwa/assets-generator` | Workstream B icon generation | ✗ (not installed) | 1.0.2 (latest) | Install as devDependency in `apps/pwa` | +| `sharp` | Transitive dep of assets-generator | ✗ (not installed) | 0.35.1 (latest) | Installed automatically with assets-generator | +| ImageMagick `convert` | Alternative ICO generation | ✗ | — | Not needed — using assets-generator instead | +| Playwright | Workstream A regression assertions | ✓ | 1.60.0 | — | +| Vitest | Unit tests | ✓ | ^4.1.8 | — | + +**Missing dependencies with no fallback:** none +**Missing dependencies with fallback:** `@vite-pwa/assets-generator` — install as devDependency in `apps/pwa`. + +--- + +## Validation Architecture + +### Test Framework + +| Property | Value | +|----------|-------| +| E2E Framework | Playwright 1.60.0 | +| Unit Framework | Vitest ^4.1.8 | +| E2E Config | `apps/pwa/playwright.config.ts` | +| Unit Config | `apps/pwa/vitest.config.ts` | +| E2E quick run | `pnpm --filter @familysync/pwa exec playwright test --project=pixel layout.spec.ts` | +| E2E full suite | `pnpm --filter @familysync/pwa test:e2e` | + +### Phase Requirements → Validation Map + +| Workstream | Behavior | Test Type | Command / Method | Automated? | +|------------|----------|-----------|------------------|------------| +| A — FAB overlap | FAB bottom edge ≤ BottomTabBar top edge on iphone/pixel | Playwright geometry assertion | New assertion in `layout.spec.ts` | CI (iphone + pixel profiles) | +| A — Content clearance | Color legend chips visible; content scrollable above bar | Playwright + playwright-cli sweep | `layout.spec.ts` full suite + manual scroll | Partially automated | +| A — No horizontal overflow | `scrollWidth ≤ clientWidth` after fix | Playwright Rule 2 | Existing `layout.spec.ts` Rule 2 tests | CI (all profiles) | +| A — Tap targets preserved | All existing ≥44px/≥56px assertions pass | Playwright Rule 1 | Existing `layout.spec.ts` Rule 1 tests | CI (all profiles) | +| B — Asset existence | All 7 files present in `apps/pwa/public/` | Shell `ls` / existence check | `ls apps/pwa/public/{favicon.svg,favicon.ico,icon-192.png,icon-512.png,icon-maskable-512.png,apple-touch-icon.png,logo.svg}` | CI gate / Wave 0 check | +| B — Manifest references resolve | PWA manifest icons all 200 in browser | Playwright | `page.goto('/')` then check `navigator.serviceWorker` / devtools manifest tab | playwright-cli | +| B — Maskable dimensions | `icon-maskable-512.png` is exactly 512×512 | Node.js check using `sharp` | `node -e "require('sharp')('/path/icon-maskable-512.png').metadata().then(m => console.log(m.width, m.height))"` | Scripted | +| B — ICO is not zero-byte | `favicon.ico` exists and is >100 bytes | Shell | `test $(wc -c < apps/pwa/public/favicon.ico) -gt 100` | CI gate | +| B — Visual logo approval | Logo meets warm/rounded brief | Human review | playwright-cli screenshot + operator approval | Human checkpoint (checkpoint:human-verify) | +| C — No hard-coded values in components | `grep` finds no hex/px literals in component files | `grep` | `grep -rn '#[0-9a-fA-F]\{3,6\}\|[0-9]\+px' apps/pwa/src/components/ apps/pwa/src/routes/` | CI gate | +| C — Schedule-X colors unchanged | Calendar grid retains custom colors post-restructure | Playwright + playwright-cli | playwright-cli visual sweep of /calendar | playwright-cli | +| C — Build succeeds | TypeScript + Vite build exits 0 | Build | `pnpm --filter @familysync/pwa build` | CI | +| D-07 — Logout control renders | "Sign out" button visible in SettingsSheet | Playwright + playwright-cli | playwright-cli: open settings sheet, confirm button visible | playwright-cli | +| D-07 — Logout wiring | `fetchLocalLogout` called on click; navigate to /login | Playwright interaction | playwright-cli: click Sign out, confirm redirect to /login | playwright-cli | +| D-08 — Admin create toast | "Member added." toast appears after create success | Playwright interaction | playwright-cli: create member, confirm toast | playwright-cli (admin session) | +| D-08 — Admin reset toast | "Password reset." toast appears after reset success | Playwright interaction | playwright-cli: reset password, confirm toast | playwright-cli (admin session) | +| D-08 — Toast auto-dismiss | Toast disappears after ~3 seconds | Playwright interaction | playwright-cli: wait 3.5s after toast appears | playwright-cli | +| D-09 — Dialog desktop centering | Sheet renders centered (`top: 50%; left: 50%; transform`) on desktop | Playwright geometry | playwright-cli at 1280×720: open SettingsSheet, confirm geometry | playwright-cli (desktop) | +| D-09 — Dialog phone bottom-sheet | Sheet renders at bottom on phone | Playwright geometry | playwright-cli at 390×844: open SettingsSheet, confirm bottom-sheet | playwright-cli (phone) | +| D-10 — Admin tabs render | Two-tab strip visible with correct labels | Playwright | playwright-cli at /admin: confirm "Members & Accounts" and "Settings" tabs | playwright-cli (admin session) | +| D-10 — Admin tabs keyboard | ArrowLeft/ArrowRight switches tabs | Playwright keyboard | playwright-cli: focus tab, ArrowRight, confirm second tab active | playwright-cli | +| D-10 — Admin tab ARIA | `role="tablist"`, `role="tab"`, `aria-selected`, `role="tabpanel"` present | Playwright | `page.getByRole('tablist')`, `page.getByRole('tab', {name: ...})` | Playwright test (add to layout.spec.ts or admin.spec.ts) | + +### Sampling Rate + +- **Per task commit:** `pnpm --filter @familysync/pwa exec playwright test --project=pixel layout.spec.ts` (fast; phone profile covers the primary fix geometry) +- **Per wave merge:** `pnpm --filter @familysync/pwa test:e2e` (full 3-profile suite) +- **Phase gate:** Full Playwright suite green + playwright-cli visual sweeps passed + human logo approval received before `/gsd-verify-work` + +### Wave 0 Gaps + +- [ ] The overlap regression assertion does not exist yet — add to `apps/pwa/e2e/layout.spec.ts` (Workstream A) +- [ ] Admin tab ARIA assertions — add to a new `apps/pwa/e2e/admin.spec.ts` or append to `layout.spec.ts` (Workstream D-10) +- [ ] Asset existence check script — can be a Wave 0 npm script or inline CI step + +--- + +## Security Domain + +`security_enforcement: true` in `.planning/config.json`. ASVS level 1. + +| ASVS Category | Applies | Control | +|---------------|---------|---------| +| V2 Authentication | No — no auth changes; logout calls existing endpoint | Existing endpoint (`POST /api/auth/local/logout`) already verified in Phase 19 | +| V3 Session Management | Indirect — logout clears session cookie | `fetchLocalLogout` posts to existing endpoint; cookie clearance is server-side | +| V4 Access Control | No — admin page UX gating unchanged; server 403 unchanged | No new routes or authorization changes | +| V5 Input Validation | Minimal — admin tab state, toast message strings are hardcoded constants | No user input reaches new UI surfaces except the existing admin forms (unchanged) | +| V6 Cryptography | No | No new crypto | + +### Known Threat Patterns + +| Pattern | STRIDE | Mitigation | +|---------|--------|------------| +| XSS via toast message content | Spoofing/Tampering | Toast copy is hardcoded JSX string constants ("Member added." / "Password reset.") — no user-controlled content; T-05-24 invariant maintained | +| Session fixation after logout | Elevation of Privilege | `fetchLocalLogout` clears the `local-session` cookie server-side; client navigates to `/login` regardless of success/failure | +| Admin route access without OIDC | Elevation of Privilege | No change — `isAdmin` check in App.tsx is UX-only; server enforces 403 on all `/api/admin/*` (unchanged) | + +--- + +## Assumptions Log + +| # | Claim | Section | Risk if Wrong | +|---|-------|---------|---------------| +| A1 | `@vite-pwa/assets-generator` `minimal-2023` preset generates a maskable icon with the logo properly inset in the 80% safe zone (not just solid-color fill) | Standard Stack / Asset Toolchain | If the preset only fills the 512×512 with a background and does not inset the logo, the maskable icon will have the logo cropped on adaptive-icon masks — require custom `padding` option in config | +| A2 | The 48×48 single-size `favicon.ico` from `@vite-pwa/assets-generator` is adequate for this household app (vs. 16+32 multi-size) | Standard Stack | Negligible visual risk — legacy browsers (IE, old Safari) may show a slightly blurry icon; no functional impact for modern browsers which prefer `favicon.svg` | +| A3 | `sharp` SUS verdict is a false positive due to version recency window; the package is safe to use | Package Legitimacy Audit | Near-zero risk — 13-year-old package, 65.6M weekly downloads, lovell/sharp well-known author; risk if the seam has additional signals not surfaced in the output | +| A4 | Hand-authoring SVG for the logo (warm/rounded family vibes) in-phase by Claude produces a logo that meets the brand brief to the operator's satisfaction at the checkpoint | Workstream B | If operator rejects the generated logo, the phase stalls at the checkpoint; the plan must include a checkpoint:human-verify before assets are wired in | + +--- + +## Open Questions + +1. **`@vite-pwa/assets-generator` maskable padding behavior** + - What we know: the `minimal-2023` preset produces a `maskable` 512×512 PNG with a white background; the documentation describes it as "safe zone padded" + - What's unclear: whether the preset option allows overriding the background color to a warm brand color (e.g., `#fef3ec`) matching the logo palette, vs. always producing white + - Recommendation: use white background for the maskable icon in Wave 1; if the operator wants a brand-color background, add a `maskableIconOptions` config in `pwa-assets.config.ts` at the checkpoint + +2. **Brand accent checkpoint outcome** + - What we know: two candidates (Variant A: `#4a90d9` cool blue; Variant B: warm rose `#f25c7a` or amber `#e8915a`) per UI-SPEC §Color + - What's unclear: which the operator will select after seeing the logo + - Recommendation: produce the logo, show both accent variants in context (the token restructure in C makes it a one-line swap), checkpoint before committing accent to `index.html`/`vite.config.ts`/`tokens.css` + +--- + +## Sources + +### Primary (HIGH confidence — official project documentation) +- `apps/pwa/src/App.tsx` — `contentStyle`, `isPhone()`, phone/desktop branch; confirmed missing `paddingBottom` [VERIFIED: codebase grep] +- `apps/pwa/src/components/CalendarShell.tsx:469–470` — FAB `bottom: 'var(--space-6)'` defect [VERIFIED: codebase grep] +- `apps/pwa/src/components/BottomTabBar.tsx:72` — bar `height: 'calc(56px + env(safe-area-inset-bottom, 0px))'` [VERIFIED: codebase grep] +- `apps/pwa/src/api/client.ts:124–134` — `fetchLocalLogout()` exists, POST, credentials include [VERIFIED: codebase grep] +- `apps/pwa/src/styles/tokens.css` — single `:root {}` block confirmed; `--sx-color-*` overrides at bottom [VERIFIED: codebase grep] +- `apps/pwa/vite.config.ts:38–42` — maskable icon defect (reusing `icon-512.png`) confirmed [VERIFIED: codebase grep] +- `apps/pwa/index.html` — no `` entries confirmed [VERIFIED: codebase grep] +- `apps/pwa/public/` — only 3 stub files, none maskable, no favicon.ico/svg [VERIFIED: codebase grep] +- `.planning/config.json` — `nyquist_validation: true`, `security_enforcement: true` [VERIFIED: codebase grep] + +### Secondary (MEDIUM confidence — official package docs) +- `@vite-pwa/assets-generator` v1.0.2 — vite-pwa-org.netlify.app/assets-generator/ [CITED: vite-pwa-org.netlify.app/assets-generator/] +- `sharp` v0.35.1 — sharp.pixelplumbing.com; github.com/lovell/sharp [CITED: npm registry metadata] +- `vite-plugin-pwa` v1.3.0 — peer-depends on `@vite-pwa/assets-generator ^1.0.0` [CITED: npm registry peerDependencies] + +### Tertiary (LOW confidence — training knowledge) +- ARIA tabs pattern (roving tabindex, `role="tablist"/"tab"/"tabpanel"`) — standard W3C ARIA spec [ASSUMED — widely documented, no drift risk] + +--- + +## Metadata + +**Confidence breakdown:** +- Fix surfaces: HIGH — directly confirmed against codebase source +- Asset toolchain: MEDIUM — based on official vite-pwa documentation and npm registry; `minimal-2023` preset maskable behavior [ASSUMED] pending a test run +- Architecture patterns: HIGH — Workstreams A/C/D are CSS/React patterns with no external dependencies +- Pitfalls: HIGH — drawn from confirmed code structure and known project conventions + +**Research date:** 2026-06-18 +**Valid until:** 2026-08-18 (stable APIs; vite-plugin-pwa and @vite-pwa/assets-generator are stable releases)