Files
familysync/.planning/milestones/v1.1-phases/17-ui-optimization-polish/17-RESEARCH.md
T
2026-06-18 22:21:38 -04:00

37 KiB
Raw Blame History

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>

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 <div> for <img>, override --brand-logo-* tokens. No LoginPage layout changes. Accessibility shape unchanged (<h1> 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. </user_constraints>

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 <div role="dialog"> 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

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:

// 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:

# Run from apps/pwa/
npx --yes @vite-pwa/assets-generator generate

Or add to package.json scripts:

"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):

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):

<meta name="theme-color" content="{CHECKPOINT_ACCENT}" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />

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:

/* tokens.css — add to :root */
--bottom-chrome-h: calc(56px + env(safe-area-inset-bottom, 0px));
// CalendarShell.tsx — FAB bottom offset (phone branch only)
bottom: 'calc(var(--bottom-chrome-h) + var(--space-6))',
// 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:

// 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)

// AdminPage.tsx — roving tabindex ARIA tabs
<div role="tablist">
  {(['members', 'settings'] as const).map((id) => (
    <button
      key={id}
      role="tab"
      id={`admin-tab-${id}`}
      aria-selected={activeTab === id}
      aria-controls={`admin-panel-${id}`}
      tabIndex={activeTab === id ? 0 : -1}
      onClick={() => setActiveTab(id)}
      onKeyDown={handleTabKeyDown}  // ArrowLeft/ArrowRight
    >
      {id === 'members' ? 'Members & Accounts' : 'Settings'}
    </button>
  ))}
</div>
<div
  role="tabpanel"
  id={`admin-panel-${activeTab}`}
  aria-labelledby={`admin-tab-${activeTab}`}
  tabIndex={0}
>
  {/* active panel content */}
</div>

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 <html> 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 155163): 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 463484: 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 <link rel="apple-touch-icon">, one <meta name="theme-color" content="#4A90D9">, no <link rel="icon"> CONFIRMED — favicon links entirely absent
apps/pwa/vite.config.ts icons array (lines 3842): 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 <div aria-hidden="true">FS</div> with --brand-logo-* token consumption; <h1>FamilySync</h1> and tagline <p> 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 124134 CONFIRMED — POST /api/auth/local/logout, credentials: 'include', redirect: 'manual'; handles opaqueredirect
SyncStateToast visual pattern apps/pwa/src/components/SyncStateToast.tsx 160 CONFIRMED — toast renders with role="status" or role="alert", bottom-center position; reuse the visual style
AdminPage.tsx apps/pwa/src/routes/AdminPage.tsx 180 CONFIRMED — useMutation imports from TanStack Query present; sectionLabelStyle defined; currently single-scroll layout
SettingsSheet.tsx sheet structure apps/pwa/src/components/SettingsSheet.tsx 160 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 <div> for an <img>, the --brand-logo-border-radius: 50% token (currently in tokens.css) clips the <img> 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 <img> 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.tsxcontentStyle, isPhone(), phone/desktop branch; confirmed missing paddingBottom [VERIFIED: codebase grep]
  • apps/pwa/src/components/CalendarShell.tsx:469470 — 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:124134fetchLocalLogout() 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:3842 — maskable icon defect (reusing icon-512.png) confirmed [VERIFIED: codebase grep]
  • apps/pwa/index.html — no <link rel="icon"> entries confirmed [VERIFIED: codebase grep]
  • apps/pwa/public/ — only 3 stub files, none maskable, no favicon.ico/svg [VERIFIED: codebase grep]
  • .planning/config.jsonnyquist_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)