From 5e1c714894d2177da545055c3fe42dcfd0919068 Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 18 Jun 2026 12:48:35 -0400 Subject: [PATCH 1/3] fix(17-03): lift FAB above BottomTabBar and reserve phone content padding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CalendarShell.tsx: FAB bottom changed from var(--space-6) to calc(var(--bottom-chrome-h) + var(--space-6)) — sits 24px above bar - App.tsx: contentStyle gains phone-only paddingBottom: var(--bottom-chrome-h) via spread pattern ...(phone ? {...} : {}) — desktop unchanged --- apps/pwa/src/App.tsx | 2 ++ apps/pwa/src/components/CalendarShell.tsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/pwa/src/App.tsx b/apps/pwa/src/App.tsx index 84feadf..c11e6ea 100644 --- a/apps/pwa/src/App.tsx +++ b/apps/pwa/src/App.tsx @@ -160,6 +160,8 @@ export default function App() { flexDirection: 'column', overflow: 'hidden', position: 'relative', + // Phone-only: reserve space for the fixed BottomTabBar so content is not occluded + ...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {}), }; // Setup gate: while setup status is loading, render nothing (prevent flash). diff --git a/apps/pwa/src/components/CalendarShell.tsx b/apps/pwa/src/components/CalendarShell.tsx index 6b28373..cc7f0de 100644 --- a/apps/pwa/src/components/CalendarShell.tsx +++ b/apps/pwa/src/components/CalendarShell.tsx @@ -467,7 +467,7 @@ export function CalendarShell() { onClick={() => setEventForm(true, 'create')} style={{ position: 'fixed', - bottom: 'var(--space-6)', + bottom: 'calc(var(--bottom-chrome-h) + var(--space-6))', right: 'var(--space-6)', width: '56px', height: '56px', From 85a803fba626d5ca19fc6dd87b064aa39c74e8ac Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 18 Jun 2026 12:51:32 -0400 Subject: [PATCH 2/3] =?UTF-8?q?test(17-03):=20add=20FAB=E2=86=94BottomTabB?= =?UTF-8?q?ar=20overlap=20regression=20guard=20to=20layout.spec.ts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New test: "New Event FAB does not overlap BottomTabBar (A — phone only)" - Skips on desktop profile; runs iphone + pixel (the profiles that exposed D-01) - Asserts fabBox.y + fabBox.height <= navBox.y (FAB bottom ≤ bar top) - Confirms Task 1 fix resolves the seed defect — passes green on both profiles --- apps/pwa/e2e/layout.spec.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/apps/pwa/e2e/layout.spec.ts b/apps/pwa/e2e/layout.spec.ts index 4e4e490..5f382bd 100644 --- a/apps/pwa/e2e/layout.spec.ts +++ b/apps/pwa/e2e/layout.spec.ts @@ -199,6 +199,40 @@ test.describe('Rule 2 — No horizontal overflow', () => { }); }); +// ── D-01 Regression Guard: FAB does not overlap BottomTabBar (Workstream A) ── +// +// Permanent overlap assertion for the seed defect: New Event FAB was landing on the +// Admin tab at 390×844, clipping the color-legend chips behind the BottomTabBar. +// Fixed by calc(var(--bottom-chrome-h) + var(--space-6)) in CalendarShell.tsx. +// Runs on iphone + pixel (the profiles that exposed the issue); skipped on desktop. + +test.describe('D-01 regression guard — FAB does not overlap BottomTabBar', () => { + 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' }); + + await expect(fab).toBeVisible(); + await expect(nav).toBeVisible(); + + const fabBox = await fab.boundingBox(); + const navBox = await nav.boundingBox(); + + expect(fabBox, 'New Event FAB bounding box must not be null').not.toBeNull(); + expect(navBox, 'BottomTabBar bounding box must not be null').not.toBeNull(); + + expect( + fabBox!.y + fabBox!.height, + `FAB bottom edge (${fabBox!.y + fabBox!.height}) must be ≤ BottomTabBar top edge (${navBox!.y}) — no overlap`, + ).toBeLessThanOrEqual(navBox!.y); + }); +}); + // ── Harness self-validation — injected-defect proofs (TEST-01 acceptance bar) ── // // Each test is a PASSING test that proves the assertion would have failed under a From 28e9ca984039bd336ba50017a1cb7e3b226a258c Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Thu, 18 Jun 2026 12:54:30 -0400 Subject: [PATCH 3/3] =?UTF-8?q?docs(17-03):=20complete=20phone=20layout=20?= =?UTF-8?q?overlap=20fix=20plan=20=E2=80=94=20SUMMARY.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FAB lifted above BottomTabBar, phone content padding added, D-01 regression guard committed to layout.spec.ts; 3-profile e2e sweep green. --- .../17-03-SUMMARY.md | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 .planning/phases/17-ui-optimization-polish/17-03-SUMMARY.md diff --git a/.planning/phases/17-ui-optimization-polish/17-03-SUMMARY.md b/.planning/phases/17-ui-optimization-polish/17-03-SUMMARY.md new file mode 100644 index 0000000..2911038 --- /dev/null +++ b/.planning/phases/17-ui-optimization-polish/17-03-SUMMARY.md @@ -0,0 +1,127 @@ +--- +phase: "17" +plan: "03" +subsystem: pwa/layout +tags: [layout-fix, fab-overlap, bottom-tab-bar, phone-only, regression-guard, css-tokens, d-01, d-02] +dependency_graph: + requires: + - apps/pwa/src/styles/tokens.css — --bottom-chrome-h token (provided by plan 17-01) + provides: + - FAB overlap fix: CalendarShell.tsx FAB bottom = calc(var(--bottom-chrome-h) + var(--space-6)) + - Phone content padding: App.tsx contentStyle paddingBottom = var(--bottom-chrome-h) (phone-only) + - Regression guard: apps/pwa/e2e/layout.spec.ts — D-01 overlap assertion (iphone + pixel) + affects: + - apps/pwa/src/components/CalendarShell.tsx (FAB bottom offset) + - apps/pwa/src/App.tsx (contentStyle paddingBottom) + - apps/pwa/e2e/layout.spec.ts (new overlap assertion) +tech_stack: + added: [] + patterns: + - "CSS calc() combining --bottom-chrome-h token with --space-6 for FAB clearance above BottomTabBar" + - "Spread pattern ...(phone ? { paddingBottom: var(--bottom-chrome-h) } : {}) for phone-only contentStyle" + - "Playwright boundingBox() geometry assertion: fabBox.y + fabBox.height <= navBox.y" +key_files: + created: [] + modified: + - apps/pwa/src/components/CalendarShell.tsx + - apps/pwa/src/App.tsx + - apps/pwa/e2e/layout.spec.ts +decisions: + - "FAB bottom uses calc(var(--bottom-chrome-h) + var(--space-6)) — positions FAB 24px above bar top edge regardless of safe-area-inset value" + - "contentStyle paddingBottom is phone-only via spread pattern — desktop has no BottomTabBar and must not gain extra bottom padding (RESEARCH Pitfall 2)" + - "Overlap assertion skips desktop profile — the desktop New Event button is a toolbar button, not the FAB; geometry check is semantically wrong for sidebar layout" + - "Full e2e suite run on all three profiles confirmed zero regressions from layout changes" +metrics: + duration: "~18 minutes" + completed_date: "2026-06-18" + tasks_completed: 3 + tasks_total: 3 + files_changed: 3 +status: complete +--- + +# Phase 17 Plan 03: Phone Layout Overlap Fix — Summary + +Fixed the long-standing phone (≤767px) fixed-chrome overlap where the `position: fixed` BottomTabBar was covering the New Event FAB and occluding the bottom color-legend chips, using the `--bottom-chrome-h` token from plan 17-01. Added a permanent D-01 overlap regression assertion to `layout.spec.ts`. Full 3-profile Playwright suite is green. + +## What Was Built + +**One-liner:** FAB lifted above BottomTabBar via `calc(var(--bottom-chrome-h) + var(--space-6))`, phone content reserves `--bottom-chrome-h` padding, permanent overlap regression guard added to CI. + +### Task 1: FAB lift + phone content padding (CalendarShell.tsx, App.tsx) + +**CalendarShell.tsx** (phone-only FAB, lines 463–491): +- Changed `bottom: 'var(--space-6)'` → `bottom: 'calc(var(--bottom-chrome-h) + var(--space-6))'` +- FAB now sits 24px (var(--space-6)) above the BottomTabBar top edge regardless of safe-area-inset +- All other FAB style properties unchanged (right, width, height, zIndex, etc.) + +**App.tsx** (contentStyle, lines 155–164): +- Added phone-only spread: `...(phone ? { paddingBottom: 'var(--bottom-chrome-h)' } : {})` +- Phone content area now reserves space equal to the BottomTabBar height +- Desktop branch has no paddingBottom — geometry unchanged +- Build: `pnpm --filter @familysync/pwa build` exits 0 + +### Task 2: FAB↔BottomTabBar overlap regression assertion (layout.spec.ts) + +Added new `test.describe` block "D-01 regression guard — FAB does not overlap BottomTabBar": +- Test name: "New Event FAB does not overlap BottomTabBar (A — phone only)" +- Skips on desktop (`testInfo.project.name === 'desktop'`) +- Locates FAB via `page.getByRole('button', { name: 'New Event' })` and bar via `page.getByRole('navigation', { name: 'Main navigation' })` +- Asserts `fabBox.y + fabBox.height <= navBox.y` (FAB bottom ≤ BottomTabBar top) +- Passes GREEN on iphone (390×844 WebKit) and pixel (412×915 Chromium) + +### Task 3: Small-viewport sweep across all three profiles + +- `pnpm --filter @familysync/pwa test:e2e` (all 3 profiles): **115 passed, 23 skipped, 0 failed** + - iphone profile: all layout.spec.ts assertions pass including new D-01 guard + - pixel profile: all layout.spec.ts assertions pass including new D-01 guard + - desktop profile: all layout.spec.ts assertions pass; D-01 guard correctly skipped +- No violations flagged by Rules 1–4 assertions +- Admin tab tap target still meets ≥44px after layout changes +- Color-legend chips confirmed fully visible via playwright-cli screenshot at 390×844 + +## Visual Confirmation (playwright-cli @390×844) + +Screenshot taken via playwright-cli (Chromium, viewport 390×844, reloaded to pick up phone layout): +- **New Event FAB (+)**: clearly positioned above the BottomTabBar with visible gap +- **Color-legend chips** ("Dev User" blue, "Family" pink): fully visible between calendar content and BottomTabBar — not clipped or occluded +- **BottomTabBar** (Calendar, Lists, Admin): fully visible at the bottom edge + +## Verification Results + +| Check | Result | +|-------|--------| +| `grep 'calc(var(--bottom-chrome-h) + var(--space-6))'` in CalendarShell.tsx | PASS | +| `grep "paddingBottom: 'var(--bottom-chrome-h)'"` in App.tsx | PASS | +| `grep 'New Event FAB does not overlap BottomTabBar'` in layout.spec.ts | PASS | +| `pnpm --filter @familysync/pwa build` | PASS | +| Playwright iphone + pixel layout.spec.ts (overlap assertion) | PASS — 32 passed, 2 skipped | +| Full 3-profile `pnpm --filter @familysync/pwa test:e2e` | PASS — 115 passed, 23 skipped, 0 failed | +| playwright-cli visual confirmation @390×844 | PASS — FAB above bar, legend chips visible | +| Desktop geometry unchanged | PASS — no desktop layout changes | + +## Deviations from Plan + +None. Plan executed exactly as written. The three-task sequence (CSS fix, regression guard, sweep) was completed without any deviations. The FAB bottom value, phone contentStyle spread pattern, and overlap assertion all match the plan specification exactly. + +## Known Stubs + +None. All changes are wired and fully functional. + +## Threat Flags + +None. CSS-only layout offsets plus a Playwright geometry test — no new network endpoints, auth paths, or data flows introduced. + +## Self-Check: PASSED + +| Item | Result | +|------|--------| +| CalendarShell.tsx exists | FOUND | +| App.tsx exists | FOUND | +| layout.spec.ts exists | FOUND | +| SUMMARY.md exists | FOUND | +| Commit 5e1c714 (Task 1) | FOUND | +| Commit 85a803f (Task 2) | FOUND | +| CalendarShell FAB bottom grep | PASS | +| App.tsx paddingBottom grep | PASS | +| layout.spec.ts overlap assertion grep | PASS |