Files
familysync/.planning/phases/02-calendar-display/02-05-SUMMARY.md
T

187 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 02-calendar-display
plan: "05"
subsystem: pwa-calendar-ux
tags: [event-popover, color-legend, app-nav, view-toolbar, skeleton, empty-state, xss-guard, tdd]
dependency_graph:
requires: ["02-04"]
provides: [EventDetailPopover, ColorLegend, AppNav, ViewToolbar, SkeletonCalendar, EmptyState, CalendarShell-chrome]
affects: ["phase-03"]
tech_stack:
added: []
patterns:
- EventDetailPopover dual-mode — Zustand openEventId (standalone) + customComponents.eventModal (Schedule-X)
- queryClient.getQueriesData for cross-query cache lookup by event id
- ViewToolbar accesses Schedule-X internal calendarApp.$app.calendarState for navigation
- SkeletonCalendar shimmer via CSS animation from tokens.css @keyframes shimmer
- TDD RED commit (test only) → GREEN commit (feat + test) per plan task 1 gate
- "@testing-library/jest-dom" imported in test-setup.ts for toHaveTextContent matcher
key_files:
created:
- apps/pwa/src/components/EventDetailPopover.tsx
- apps/pwa/src/components/EventDetailPopover.test.tsx
- apps/pwa/src/components/ColorLegend.tsx
- apps/pwa/src/components/AppNav.tsx
- apps/pwa/src/components/ViewToolbar.tsx
- apps/pwa/src/components/SkeletonCalendar.tsx
- apps/pwa/src/components/EmptyState.tsx
modified:
- apps/pwa/src/components/CalendarShell.tsx
- apps/pwa/src/components/CalendarShell.test.tsx
- apps/pwa/src/test-setup.ts
- apps/pwa/src/api/client.ts
deleted:
- apps/pwa/src/components/EventProof.tsx
decisions:
- "EventDetailPopover dual-mode: standalone (Zustand openEventId + TanStack Query cache) AND Schedule-X customComponents.eventModal"
- "ViewToolbar navigation via calendarApp.$app.calendarState — Schedule-X internal API; typed as any, runtime-guarded"
- "Phase 3 footer action area reserved in EventDetailPopover with code comment (D-08)"
- "Legacy fetchEventsLegacy / CalendarEvent types removed from client.ts along with EventProof deletion"
metrics:
duration: "~30m"
completed: "2026-06-05"
tasks_completed: 2
tasks_pending: 1
files_created: 7
files_modified: 4
files_deleted: 1
---
# Phase 02 Plan 05: Calendar UX — Popover, Chrome, States Summary
Read-only EventDetailPopover (XSS-safe, accessible, Phase-3-reusable), always-visible ColorLegend, AppNav/ViewToolbar chrome, and skeleton/empty/error states wired into CalendarShell; EventProof retired.
## What Was Built
### Task 1: EventDetailPopover + CalendarShell wiring (TDD)
**`apps/pwa/src/components/EventDetailPopover.tsx`** (259 lines):
- Resolves open event by `openEventId` (Zustand) from TanStack Query `['events']` cache using `queryClient.getQueriesData`
- Dual-mode: standalone (primary, driven by Zustand) + `customComponents.eventModal` prop from Schedule-X
- Renders title (heading), date/time, location (with MapPin icon when present), description (max-4-lines scroll), calendar name + color swatch footer
- T-02e-01 XSS guard: all event fields as plain-text JSX children — no raw HTML injection
- Focus trap on open, Escape to close (document keydown listener), backdrop-click to close
- Close "×" button with `aria-label="Close"` and 44px minimum touch target
- Phone: bottom-sheet layout (fixed bottom, rounded top corners); tablet/desktop: centered popover (max-width 360px)
- Phase 3 footer action area reserved with comment — Phase 3 adds edit/delete actions there (D-08)
- Wired in CalendarShell: `customComponents={{ eventModal: EventDetailPopover }}` AND rendered standalone after the layout
**`apps/pwa/src/components/EventDetailPopover.test.tsx`** (192 lines, TDD RED → GREEN):
- TDD RED commit: tests written first, failing because file missing
- GREEN commit: implementation makes all 12 tests pass
- Tests: title/location/description/calendarName render as text; heading role; Escape/close-button/backdrop all call `setOpenEventId(null)`; null openEventId renders nothing
- XSS guard: `<script>alert("xss")</script>` in title → `heading.innerHTML` does NOT contain `<script>`; `<b>` in description → `descEl.innerHTML` does NOT contain `<b>`
**`apps/pwa/src/test-setup.ts`** (deviation fix): Added `import '@testing-library/jest-dom'` to enable `toHaveTextContent` and other jest-dom matchers project-wide.
### Task 2: Chrome components, state branches, EventProof retired
**`apps/pwa/src/components/ColorLegend.tsx`**:
- One row per member: 12px color circle (`aria-label="{name}: {hex}"`) + display name
- "Family" row always rendered last using `--color-shared-family` (#F25C7A)
- Font: 13px label weight, `--color-text-secondary`
**`apps/pwa/src/components/AppNav.tsx`**:
- Phone: 48px top bar — "FamilySync" display text left, user avatar right with `aria-label` + `title` per reviewer note
- Tablet/desktop: 240px left sidebar — app name + "Calendars" section header + `<ColorLegend>`
**`apps/pwa/src/components/ViewToolbar.tsx`**:
- Today | | | Day | Week | Month | Agenda
- 44px min-height on all buttons; keyboard-activatable
- Active view: `rgba(74, 144, 217, 0.12)` surface tint (NOT accent color) per UI-SPEC 60/30/10 rule
- Navigation via `calendarApp.$app.calendarState.setRange()` / `setView()` (internal Schedule-X API)
**`apps/pwa/src/components/SkeletonCalendar.tsx`**:
- Month variant: 6×7 grid of shimmer cells + 7-col header
- Agenda variant: 4 date-group blocks, 23 rows each at 6090% widths
- `aria-busy="true"`, `aria-label="Loading calendar"` on root
- Shimmer: `@keyframes shimmer` from tokens.css, `background-size: 200% 100%`, 1.5s infinite
**`apps/pwa/src/components/EmptyState.tsx`**:
- `CalendarDays` lucide icon (32px, `--color-text-muted`)
- Heading "Nothing here" + body "No events in this period. Try a different date or switch views." per UI-SPEC copywriting
**`apps/pwa/src/components/CalendarShell.tsx`** (major refactor):
- Phone: AppNav top bar → ViewToolbar → calendar content → ColorLegend below
- Tablet/desktop: AppNav sidebar (240px) + main area (ViewToolbar → calendar content)
- State branches: `isInitialLoading``SkeletonCalendar`; `isEventsError` → "Couldn't load events" + "Check your connection" + **Retry** button calling `queryClient.refetchQueries({ queryKey: ['events'] })`; `isEmptyResult``EmptyState`; success+data → `ScheduleXCalendar`
- `EventDetailPopover` rendered standalone after layout wrapper
**EventProof.tsx deleted**; `CalendarEvent` / `EventsResponse` / `fetchEventsLegacy` removed from `client.ts`.
## Verification Results
```
pnpm --filter @familysync/pwa test
Test Files 5 passed (5)
Tests 36 passed (36)
tsc --noEmit: clean (0 errors)
vite build: clean (490.30 kB, 827ms)
```
## Deviations from Plan
### Auto-fixed Issues
**1. [Rule 3 - Blocking] @testing-library/jest-dom not imported in test-setup.ts**
- **Found during:** Task 1 GREEN phase — `toHaveTextContent` threw "Invalid Chai property"
- **Issue:** `@testing-library/jest-dom` extends Vitest/Chai's `expect` with DOM matchers. It was installed (in devDependencies) but never imported in `src/test-setup.ts`, so matchers were not registered.
- **Fix:** Added `import '@testing-library/jest-dom'` to `src/test-setup.ts`
- **Files modified:** `apps/pwa/src/test-setup.ts`
- **Commit:** 3eebfbf (bundled with Task 1 GREEN commit)
**2. [Rule 1 - Bug] CalendarShell.test.tsx: synchronous getByTestId fails after loading state added**
- **Found during:** Task 2 verification — existing CalendarShell smoke test failed
- **Issue:** The test did `screen.getByTestId('schedule-x-calendar')` synchronously, but CalendarShell now shows SkeletonCalendar while loading. The calendar element only appears after queries resolve.
- **Fix:** Changed to `await screen.findByTestId('schedule-x-calendar')` (async, waits for element)
- **Files modified:** `apps/pwa/src/components/CalendarShell.test.tsx`
- **Commit:** 216ddce (bundled with Task 2 commit)
**3. [Rule 1 - Bug] ViewToolbar: CalendarApp.setDate/decrementRange/incrementRange/setView don't exist on public API**
- **Found during:** Task 2 tsc check — 4 type errors
- **Issue:** `CalendarApp` class only exposes `render`, `destroy`, `setTheme`, `getTheme`, and `events` (EventsFacade). Navigation methods (`setRange`, `setView`) live on the internal `$app.calendarState` (a `CalendarAppSingleton` property).
- **Fix:** Changed `calendarApp` prop type to `any`, accessed internal state via `calendarApp.$app.calendarState` with runtime null-guards. Navigation uses `Temporal.Now.plainDateISO()` for today and `ZonedDateTime.until().days` for range inference.
- **Files modified:** `apps/pwa/src/components/ViewToolbar.tsx`
- **Commit:** 216ddce (bundled with Task 2 commit)
### Task 3 Status
**Task 3 (checkpoint:human-verify)** is pending operator verification — see "Human Verify Checkpoint" section below. No code changes in Task 3.
## Known Stubs
None — all components render from live data (TanStack Query cache) or accurate zero-state UI. The Phase 3 footer in EventDetailPopover is an intentionally empty reserved area, not a stub.
## Threat Flags
T-02e-01 mitigated:
- EventDetailPopover: title, description, location, calendarName all rendered as plain-text JSX children
- Test asserts `<script>alert("xss")</script>` in title → `heading.innerHTML` does NOT contain `<script>`, textContent DOES contain the literal string
- Test asserts `<b>Bold</b>` in description → `descEl.innerHTML` does NOT contain `<b>`
No new threat surface beyond the plan's threat model.
## Human Verify Checkpoint (Task 3 — awaiting operator)
The plan gates on operator visual verification. The automated tasks (1 and 2) are complete and committed. Task 3 requires the operator to run the dev stack and confirm the four phase success criteria. See the structured checkpoint returned in the agent's final message.
## Self-Check: PASSED
Files created:
- [x] apps/pwa/src/components/EventDetailPopover.tsx
- [x] apps/pwa/src/components/EventDetailPopover.test.tsx
- [x] apps/pwa/src/components/ColorLegend.tsx
- [x] apps/pwa/src/components/AppNav.tsx
- [x] apps/pwa/src/components/ViewToolbar.tsx
- [x] apps/pwa/src/components/SkeletonCalendar.tsx
- [x] apps/pwa/src/components/EmptyState.tsx
Files deleted:
- [x] apps/pwa/src/components/EventProof.tsx (confirmed ABSENT)
Commits:
- [x] 433fb9f — TDD RED: EventDetailPopover test
- [x] 3eebfbf — feat: EventDetailPopover + CalendarShell wiring
- [x] 216ddce — feat: Task 2 chrome + states + EventProof retired