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

16 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, user_setup, must_haves
phase plan type wave depends_on files_modified autonomous requirements user_setup must_haves
02-calendar-display 05 execute 4
02-04
apps/pwa/src/components/EventDetailPopover.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
apps/pwa/src/components/CalendarShell.tsx
apps/pwa/src/components/EventProof.tsx
apps/pwa/src/components/EventDetailPopover.test.tsx
false
CAL-02
CAL-03
CAL-07
truths artifacts key_links
Tapping any event opens a read-only detail popover (title, date/time, location, description, calendar name + owner color) built for Phase 3 reuse as the edit surface
A color legend (member → color, plus 'Family' rose row) is always visible so ownership is decodable
Initial load shows a shimmer skeleton; a successful fetch with zero events shows the empty state; a failed fetch shows the error state with a working Retry
All chrome (AppNav, ViewToolbar, legend) is token-styled with 44px minimum touch targets and is keyboard/focus accessible
The popover never renders event fields via dangerouslySetInnerHTML (XSS guard) and traps focus with Escape-to-close
path provides exports
apps/pwa/src/components/EventDetailPopover.tsx read-only event detail popover; Phase-3-reusable edit surface; focus trap + XSS-safe
EventDetailPopover
path provides exports
apps/pwa/src/components/ColorLegend.tsx always-visible member→color legend with Family row
ColorLegend
path provides exports
apps/pwa/src/components/SkeletonCalendar.tsx shimmer loading skeleton (month + agenda variants)
SkeletonCalendar
from to via pattern
apps/pwa/src/components/CalendarShell.tsx apps/pwa/src/components/EventDetailPopover.tsx customComponents.eventModal + openEventId from Zustand EventDetailPopover
from to via pattern
apps/pwa/src/components/CalendarShell.tsx SkeletonCalendar | EmptyState | error state TanStack Query isLoading/empty/isError branches SkeletonCalendar
Complete the calendar UX: the read-only EventDetailPopover (built for Phase 3 reuse), the always-visible ColorLegend, the AppNav + ViewToolbar chrome, and the loading / empty / error states — all token-styled, accessible, and touch-friendly. Wire the popover and state branches into CalendarShell, retire the Phase 1 EventProof, and gate the phase on a human visual verification.

Purpose: D-07 (legend), D-08 (tap-to-expand popover reusable in Phase 3), and the "slick" constraint (skeleton/empty/error polish) land here. This closes the four phase success criteria into a verifiable, glanceable calendar. Output: popover + legend + nav + toolbar + skeleton + empty/error states wired into CalendarShell; EventProof removed; human-verify checkpoint.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/02-calendar-display/02-UI-SPEC.md @.planning/phases/02-calendar-display/02-PATTERNS.md @.planning/phases/02-calendar-display/02-04-SUMMARY.md Task 1: EventDetailPopover (read-only, accessible, XSS-safe, Phase-3-reusable) + wire into CalendarShell apps/pwa/src/components/EventDetailPopover.tsx, apps/pwa/src/components/CalendarShell.tsx, apps/pwa/src/components/EventDetailPopover.test.tsx - apps/pwa/src/components/CalendarShell.tsx (Plan 04: openEventId via setOpenEventId, reserved customComponents.eventModal slot, the hydrated events in the TanStack Query cache) - apps/pwa/src/App.tsx MemberBadge (component prop + inline-style analog, lines 3655) - apps/pwa/src/store/calendarStore.ts (openEventId / setOpenEventId) - .planning/phases/02-calendar-display/02-UI-SPEC.md §"EventDetailPopover" + §"Interaction Contract: Keyboard / accessibility" + §"Copywriting Contract" (close = "×", aria-label="Close") - .planning/phases/02-calendar-display/02-PATTERNS.md §"apps/pwa/src/components/EventDetailPopover.tsx" (focus trap, Escape, never dangerouslySetInnerHTML) - .planning/phases/02-calendar-display/02-RESEARCH.md §"Pattern 4" (customComponents eventModal) + §Security (XSS via event fields) - popover renders title (heading), date/time line, location line when present, description block when present, and calendar name + owner color swatch - Escape closes the popover and clears openEventId; clicking the backdrop closes it - event title/description rendered as plain text children (no dangerouslySetInnerHTML) - close button has aria-label="Close" and a ≥44px touch target Create `apps/pwa/src/components/EventDetailPopover.tsx` exporting `EventDetailPopover`. It resolves the open event from the TanStack Query events cache by `openEventId` (Zustand) — or accepts the Schedule-X eventModal `calendarEvent` prop when used as `customComponents.eventModal`. Render per UI-SPEC §EventDetailPopover: color chip + title (--text-heading), date/time (--text-label, secondary), location with a lucide-react MapPin icon if present, description block (max 4 lines then scroll), and a calendar-name + owner-color-swatch footer. Reserve an empty footer action area with a comment noting Phase 3 adds edit/delete here (D-08). On phone render as a bottom sheet; on tablet/desktop as an anchored popover (max-width 360px) — use the --bp-* tokens. Implement: focus trap while open, Escape closes (calls setOpenEventId(null)), focus returns to the triggering element, close "×" button aria-label="Close" with min 44px target, backdrop tap closes. NEVER use dangerouslySetInnerHTML for any event field (XSS guard). Token-styled only.
Wire into CalendarShell: pass `customComponents={{ eventModal: EventDetailPopover }}` to `<ScheduleXCalendar>`, and ensure the event-click path sets openEventId so the popover opens. Keep the eventsService/eventModal plugin wiring from Plan 04.

