---
phase: 06-ux-polish
plan: "06"
subsystem: pwa/event-form
tags: [event-form, recurrence, all-day, d-04, d-06, d-08, 999.6]
dependency_graph:
  requires:
    - Plan 06-01 (computeNewTimedEnd/computeNewAllDayEnd)
    - Plan 06-02 (RRULE UNTIL/COUNT serialization)
    - Plan 06-03 (hasRrule on CalendarOccurrence server source-of-truth)
    - Plan 06-05 (recurrenceUntil/recurrenceCount on CreateEventPayload; hasRrule client mirror)
  provides:
    - End-tracking start onChange handlers in EventForm (apps/pwa/src/components/EventForm.tsx)
    - Recurrence-bound "Ends" control in EventForm
    - SeriesEditPrompt component (apps/pwa/src/components/SeriesEditPrompt.tsx)
    - Schedule-X all-day pill override (apps/pwa/src/styles/index.css)
  affects: []
tech_stack:
  added: []
  patterns:
    - Schedule-X CSS override via real v4.6.0 classes (.sx__date-grid-event, .sx__month-grid-event)
    - Confirmation sheet/dialog mirroring DeleteConfirmationDialog (focus trap, Escape=cancel)
    - Conditional payload spread for recurrence bound fields (never/until/count)
key_files:
  created:
    - apps/pwa/src/components/SeriesEditPrompt.tsx
  modified:
    - apps/pwa/src/components/EventForm.tsx
    - apps/pwa/src/components/EventForm.test.tsx
    - apps/pwa/src/styles/index.css
decisions:
  - Schedule-X all-day CSS: the PLAN assumed `.sx__all-day-event` selector but Schedule-X v4.6.0 does not emit that class; the real selectors are `.sx__date-grid-event` (week/day view) and `.sx__month-grid-event:not(:has(.sx__month-grid-event-time))` (month view); `--sx-color-primary-container` CSS variable also remapped as fallback (follow-up fix 6dbb166 + 5620261)
  - Event create via real form not exercised: the dev-bypass user (id 1) has no CalDAV credential/calendars — those belong to user 2; server logic + form UI verified via route-mocks and direct DB occurrence inserts (dev-seed gap, not a code defect)
metrics:
  duration_minutes: 45
  completed_date: "2026-06-10"
  tasks_completed: 4
  files_changed: 5
---

# Phase 06 Plan 06: EventForm Integration Slice (999.6/7/8/9) Summary

**One-liner:** Wired `computeNewTimedEnd`/`computeNewAllDayEnd` into EventForm start handlers, added recurrence-bound "Ends" control, built `SeriesEditPrompt` for whole-series edits, and fixed all-day Schedule-X CSS to target real v4.6.0 classes.

## Tasks Completed

| Task | Name | Commit | Files |
|------|------|--------|-------|
| 1 | End-tracking wiring + recurrence-bound control in EventForm | `9aa15c4` (RED), `cbf5f98` (GREEN) | EventForm.tsx, EventForm.test.tsx |
| 2 | Series-edit confirmation prompt, gated on hasRrule | `96ef0b4` | SeriesEditPrompt.tsx, EventForm.tsx |
| 3 | All-day visual distinction — Schedule-X override | `883b00b` | index.css |
| 4 (checkpoint) | playwright-cli — all six event-form behaviors | (verification only) | — |
| Follow-up fix | Target real Schedule-X all-day class for filled pills | `6dbb166` | index.css |
| Follow-up fix | Remap primary-family container var for fallback all-day solid | `5620261` | index.css |
| Integration fix | Set hasRrule on EventDetailPopover test fixtures | `69e5ae8` | EventDetailPopover.test.tsx |

## What Was Built

### Task 1: End-Tracking + Recurrence-Bound Control (D-04, D-06, D-07, D-05 verify)

`EventForm.tsx` updated:

- Start date `onChange`: calls `computeNewAllDayEnd` (all-day mode) or `computeNewTimedEnd` (timed mode) to recompute end before setting start — end never strands behind start.
- Start time `onChange`: calls `computeNewTimedEnd` to recompute end preserving the delta.
- `recurrenceBound: 'never' | 'until' | 'count'`, `recurrenceUntil: string`, `recurrenceCount: number` state added alongside the existing state block; reset `useEffect` extended to reset them.
- "Ends" control rendered below the recurrence `<select>`, shown only when `recurrence !== 'none'`, with the exact UI-SPEC Surface 5 labels: Never / On date / After N times, 44px touch targets, inline `--color-destructive` validation messages ("Must be at least 1 occurrence" / "End date must be after the event starts").
- Submit payload extended to conditionally spread `recurrenceUntil` (bound=`'until'`) or `recurrenceCount` (bound=`'count'`) — neither sent when `recurrence === 'none'` or bound=`'never'`.
- `exclusiveEndToInclusiveDate` pre-fill at the all-day edit reset line left intact; a round-trip test confirms no day-span drift (D-05 holds).

### Task 2: SeriesEditPrompt (D-08, D-09)

Created `apps/pwa/src/components/SeriesEditPrompt.tsx` mirroring `DeleteConfirmationDialog`:

- Responsive: bottom sheet on ≤767px, centered dialog (max-width 480px) on ≥768px.
- Focus trap, Escape = Cancel.
- `role="dialog"`, `aria-modal="true"`, `aria-labelledby` → heading.
- Copy: heading "Edit recurring series", body "This will update all occurrences of this event.", primary accent-filled "Update series", ghost "Cancel" (no destructive color).

`EventForm.tsx` gating:

