Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-12-PLAN.md
T

10 KiB

phase, plan, type, wave, depends_on, gap_closure, autonomous, requirements, files_modified, must_haves
phase plan type wave depends_on gap_closure autonomous requirements files_modified must_haves
03-event-write-back-pwa-install 12 tdd 1
true true
CAL-05
CAL-07
PWA-01
PWA-02
apps/pwa/src/components/EventForm.tsx
apps/pwa/src/components/EventForm.test.tsx
truths artifacts key_links
Opening the form in edit mode populates Title/Start/End from the cached occurrence even when the form opens before the occurrence is resolved (no blank edit form)
Editing a recurring event preselects its existing recurrence preset instead of resetting to 'none'
The edit form shows the event's original date/time consistently (no UTC-date / local-time mismatch that shifts the day)
Tab and Shift+Tab cycle focus within the open dialog and never reach background controls
The PWA install assets (icon-192/512, apple-touch-icon) exist so Add-to-Home-Screen installs with a real icon (PWA-01/PWA-02)
path provides
apps/pwa/src/components/EventForm.tsx occurrence-driven reset, recurrence derivation, zone-consistent parseDateTime, real focus trap
from to via pattern
EventForm reset effect occurrence from TanStack cache occurrence (or occurrence?.uid) in effect deps occurrence
Fix the PWA edit form so editing actually works and the dialog is accessible. Today the edit form can open blank (the reset effect ignores `occurrence`, which is null if the events query has not resolved yet — WR-03), it hard-resets recurrence to 'none' so editing a recurring event silently drops its series (WR-03), it shows the wrong day/time by mixing a UTC date with local-clock components (WR-05), and its claimed focus trap only focuses once on open (WR-07). This plan closes the user-facing half of the write path and carries the PWA install requirements (assets verified present).

Purpose: edit mode pre-populates correctly and the dialog is keyboard-accessible. Output: an EventForm that round-trips an existing event's fields and traps focus.

<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/STATE.md @.planning/phases/03-event-write-back-pwa-install/03-REVIEW.md @.planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md @apps/pwa/src/components/EventForm.tsx @apps/pwa/src/api/client.ts @apps/pwa/src/store/calendarStore.ts

<artifacts_this_phase_produces> No new exported symbols. Internal changes to EventForm: reset effect deps gain occurrence, a recurrence-deriving initializer, a zone-consistent parseDateTime, and a real Tab/Shift+Tab focus-cycle handler. IN-03 (collapse the duplicate getDefaultStartDate/getDefaultEndDate to one helper) is folded in as a cheap adjacent cleanup. </artifacts_this_phase_produces>

