Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
37 KiB
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 maskableicon-maskable-512.png(safe-zone padded),apple-touch-icon.png(180×180). Updateindex.htmlandvite.config.tsmanifest. - 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 decorativealt="" aria-hidden). - D-06: Restructure
tokens.cssinto a themeable layer (:root, [data-theme="light"]combined selector). Groundwork only — no dark palette values, noprefers-color-schemewiring, 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-htoken + permanent overlap assertion inlayout.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
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:
// 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-generatorwithoverrideManifestIcons: truein this project — the manifest is manually maintained invite.config.ts; auto-overwrite would stomp the explicit entries. - Do not modify
LoginPage.tsxwhen swapping BrandSlot internals — the seam contract from Phase 19 explicitly forbids layout changes to LoginPage. - Do not use
dangerouslySetInnerHTMLin 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 <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 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 <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 |
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 <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.tsor append tolayout.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
-
@vite-pwa/assets-generatormaskable padding behavior- What we know: the
minimal-2023preset produces amaskable512×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
maskableIconOptionsconfig inpwa-assets.config.tsat the checkpoint
- What we know: the
-
Brand accent checkpoint outcome
- What we know: two candidates (Variant A:
#4a90d9cool blue; Variant B: warm rose#f25c7aor 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
- What we know: two candidates (Variant A:
Sources
Primary (HIGH confidence — official project documentation)
apps/pwa/src/App.tsx—contentStyle,isPhone(), phone/desktop branch; confirmed missingpaddingBottom[VERIFIED: codebase grep]apps/pwa/src/components/CalendarShell.tsx:469–470— FABbottom: 'var(--space-6)'defect [VERIFIED: codebase grep]apps/pwa/src/components/BottomTabBar.tsx:72— barheight: '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 (reusingicon-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.json—nyquist_validation: true,security_enforcement: true[VERIFIED: codebase grep]
Secondary (MEDIUM confidence — official package docs)
@vite-pwa/assets-generatorv1.0.2 — vite-pwa-org.netlify.app/assets-generator/ [CITED: vite-pwa-org.netlify.app/assets-generator/]sharpv0.35.1 — sharp.pixelplumbing.com; github.com/lovell/sharp [CITED: npm registry metadata]vite-plugin-pwav1.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-2023preset 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)