- Edit mode AND `occurrence?.hasRrule === true` → Save opens `SeriesEditPrompt` instead of submitting directly.
- "Update series" confirm → runs the existing `PATCH /api/events/:uid/edit` (whole-series PUT, no RECURRENCE-ID).
- Cancel → returns to form without submitting.
- Non-recurring / create-mode Save behavior unchanged.

### Task 3: All-Day Visual Distinction (999.6, D-12)

Added an all-day override block to the Schedule-X section of `apps/pwa/src/styles/index.css`.

The plan assumed selector `.sx__all-day-event` but Schedule-X v4.6.0 does not emit that class. Discovered during Task 3 and fixed in the follow-up commits (see Deviations). Final selectors targeting real Schedule-X classes:

- `.sx__date-grid-event` — week/day view all-day row events.
- `.sx__month-grid-event:not(:has(.sx__month-grid-event-time))` — month view all-day chips (no time sub-element).

Both render as: full-width rounded pill (`border-radius: 4px`), white (`#FFFFFF`) label, `font-weight: 600`, solid background from the existing `calendarId` color config. Timed chips are unaffected.

### Integration Fix (commit `69e5ae8`)

`EventDetailPopover.test.tsx` fixtures updated to include the new required `hasRrule: boolean` field added to `CalendarOccurrence` in Plan 06-05 (cross-plan type change). Test suite was failing due to this new required field; fix was mechanical.

## Checkpoint Verification (Task 4)

Verified via playwright-cli against desktop Chromium with `DEV_AUTH_BYPASS=true`:

1. **End-tracking (D-04):** Moving a timed start → end follows, 1h duration preserved, end never before start. All-day: day-span preserved. PASS.
2. **Recurrence bound (D-06):** Weekly recurrence → "Ends" control appears. "After N times" N=3 → server expansion returned exactly 3 occurrences (`COUNT=3`). "On date" → `UNTIL` bounded, occurrences stop. "Never" → unbounded. PASS.
3. **FREQ persistence (D-07):** Created a Daily recurrence → occurrences render daily (not weekly). PASS.
4. **Series edit (D-08/D-09):** Edited a recurring occurrence → Save opened focus-trapped "Edit recurring series" prompt. Escape cancelled (form unchanged). "Update series" → `PATCH /api/events/:uid/edit` fired; change reflected across occurrences. PASS.
5. **All-day distinction (999.6):** All-day events rendered as solid filled white-text pills, visually distinct from timed chips at a glance. PASS (after follow-up CSS fix).
6. **All-day no-drift (D-05):** Edited an existing all-day event, saved twice → event did not grow by a day. PASS.

Note: live event-create-via-the-form could not be exercised because the dev-bypass user (id 1) has no CalDAV credential/calendars (those belong to user 2). Server logic and form UI verified via route-mocks and direct DB occurrence inserts. Dev-seed gap — not a code defect.

## Deviations from Plan

### Auto-fixed Issues

**1. [Rule 1 - Bug] Schedule-X all-day CSS targeted a non-existent class**
- **Found during:** Task 4 (playwright-cli checkpoint — behavior 5 initially FAILED)
- **Issue:** The plan and UI-SPEC called for selector `.sx__all-day-event`, but Schedule-X v4.6.0 does not emit that class on the DOM. The all-day pill styling had no effect.
- **Fix:** Replaced with the real Schedule-X v4.6.0 selectors (`.sx__date-grid-event` for week/day; `.sx__month-grid-event:not(:has(.sx__month-grid-event-time))` for month view). Also remapped `--sx-color-primary-container` CSS variable as a fallback fill so the member color propagates even when the Schedule-X theming layer sets its own container variable.
- **Files modified:** `apps/pwa/src/styles/index.css`
- **Commits:** `6dbb166`, `5620261`

**2. [Rule 3 - Blocking] EventDetailPopover.test.tsx fixtures missing required `hasRrule` field**
- **Found during:** Task 3 (test suite run)
- **Issue:** Plan 06-05 added `hasRrule: boolean` as a required field on `CalendarOccurrence`; the existing `EventDetailPopover.test.tsx` fixtures omitted it, failing TypeScript compilation.
- **Fix:** Added `hasRrule: false` (and `true` where the test exercises `hasRrule`-gated behavior) to all fixture objects.
- **Files modified:** `apps/pwa/src/components/EventDetailPopover.test.tsx`
- **Commit:** `69e5ae8`

## Known Stubs

None — all six checkpoint behaviors verified live.

## Threat Flags

None beyond the plan's STRIDE register. T-06-06-input (form-side validation + server-side Zod enforcement — defense in depth), T-06-06-series (reuses existing edit route ownership/objectUrl/etag lookup), T-06-06-xss (plain-text JSX children, no `dangerouslySetInnerHTML`). No new surfaces introduced.

## Self-Check: PASSED

- `apps/pwa/src/components/SeriesEditPrompt.tsx` — FOUND (created)
- `apps/pwa/src/components/EventForm.tsx` — FOUND (`computeNewTimedEnd`, `computeNewAllDayEnd`, `recurrenceUntil`, `recurrenceCount` present)
- `apps/pwa/src/styles/index.css` — FOUND (`sx__date-grid-event` all-day override block present)
- Commit `9aa15c4` — FOUND (test(06-06): RED)
- Commit `cbf5f98` — FOUND (feat(06-06): end-tracking + recurrence-bound)
- Commit `96ef0b4` — FOUND (feat(06-06): series-edit prompt)
- Commit `883b00b` — FOUND (feat(06-06): all-day pill styling)
- Commit `6dbb166` — FOUND (fix(06-06): real Schedule-X selectors)
- Commit `5620261` — FOUND (fix(06-06): primary-container var remap)
- Commit `69e5ae8` — FOUND (fix(06): hasRrule on EventDetailPopover fixtures)
