--- phase: 03-event-write-back-pwa-install plan: 12 type: tdd wave: 1 depends_on: [] gap_closure: true autonomous: true requirements: [CAL-05, CAL-07, PWA-01, PWA-02] files_modified: - apps/pwa/src/components/EventForm.tsx - apps/pwa/src/components/EventForm.test.tsx must_haves: truths: - "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)" artifacts: - path: apps/pwa/src/components/EventForm.tsx provides: "occurrence-driven reset, recurrence derivation, zone-consistent parseDateTime, real focus trap" key_links: - from: "EventForm reset effect" to: "occurrence from TanStack cache" via: "occurrence (or occurrence?.uid) in effect deps" pattern: "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. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.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 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. 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. 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. Create `.planning/phases/03-event-write-back-pwa-install/03-12-SUMMARY.md` when done.