Write `EventDetailPopover.test.tsx` (jsdom): renders an event's fields as text, Escape triggers close, and asserts no dangerouslySetInnerHTML usage (render a title containing an HTML-looking string and assert it appears escaped as text).
cd apps/pwa && pnpm test -- src/components/EventDetailPopover.test.tsx cd apps/pwa && grep -q "customComponents" src/components/CalendarShell.tsx && grep -q "EventDetailPopover" src/components/CalendarShell.tsx && ! grep -q "dangerouslySetInnerHTML" src/components/EventDetailPopover.tsx && echo POPOVER_WIRED_XSS_SAFE - EventDetailPopover.tsx exports EventDetailPopover and renders title/time/location/description/calendar-name+color - Escape closes and clears openEventId; close button has aria-label="Close" and ≥44px target - No dangerouslySetInnerHTML anywhere in EventDetailPopover.tsx - CalendarShell passes customComponents.eventModal = EventDetailPopover - EventDetailPopover.test.tsx green incl. the escaped-HTML-as-text assertion Tap-to-expand read-only popover (accessible, XSS-safe, Phase-3-reusable) wired into the calendar; test green. Task 2: ColorLegend + AppNav + ViewToolbar + skeleton/empty/error states; retire EventProof 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, apps/pwa/src/components/CalendarShell.tsx, apps/pwa/src/components/EventProof.tsx - apps/pwa/src/components/CalendarShell.tsx (Task 1 + Plan 04: eventsQuery isLoading/isError/data, calendars config for legend, selectedView/setSelectedView) - apps/pwa/src/App.tsx (MemberBadge + ColorSwatch analogs for legend swatches) - apps/pwa/src/components/EventProof.tsx (Phase 1 proof component to delete; confirm no remaining imports) - .planning/phases/02-calendar-display/02-UI-SPEC.md §"Component Inventory" (ColorLegend, AppNav, ViewToolbar, SkeletonCalendar, EmptyState) + §"Copywriting Contract" + §"Interaction Contract" + §"Post-Verification Reviewer Notes" (grid is primary focal point; phone-nav avatar needs aria-label+title) - .planning/phases/02-calendar-display/02-PATTERNS.md §"ColorLegend", §"SkeletonCalendar" (shimmer keyframe), §"CSS Token Usage in Components" Create the chrome and state components, token-styled, 44px touch targets, accessible per UI-SPEC: - `ColorLegend.tsx`: one row per member (12px color circle + display name) plus a "Family" row using --color-shared-family. Always rendered, non-interactive (filter deferred, D-07). Swatch aria-label="{name}: {hex}". Members sourced from the calendars config / /api/me. - `AppNav.tsx`: phone = 48px top bar (app name "FamilySync" left, user color swatch right with aria-label + title per reviewer note); tablet/desktop = 240px left sidebar (app name + ColorLegend). Accent colors NOT used on chrome (UI-SPEC 60/30/10). - `ViewToolbar.tsx`: Today | < | > | [Day][Week][Month][Agenda]. Buttons role="button", keyboard-activatable, 44px min height, --text-label. Active view uses a subtle surface tint (NOT accent). Calls setSelectedView + drives Schedule-X view; prev/next/today drive Schedule-X navigation. - `SkeletonCalendar.tsx`: shimmer (the @keyframes shimmer from tokens.css), month variant = 6×7 placeholder grid, agenda variant = 4 date-group blocks; root aria-busy="true", aria-label="Loading calendar". No spinner. - `EmptyState.tsx`: centered lucide-react CalendarDays (32px, --color-text-muted) + heading "Nothing here" + body "No events in this period. Try a different date or switch views." (UI-SPEC copy).
In CalendarShell, render AppNav + ViewToolbar + ColorLegend chrome around `<ScheduleXCalendar>` (grid is the primary focal point per reviewer note). Branch on the events query: `isLoading` (initial) → SkeletonCalendar; success + `occurrences.length === 0` → EmptyState; `isError` (after retry:2) → error state replacing the grid with heading "Couldn't load events", body "Check your connection and try again.", and a "Retry" button calling `queryClient.refetchQueries({ queryKey: ['events'] })`. All token-styled.

