docs(02-05): complete plan 05 autonomous tasks — SUMMARY.md
This commit is contained in:
@@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
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, 2–3 rows each at 60–90% 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
|
||||||
Reference in New Issue
Block a user