chore: merge executor worktree (worktree-agent-ad448992e936c77aa)
This commit is contained in:
@@ -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 |
|
||||||
@@ -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) ──
|
// ── 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
|
// Each test is a PASSING test that proves the assertion would have failed under a
|
||||||
|
|||||||
@@ -160,6 +160,8 @@ export default function App() {
|
|||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
position: 'relative',
|
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).
|
// Setup gate: while setup status is loading, render nothing (prevent flash).
|
||||||
|
|||||||
@@ -467,7 +467,7 @@ export function CalendarShell() {
|
|||||||
onClick={() => setEventForm(true, 'create')}
|
onClick={() => setEventForm(true, 'create')}
|
||||||
style={{
|
style={{
|
||||||
position: 'fixed',
|
position: 'fixed',
|
||||||
bottom: 'var(--space-6)',
|
bottom: 'calc(var(--bottom-chrome-h) + var(--space-6))',
|
||||||
right: 'var(--space-6)',
|
right: 'var(--space-6)',
|
||||||
width: '56px',
|
width: '56px',
|
||||||
height: '56px',
|
height: '56px',
|
||||||
|
|||||||
Reference in New Issue
Block a user