Delete `apps/pwa/src/components/EventProof.tsx` and remove any remaining imports/references to it (Plan 04 removed it from the render path; confirm the file and its imports are gone).
cd apps/pwa && grep -q "SkeletonCalendar" src/components/CalendarShell.tsx && grep -q "EmptyState" src/components/CalendarShell.tsx && grep -q "Couldn't load events" src/components/CalendarShell.tsx && grep -q "ColorLegend" src/components/CalendarShell.tsx && echo STATES_WIRED cd /home/luc/Projects/familysync && ! test -f apps/pwa/src/components/EventProof.tsx && ! grep -rq "EventProof" apps/pwa/src && echo EVENTPROOF_REMOVED cd apps/pwa && pnpm exec tsc --noEmit && pnpm exec eslint src --max-warnings=0 2>/dev/null || pnpm exec tsc --noEmit - ColorLegend, AppNav, ViewToolbar, SkeletonCalendar, EmptyState exist and are token-styled (no hard-coded hex/px) - CalendarShell renders the chrome and branches loading→Skeleton, empty→EmptyState, error→error state with working Retry (refetchQueries(['events'])) - ViewToolbar buttons are 44px min height, keyboard-activatable; active view uses a surface tint not accent - EventProof.tsx is deleted and no references to it remain anywhere in apps/pwa/src - tsc --noEmit clean in apps/pwa Legend, nav, toolbar, and loading/empty/error states are wired and token-styled; EventProof retired; PWA typechecks. Task 3: [CHECKPOINT] Visual + functional verification of the calendar (4 success criteria) Operator-only manual verification: run the dev stack behind the dev-auth bypass and confirm all four phase success criteria against the live calendar UI. No code is written in this task. Follow the steps in how-to-verify exactly and report pass/fail per criterion. The complete read-only calendar: unified color-coded events across day/week/month/agenda, the always-visible color legend, tap-to-expand read-only detail popover, and polished skeleton/empty/error states — all on the clean token theme, behind the dev-auth bypass. 1. Start the stack in dev with the bypass: ensure `NODE_ENV` is not production and `DEV_AUTH_BYPASS=true`, then run the API + PWA dev servers (e.g. `pnpm -r dev` or the project's documented dev command). Confirm the shared-family calendar was marked is_shared (Plan 02 checkpoint). 2. Open the PWA in a desktop browser. CONFIRM (success criterion 1): events appear color-coded — each member's events in their assigned color, shared-family events in the rose; the legend decodes which color is whom. 3. Switch Day / Week / Month / Agenda (success criterion 2): all events render correctly in each view; no missing or misplaced events. 4. Find a recurring event (e.g. a weekly meeting) and confirm (success criterion 3) all its occurrences show in the current window; navigate across a DST boundary (March 2026) and confirm the time does not jump ±1 hour. 5. Find an all-day event (birthday/holiday) and confirm (success criterion 4) it appears as a full-day banner on the correct date — not shifted a day early/late. 6. Tap an event: the read-only popover opens with title/time/location/description; Escape and backdrop-tap both close it. 7. Resize to a phone width (or open on a phone via the dev URL): confirm the default view is Agenda and the popover is a bottom sheet. 8. Force the empty state (navigate to a far-future empty window) and the error state (stop the API, hit Retry) and confirm both render as specified. Type "approved" if all four success criteria hold, or describe the specific view/event/state that is wrong.

<threat_model>

Trust Boundaries

Boundary Description
event fields → popover DOM title/location/description rendered into the popover

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-02e-01 Tampering (XSS) EventDetailPopover event fields mitigate Plain-text JSX children only; no dangerouslySetInnerHTML; test asserts HTML-looking title renders escaped
T-02e-02 Denial of service Retry button hammering /api/events accept Manual user action, retry:2 backoff already on the query; two-person self-hosted app, negligible risk
</threat_model>
- `pnpm --filter @familysync/pwa test` green (popover test) - `pnpm -r test` + `tsc --noEmit` clean in both workspaces (phase gate) - Human-verify checkpoint passes all four success criteria

<success_criteria>

  • Color-coded events + legend decode ownership (criterion 1)
  • All four views render events correctly (criterion 2)
  • Recurring occurrences correct incl. DST (criterion 3)
  • All-day events as full-day banners with no shift (criterion 4)
  • Tap-to-expand popover + skeleton/empty/error states polished and accessible </success_criteria>

<artifacts_produced>

Artifacts this phase produces (Plan 05)

  • EventDetailPopover (React component, Phase-3-reusable edit surface) — EventDetailPopover.tsx
  • ColorLegend, AppNav, ViewToolbar, SkeletonCalendar, EmptyState (React components)
  • CalendarShell: chrome + loading/empty/error branches + customComponents.eventModal wiring
  • EventProof.tsx DELETED (Phase 1 proof component retired)
  • EventDetailPopover.test.tsx </artifacts_produced>
Create `.planning/phases/02-calendar-display/02-05-SUMMARY.md` when done