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).
@.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.
<threat_model>
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.
</threat_model>
- `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
<success_criteria>
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.
</success_criteria>
Create `.planning/phases/17-ui-optimization-polish/17-03-SUMMARY.md` when done.