--- phase: 17-ui-optimization-polish plan: 03 type: execute wave: 2 depends_on: ["17-01"] files_modified: - apps/pwa/src/components/CalendarShell.tsx - apps/pwa/src/App.tsx - apps/pwa/e2e/layout.spec.ts autonomous: true requirements: [D-01, D-02] must_haves: truths: - "The New Event FAB never intersects the BottomTabBar rect on iphone/pixel profiles" - "Phone content scrolls fully above the BottomTabBar — the color-legend chips are not occluded" - "Desktop layout geometry is unchanged (no extra bottom padding, no FAB offset change)" - "A permanent overlap regression assertion guards the FAB↔BottomTabBar geometry in CI" artifacts: - path: "apps/pwa/e2e/layout.spec.ts" provides: "Permanent FAB↔BottomTabBar overlap assertion (iphone + pixel, skipped on desktop)" contains: "New Event FAB does not overlap BottomTabBar" key_links: - from: "apps/pwa/src/components/CalendarShell.tsx" to: "apps/pwa/src/styles/tokens.css" via: "FAB bottom offset reads var(--bottom-chrome-h)" pattern: "var\\(--bottom-chrome-h\\)" - from: "apps/pwa/src/App.tsx" to: "apps/pwa/src/styles/tokens.css" via: "phone contentStyle paddingBottom reads var(--bottom-chrome-h)" pattern: "var\\(--bottom-chrome-h\\)" --- Fix the long-standing phone (≤767px) fixed-chrome overlap where the `position: fixed` BottomTabBar covers the New Event FAB and occludes the bottom of the calendar + color-legend chips (D-01, the phase seed defect), using the shared `--bottom-chrome-h` token added by plan 17-01 (D-02 fix technique). Add a permanent overlap regression assertion to `layout.spec.ts` (D-02 regression guard) and run the bounded small-viewport sweep across all three Playwright profiles. CSS-only — no behaviour change, no desktop geometry change. Purpose: the FAB currently lands on the Admin tab and the "Dev User" legend is clipped at 390×844; this fix lifts the FAB above the bar and reserves content padding so nothing is occluded at rest. Output: corrected FAB offset (CalendarShell.tsx), phone-only content padding (App.tsx), and a new CI overlap assertion (layout.spec.ts). @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/17-ui-optimization-polish/17-UI-SPEC.md @.planning/phases/17-ui-optimization-polish/17-PATTERNS.md @.planning/phases/17-ui-optimization-polish/17-RESEARCH.md @.planning/phases/17-ui-optimization-polish/17-01-SUMMARY.md Task 1: Lift the FAB and reserve phone content padding via --bottom-chrome-h apps/pwa/src/components/CalendarShell.tsx, apps/pwa/src/App.tsx - apps/pwa/src/components/CalendarShell.tsx lines 463–491 (the phone-only FAB block; current `bottom: 'var(--space-6)'` is the defect site) - apps/pwa/src/App.tsx lines 64, 84, 155–163 (`isPhone()`, the `phone` boolean, and `contentStyle` which lacks paddingBottom) - apps/pwa/src/components/BottomTabBar.tsx line ~73 (bar height `calc(56px + env(safe-area-inset-bottom, 0px))` — resolves identically to `--bottom-chrome-h`) - 17-UI-SPEC.md §"Workstream A — Phone-Layout Overlap Fix" → "Fix contract" + "Visual invariants" (exact before/after values) - 17-PATTERNS.md §"apps/pwa/src/components/CalendarShell.tsx" and §"apps/pwa/src/App.tsx" (line-anchored before/after excerpts + the `...(phone ? {...} : {})` spread pattern) - 17-RESEARCH.md §"Common Pitfalls" → Pitfall 2 (paddingBottom must be phone-only) - 17-01-SUMMARY.md (confirms `--bottom-chrome-h` is available in tokens.css) In `apps/pwa/src/components/CalendarShell.tsx`, in the phone-only FAB style block, change ONLY the `bottom` property from `'var(--space-6)'` to `'calc(var(--bottom-chrome-h) + var(--space-6))'`. This sits the FAB `var(--space-6)` (24px) above the BottomTabBar top edge regardless of the safe-area-inset value. Leave `right`, `width`, `height`, `zIndex`, and all other FAB style properties unchanged. In `apps/pwa/src/App.tsx`, add `paddingBottom` to `contentStyle` PHONE-ONLY using the existing `phone` boolean and the established spread pattern: append `...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {})` to the `contentStyle` object literal. Do NOT add this unconditionally — desktop has no BottomTabBar and must not gain extra bottom padding (RESEARCH Pitfall 2). Do not change any other contentStyle property. Optionally (not required for the visual invariant) reference `var(--bottom-chrome-h)` from BottomTabBar.tsx's height; if you do, the value must remain identical (`calc(56px + env(safe-area-inset-bottom, 0px))`). Skipping this is fine — keep the diff minimal. grep -q 'calc(var(--bottom-chrome-h) + var(--space-6))' apps/pwa/src/components/CalendarShell.tsx && grep -q "paddingBottom: 'var(--bottom-chrome-h)'" apps/pwa/src/App.tsx && pnpm --filter @familysync/pwa build - CalendarShell.tsx FAB `bottom` is exactly `calc(var(--bottom-chrome-h) + var(--space-6))`. - App.tsx `contentStyle` adds `paddingBottom: 'var(--bottom-chrome-h)'` ONLY inside the `phone` branch (the `...(phone ? {...} : {})` spread); desktop branch has no paddingBottom. - `pnpm --filter @familysync/pwa build` exits 0. - No other FAB/contentStyle properties changed (diff is minimal, CSS-only). The FAB clears the BottomTabBar by var(--space-6), phone content reserves --bottom-chrome-h padding, desktop geometry is untouched, and the build passes. Task 2: Add the FAB↔BottomTabBar overlap regression assertion to layout.spec.ts apps/pwa/e2e/layout.spec.ts - apps/pwa/e2e/layout.spec.ts lines 1–60 (file header, imports, existing `test.describe` + `boundingBox()` assertion pattern, the `navigation`/`Main navigation` role locator) - apps/pwa/playwright.config.ts (the three profiles: iphone 390×844 WebKit, pixel 412×915 Chromium, desktop 1280×720 — the assertion skips desktop via `testInfo.project.name`) - 17-UI-SPEC.md §"Workstream A" → "Regression guard (D-02 decision)" (the exact assertion contract) - 17-PATTERNS.md §"apps/pwa/e2e/layout.spec.ts" (the new overlap assertion excerpt) - 17-VALIDATION.md §"Wave 0 Requirements" (this assertion is the Wave 0 overlap-regression item) Add a new Playwright test to `apps/pwa/e2e/layout.spec.ts` named "New Event FAB does not overlap BottomTabBar (A — phone only)". It must: skip on the desktop profile via `test.skip(testInfo.project.name === 'desktop', 'Phone-only assertion')`; `page.goto('/calendar')`; locate the FAB via `page.getByRole('button', { name: 'New Event' })` and the bar via `page.getByRole('navigation', { name: 'Main navigation' })`; read both `boundingBox()`; assert neither is null; and assert `fabBox.y + fabBox.height <= navBox.y` (the FAB bottom edge is at or above the BottomTabBar top edge). Match the existing file's test style and import (`import { test, expect } from '@playwright/test'`). This is the permanent regression guard for the seed defect. It runs on iphone + pixel (the profiles that exposed the long-standing, desktop-invisible defect). grep -q 'New Event FAB does not overlap BottomTabBar' apps/pwa/e2e/layout.spec.ts && pnpm --filter @familysync/pwa exec playwright test --project=iphone --project=pixel layout.spec.ts - The new test "New Event FAB does not overlap BottomTabBar (A — phone only)" exists in layout.spec.ts. - It skips on desktop (`testInfo.project.name === 'desktop'`). - It asserts `fabBox.y + fabBox.height <= navBox.y`. - `pnpm --filter @familysync/pwa exec playwright test --project=iphone --project=pixel layout.spec.ts` is GREEN (the assertion passes against the Task 1 fix — proving the overlap is resolved on both phone profiles). The overlap assertion is committed to layout.spec.ts, skips desktop, and passes green on iphone + pixel — confirming the fix and guarding against regression. Task 3: Run the bounded small-viewport sweep across all three profiles apps/pwa/e2e/layout.spec.ts (sweep run — fix only violations flagged, CSS-only) - 17-UI-SPEC.md §"Workstream A" → "Small-viewport sweep (D-01)" (the checklist: Admin tab tap target, ColorLegend chips visible, no overflow on phone profiles) - 17-VALIDATION.md §"Per-Task Verification Map" D-01 rows (color-legend visible, no horizontal overflow Rule 2, tap targets Rule 1) - apps/pwa/e2e/layout.spec.ts (Rules 1–4 assertions: ≥44px/≥56px tap targets, no overflow, in-viewport, accessible names) - .claude/skills/playwright-cli/ (for the manual color-legend visibility/scroll observation the spec suite cannot fully cover) Run the full `layout.spec.ts` suite across all three profiles (`test:e2e`). The D-01 sweep is checklist-driven by the existing Rules 1–4 assertions — fix ANY violation they flag, staying within the no-behaviour-change boundary (CSS/layout only). Expected areas to confirm per UI-SPEC: the Admin tab still meets ≥44px after the layout fix, the ColorLegend "Dev User"/member chips are fully visible (not occluded) now that content padding is added, and no horizontal overflow on phone profiles. Use the playwright-cli skill at 390×844 to visually confirm the color-legend chips are reachable by scrolling and not clipped behind the bar (the occlusion symptom from the seed defect). Do NOT undertake a free-form audit or touch desktop geometry — fix only what the assertions flag. pnpm --filter @familysync/pwa test:e2e - `pnpm --filter @familysync/pwa test:e2e` (full 3-profile suite) is GREEN — Rules 1–4 (tap targets, no overflow, in-viewport, accessible names) all pass on iphone/pixel/desktop after the fix. - playwright-cli @390×844: the ColorLegend chips are fully visible (scroll-reachable, not clipped behind the BottomTabBar) — observation noted in SUMMARY. - No desktop geometry regression (desktop profile assertions unchanged/green). The full layout.spec.ts suite is green on all three profiles, the color-legend occlusion is confirmed resolved via playwright-cli, and no desktop geometry changed. ## Trust Boundaries | Boundary | Description | |----------|-------------| | (none new) | CSS-only layout offsets + a Playwright test. No runtime data flow, no new endpoints, no user input. | ## STRIDE Threat Register | Threat ID | Category | Component | Disposition | Mitigation Plan | |-----------|----------|-----------|-------------|-----------------| | T-17-03-01 | Tampering | FAB/content CSS offsets | accept | Pure layout geometry via existing CSS custom property; no executable content, no input. No new threat above LOW for Workstream A. | No new high/medium-severity threats. CSS-only positioning change plus a geometry test. - `grep -q 'calc(var(--bottom-chrome-h) + var(--space-6))' apps/pwa/src/components/CalendarShell.tsx` — FAB lifted - `grep -q "paddingBottom: 'var(--bottom-chrome-h)'" apps/pwa/src/App.tsx` — phone padding added - `grep -q 'New Event FAB does not overlap BottomTabBar' apps/pwa/e2e/layout.spec.ts` — regression guard added - `pnpm --filter @familysync/pwa exec playwright test --project=iphone --project=pixel layout.spec.ts` — overlap assertion green - `pnpm --filter @familysync/pwa test:e2e` — full 3-profile sweep green The FAB no longer overlaps the BottomTabBar on phone, the color-legend is no longer occluded, desktop geometry is unchanged, a permanent overlap assertion guards the regression in CI, and the full Playwright suite is green across all three profiles. Create `.planning/phases/17-ui-optimization-polish/17-03-SUMMARY.md` when done.