Task 1: RED+GREEN — edit-mode population, recurrence derivation, zone-consistent dates (WR-03, WR-05, IN-03) apps/pwa/src/components/EventForm.tsx, apps/pwa/src/components/EventForm.test.tsx - apps/pwa/src/components/EventForm.tsx (occurrence IIFE lines 113-124; reset effect lines 167-181 with deps [eventFormOpen,eventFormMode,eventFormUid]; parseDateTime lines 84-101; getDefaultStartDate/EndDate lines 47-53) - apps/pwa/src/api/client.ts (CalendarOccurrence.start/end format note lines 69-73: 'YYYY-MM-DD' for allDay, ISO 8601 with IANA tz for timed) - apps/pwa/src/store/calendarStore.ts (setEventForm/eventFormMode/eventFormUid lines 43-44,162-163; a todayIso helper exists per IN-03) - .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md (WR-03, WR-05, IN-03) - RED (WR-03 blank): render EventForm in edit mode where the occurrence becomes available in the ['events'] cache AFTER the form opens; assert the Title input value equals the occurrence title (not empty). Fails today because the reset effect deps exclude `occurrence`. - RED (WR-03 recurrence): edit an occurrence whose recurrence is 'weekly'; assert the Repeat select value is 'weekly', not 'none'. - RED (WR-05 zone): a timed occurrence start of '2026-06-10T23:30:00-04:00[America/Toronto]' renders Start date '2026-06-10' and time '23:30' (the event's own wall time), not a UTC-shifted '2026-06-11'/'03:30'. WR-03: add `occurrence` (or `occurrence?.uid` plus `occurrence?.start`) to the reset effect dependency array so the form re-initializes when the occurrence resolves after open. In the reset effect, derive the initial recurrence from the occurrence instead of always `setRecurrence('none')` — if the CalendarOccurrence carries a recurrence preset use it; if the occurrence shape does not expose one, add the preset to the occurrence/expand contract is OUT OF SCOPE — instead read it from the cached raw recurrence if present and default to 'none' only when genuinely absent (document the limitation in a comment citing WR-03). Guard against opening edit mode before the cache is populated: keep fields blank-safe but re-run on arrival.
WR-05: rewrite `parseDateTime` so date and time are derived in ONE consistent frame. For a timed ISO
with an offset/IANA suffix, compute the local wall-clock components with `getFullYear/getMonth/getDate/
getHours/getMinutes` together (or reuse the Temporal-based conversion the calendar render path already
uses) — never mix `toISOString().slice(0,10)` (UTC date) with `getHours()` (local time). The all-day
`^\d{4}-\d{2}-\d{2}$` branch is unchanged.

IN-03: collapse `getDefaultStartDate`/`getDefaultEndDate` into one `todayIso()` helper (reuse the
existing one in calendarStore.ts if exported); keep the separate '09:00'/'10:00' default times at the
call sites.

Commit RED then GREEN.
cd apps/pwa && npx vitest run src/components/EventForm.test.tsx - behavior: edit form Title is populated even when occurrence resolves after open. - behavior: editing a recurring event preselects its recurrence preset. - behavior: a timed occurrence renders its own wall-clock date and time (no UTC/local day shift). - source: the reset effect dependency array in EventForm.tsx includes occurrence (grep for occurrence in the deps line). - test-command: `cd apps/pwa && npx vitest run src/components/EventForm.test.tsx` passes. Edit mode pre-populates correctly (fields, recurrence, correct zone); duplicate date helpers collapsed. Task 2: RED+GREEN — real focus trap on the dialog (WR-07) + verify PWA install assets (PWA-01/02, IN-04) apps/pwa/src/components/EventForm.tsx, apps/pwa/src/components/EventForm.test.tsx - apps/pwa/src/components/EventForm.tsx (focus-on-open effect lines 274-278; dialog element lines 378-384; Escape handler lines 263-270) - .planning/phases/03-event-write-back-pwa-install/03-REVIEW.md (WR-07, IN-04) - .planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md (modal/focus interaction contract) - RED: with the dialog open, dispatch a Tab keydown from the last focusable control; assert focus wraps to the first focusable control inside the dialog (not to background). Shift+Tab from the first wraps to the last. Fails today (only one .focus() on open; Tab escapes the modal). WR-07: implement an actual focus trap on the role="dialog" element. On Tab/Shift+Tab keydown while open: query the dialog's focusable elements (`button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])`), and if focus is on the last element and Tab is pressed, move to the first (preventDefault); if on the first and Shift+Tab, move to the last. Keep the existing focus-on-open behavior (Title input). Keep the Escape-to-close handler. Do NOT introduce a new dependency — implement the trap inline (or extract a small local hook). Update the docblock so the "Focus trap" claim is now accurate.
IN-04 / PWA-01 / PWA-02: this gap does not change install code, but the requirement must be verified.
The assets `apps/pwa/public/icon-192.png`, `icon-512.png`, and `apple-touch-icon.png` exist (confirmed
present). Add a lightweight assertion (test or a checked note in the SUMMARY) that these three files
exist so the Add-to-Home-Screen flow installs with a real icon. No code change required if assets present.

Commit RED then GREEN.
cd apps/pwa && npx vitest run src/components/EventForm.test.tsx - behavior: Tab from the last focusable control wraps to the first inside the dialog; Shift+Tab from the first wraps to the last. - behavior: focus never lands on a background control while the dialog is open. - source: `ls apps/pwa/public/icon-192.png apps/pwa/public/icon-512.png apps/pwa/public/apple-touch-icon.png` all exist (PWA-01/PWA-02 install assets). - test-command: `cd apps/pwa && npx vitest run src/components/EventForm.test.tsx` passes. The dialog traps Tab focus as its docblock claims; PWA install icon assets are confirmed present for Gate 2. - `cd apps/pwa && npx vitest run src/components/EventForm.test.tsx` green. - `cd apps/pwa && npm run build` (tsc + vite) succeeds. - Optional: drive the create→edit→delete flow with playwright-cli per CLAUDE.md to confirm end-to-end UX in a desktop browser.

<success_criteria> Edit mode pre-populates fields/recurrence in the correct zone, the dialog traps focus, and the PWA install assets are confirmed present. WR-03, WR-05, WR-07, IN-03, IN-04 closed; PWA-01/PWA-02 verified. </success_criteria>

Create `.planning/phases/03-event-write-back-pwa-install/03-12-SUMMARY.md` when done.