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

10 KiB
Raw Blame History


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: isInitialLoadingSkeletonCalendar; isEventsError → "Couldn't load events" + "Check your connection" + Retry button calling queryClient.refetchQueries({ queryKey: ['events'] }); isEmptyResultEmptyState; 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:

  • 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

Files deleted:

  • apps/pwa/src/components/EventProof.tsx (confirmed ABSENT)

Commits:

  • 433fb9f — TDD RED: EventDetailPopover test
  • 3eebfbf — feat: EventDetailPopover + CalendarShell wiring
  • 216ddce — feat: Task 2 chrome + states + EventProof retired