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

6.6 KiB

phase, plan, subsystem, tags, dependency_graph, tech_stack, key_files, decisions, metrics
phase plan subsystem tags dependency_graph tech_stack key_files decisions metrics
02-calendar-display 04 pwa-calendar-shell
schedule-x
tanstack-query
zustand
hydrate-events
temporal
calendar-shell
smoke-test
requires provides affects
02-02
02-03
CalendarShell
App-root-calendar
CAL-03-smoke-test
02-05
added patterns
useCalendarApp with separate plugins array (second arg, not inside config)
CalendarCallbacks nested under config.callbacks (onRangeUpdate, onEventClick)
DateRange.start/end are Temporal.ZonedDateTime — extract ISO date via .toPlainDate().toString()
eventsService.set() called in useEffect keyed on eventsQuery.data (Pitfall 4 guard)
window.matchMedia polyfill in vitest setupFiles for Zustand module-load safety
created modified
apps/pwa/src/components/CalendarShell.tsx
apps/pwa/src/components/CalendarShell.test.tsx
apps/pwa/src/test-setup.ts
apps/pwa/src/App.tsx
apps/pwa/vitest.config.ts
CalendarShell callbacks nested under config.callbacks per CalendarConfigExternal type (not top-level)
DateRange.start/end are Temporal.ZonedDateTime; extracted to ISO date via .toPlainDate().toString() for Zustand
test-setup.ts as vitest setupFiles for window.matchMedia polyfill — calendarStore creates Zustand store at module load time before test polyfills run
App.tsx simplified to single-line wrapper; EventProof and health probe removed from render path
duration completed tasks_completed files_created files_modified
~12m 2026-06-05 2 3 2

Phase 02 Plan 04: CalendarShell — Schedule-X Mounted, Wired to Data Pipeline Summary

Schedule-X CalendarShell component wired to TanStack Query windowed fetch, hydrateEvents Temporal conversion, and Zustand range management; all four views available; CAL-03 render smoke test with Temporal hydration guards.

What Was Built

Task 1: CalendarShell + App.tsx

apps/pwa/src/components/CalendarShell.tsx (186 lines):

  • useCalendarApp(config, [eventsService, eventModal]) with all four view factories: createViewDay, createViewWeek, createViewMonthGrid, createViewMonthAgenda
  • defaultView from Zustand persisted view (D-05 defaults: phone→month-agenda, tablet-desktop→month-grid already encoded in store)
  • firstDayOfWeek: SX_FIRST_DAY_OF_WEEK (7 = Sunday, Temporal convention) — Pitfall 1 guard
  • calendars config from buildCalendarConfig(members) keyed by String(userId) and 'shared'
  • config.callbacks.onRangeUpdate converts DateRange.start/end (Temporal.ZonedDateTime) to 'YYYY-MM-DD' strings for Zustand via .toPlainDate().toString(), triggering TanStack Query refetch
  • eventsService.set(hydrateEvents(...)) in useEffect keyed on eventsQuery.data — Pitfall 4 guard
  • Token-only styling (var(--color-*), var(--space-*), var(--font-family-base)) — no hardcoded hex/px
  • Sign-in required error state; slim loading indicator bar

apps/pwa/src/App.tsx: Replaced EventProof landing + health probe + MemberBadge with <CalendarShell /> single render.

Critical API finding (Deviation 1): onRangeUpdate and onEventClick are NOT top-level fields on CalendarConfigExternal. They live under config.callbacks (CalendarCallbacks type). The research pattern sketched them at the top level — the actual type required nesting.

Task 2: CalendarShell Render Smoke Test (CAL-03)

apps/pwa/src/components/CalendarShell.test.tsx (6 tests):

  • Render-without-throw smoke (validates @schedule-x/react@4.1.0@schedule-x/calendar@4.6.0 import compatibility — Pitfall 6)
  • ScheduleXCalendar mounts with non-null calendarApp
  • hydrateEvents called with both timed + all-day occurrences; eventsService.set() called with hydrated events
  • All-day occurrence → Temporal.PlainDate (Pitfall 4/all-day date shift guard)
  • Timed occurrence → Temporal.ZonedDateTime (Pitfall 4 guard)
  • Error state test for /api/me rejection

apps/pwa/src/test-setup.ts: window.matchMedia polyfill. Zustand's create() runs at module load time and calls window.matchMedia to derive the D-05 default view. This must be defined before any module importing calendarStore.ts is loaded — a Vitest setupFiles entry is the only reliable placement.

apps/pwa/vitest.config.ts: Added setupFiles: ['./src/test-setup.ts'].

Verification Results

Test Files  4 passed (4)
Tests       24 passed (24)

tsc --noEmit: clean (0 errors)
vite build: clean (474.27 kB, built in 395ms)

Deviations from Plan

Auto-fixed Issues

1. [Rule 1 - Bug] CalendarCallbacks nested under config.callbacks — not top-level

  • Found during: Task 1 — tsc reported onRangeUpdate not in CalendarConfigExternal
  • Issue: Research pattern (RESEARCH.md Pattern 4) showed onRangeUpdate at the top level of the config object. The actual type (CalendarConfigExternal extends Partial<ReducedCalendarConfigInternal>) carries callbacks?: CalendarCallbacks where CalendarCallbacks contains onRangeUpdate and onEventClick. They must be nested under config.callbacks.
  • Fix: Moved onRangeUpdate and onEventClick into callbacks: { ... } in the useCalendarApp config
  • Files modified: apps/pwa/src/components/CalendarShell.tsx
  • Commit: b79f649

2. [Rule 3 - Blocking] window.matchMedia not defined in jsdom

  • Found during: Task 2 — test run crashed at Zustand store initialisation
  • Issue: calendarStore.ts calls window.matchMedia inside readPersistedView() which runs at create() time — i.e. at module load, before any test-file-level polyfill runs. Inline Object.defineProperty in the test file is too late.
  • Fix: Created src/test-setup.ts with the polyfill; added setupFiles: ['./src/test-setup.ts'] to vitest.config.ts
  • Files modified: apps/pwa/src/test-setup.ts (new), apps/pwa/vitest.config.ts
  • Commit: f0af43c

Known Stubs

None — CalendarShell fetches real windowed data from /api/events, hydrates to Temporal, and renders via Schedule-X. The Plan 05 popover slot (setOpenEventId in onEventClick) is wired but the popover UI itself is Plan 05.

Threat Flags

No new threat surface beyond the plan's threat model.

  • T-02d-01 (XSS): CalendarShell uses React JSX default escaping for all event field rendering — no dangerouslySetInnerHTML. Carried to Plan 05 popover.

Self-Check: PASSED

Files created:

  • apps/pwa/src/components/CalendarShell.tsx
  • apps/pwa/src/components/CalendarShell.test.tsx
  • apps/pwa/src/test-setup.ts

Commits:

  • b79f649 — Task 1: CalendarShell + App.tsx
  • f0af43c — Task 2: CalendarShell smoke test