Files
familysync/.planning/milestones/v1.0-phases/06-ux-polish/06-06-PLAN.md
T
Lucas Berger 982438dc10 style(13-03): apply Prettier formatting across repo
Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
2026-06-11 20:35:18 -04:00

22 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
06-ux-polish 06 execute 2
06-01
06-02
06-03
06-05
apps/pwa/src/components/EventForm.tsx
apps/pwa/src/components/SeriesEditPrompt.tsx
apps/pwa/src/styles/index.css
false
truths artifacts key_links
Scope fence (D-01/D-02): this phase delivers only the six promoted polish items (999.2/3/6/7/8/9); 999.4 reminders/VALARM, 999.5 provider setup, and 999.1 provider abstraction are NOT built here (deferred to milestone 1.1)
Moving an event's start moves its end with it, preserving duration; the end never strands behind the start (D-03/D-04, success criterion 2)
A recurring event can be bounded in the form via 'Ends: Never / On date / After N times' (D-06, success criterion 2)
Editing a recurring occurrence prompts 'Edit recurring series' before saving the whole-series change (D-08/D-09, success criterion 3)
The all-day-edit off-by-one stays fixed — re-editing an all-day event does not grow it by a day (D-05 verify, success criterion 2)
All-day events are visually distinct from timed events at a glance (999.6/D-12, success criterion 1)
path provides contains
apps/pwa/src/components/EventForm.tsx start onChange handlers that call computeNewTimedEnd/computeNewAllDayEnd; recurrence-bound control; hasRrule-gated series-edit confirmation computeNewTimedEnd
path provides contains
apps/pwa/src/components/SeriesEditPrompt.tsx whole-series edit confirmation sheet/dialog (focus trap, Escape=cancel) Update series
path provides contains
apps/pwa/src/styles/index.css Schedule-X all-day chip override (full-width filled pill) sx__all-day-event
from to via pattern
apps/pwa/src/components/EventForm.tsx apps/pwa/src/lib/eventDateTime.ts start onChange → computeNewTimedEnd / computeNewAllDayEnd computeNewTimedEnd|computeNewAllDayEnd
from to via pattern
apps/pwa/src/components/EventForm.tsx apps/pwa/src/api/client.ts payload carries recurrenceUntil/recurrenceCount; occurrence.hasRrule gates the prompt recurrenceUntil|recurrenceCount|hasRrule
Wire the prepared logic into the event form so the user-facing 999.7/999.8/999.9 fixes are live, and give all-day events their distinct look (999.6). After this plan a real user can: move a start and watch the end follow (D-04), bound a recurring series with "Ends: On date / After N times" (D-06), edit a recurring series behind a confirming prompt (D-08/D-09), and tell all-day from timed events at a glance (999.6) — with the all-day-edit off-by-one staying fixed (D-05).

This is the Wave-2 integration slice. It consumes (does NOT redefine) artifacts from earlier plans: computeNewTimedEnd/computeNewAllDayEnd (Plan 01), the recurrenceUntil/recurrenceCount payload fields + hasRrule on CalendarOccurrence (Plan 05 client types, backed by Plan 02 API contract + Plan 03 server expansion).

Purpose: This is glue + UI (form wiring, a confirmation component, a CSS override) — standard tasks, verified with playwright-cli per CLAUDE.md. The deterministic math/serialization it depends on is already unit-tested in Plans 0103. Output: EventForm end-tracking handlers + recurrence-bound control + series-edit prompt trigger; SeriesEditPrompt.tsx; all-day Schedule-X override in index.css.

<execution_context> @$HOME/.claude/gsd-core/workflows/execute-plan.md @$HOME/.claude/gsd-core/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/06-ux-polish/06-RESEARCH.md @.planning/phases/06-ux-polish/06-PATTERNS.md @.planning/phases/06-ux-polish/06-UI-SPEC.md @apps/pwa/src/components/DeleteConfirmationDialog.tsx @.planning/phases/06-ux-polish/06-01-SUMMARY.md @.planning/phases/06-ux-polish/06-05-SUMMARY.md

<artifacts_this_plan_produces> NEW symbols introduced here (exclude from drift/convergence checks):

  • Start onChange handlers in EventForm.tsx that call computeNewTimedEnd/computeNewAllDayEnd
  • recurrenceBound: 'never'|'until'|'count', recurrenceUntil: string, recurrenceCount: number state + the "Ends" control in EventForm.tsx
  • SeriesEditPrompt component (apps/pwa/src/components/SeriesEditPrompt.tsx)
  • .sx__all-day-event CSS override block in apps/pwa/src/styles/index.css NOTE: computeNewTimedEnd, computeNewAllDayEnd, recurrenceUntil/recurrenceCount payload fields, and hasRrule are NOT new here — they are consumed from Plans 01/05. </artifacts_this_plan_produces>
Task 1: End-tracking wiring + recurrence-bound control in EventForm (D-04, D-06, D-07, D-05 verify) apps/pwa/src/components/EventForm.tsx, apps/pwa/src/components/EventForm.test.tsx - apps/pwa/src/components/EventForm.tsx — start date input `onChange` (~line 671) and start time input `onChange` (~line 684); state block (199210); the reset `useEffect` (~232262); the submit handler payload construction (~355370, the `...(isEdit ? {} : { recurrence })` pattern); the recurrence `` (~768772) - apps/pwa/src/lib/eventDateTime.ts — `computeNewTimedEnd` / `computeNewAllDayEnd` signatures (from Plan 01) and the `exclusiveEndToInclusiveDate` D-05 helper at line ~199 (verify it still pre-fills inclusive on all-day edit) - apps/pwa/src/api/client.ts — `CreateEventPayload.recurrenceUntil`/`recurrenceCount` (from Plan 05) — the payload fields to send - .planning/phases/06-ux-polish/06-PATTERNS.md §"apps/pwa/src/components/EventForm.tsx" — exact onChange replacement, new state additions, reset-effect extension, payload extension - .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 4" (end-tracking behavior) + §"Surface 5" (bound control: label "Ends", options Never/On date/After N times, 44px targets, inline validation copy) + §"Copywriting Contract" (exact labels/errors) - Changing startDate (timed) updates endDate/endTime so the duration is preserved (delegates to computeNewTimedEnd); never lands end before start. - Changing startDate (all-day) updates endDate preserving the day-span (computeNewAllDayEnd). - Changing startTime (timed) recomputes end preserving the delta. - Selecting recurrence ≠ "None" reveals the "Ends" control; "On date" reveals a date input, "After N times" reveals a number input (min 1); "Never" sends neither bound field. - Submitting with bound="until" sends `recurrenceUntil`; bound="count" sends `recurrenceCount`; neither when recurrence==='none' or bound==='never'. - Inline validation: count < 1 → "Must be at least 1 occurrence"; until before start → "End date must be after the event starts". - D-05 regression: opening an existing all-day event pre-fills the inclusive end (no +1 drift); saving twice does not grow the event. Replace the bare start date/time `onChange` handlers with handlers that call `computeNewAllDayEnd` (all-day) or `computeNewTimedEnd` (timed) to recompute end, then set start — per PATTERNS §EventForm. Add `recurrenceBound`/`recurrenceUntil`/`recurrenceCount` state alongside the existing state block; extend the reset `useEffect` to reset them to defaults (mirror the `setRecurrence(... ?? 'none')` line). Render the "Ends" control below the recurrence ``, shown only when `recurrence !== 'none'`, using the exact UI-SPEC Surface 5 labels/options and 44px touch targets, with inline `--color-destructive` validation messages. Extend the submit payload to conditionally include `recurrenceUntil` (bound==='until') or `recurrenceCount` (bound==='count') only when `recurrence !== 'none'` — mirror the existing spread-conditional pattern. Do NOT re-implement the D-05 `exclusiveEndToInclusiveDate` pre-fill — leave line ~199 intact and add/keep a test asserting the all-day edit round-trip does not drift. For D-07, confirm the recurrence `` value flows 1:1 to `recurrence` in the payload (the API mapping is locked in Plan 02). Add/extend EventForm.test.tsx cases for end-tracking wiring and bound-field emission where feasible in jsdom. Commit `feat(06-06): wire end-tracking and recurrence-bound control into EventForm`. cd apps/pwa && pnpm test -- run components/EventForm - Start `onChange` handlers call `computeNewTimedEnd`/`computeNewAllDayEnd` (grep present in EventForm.tsx). - The "Ends" control renders only when recurrence ≠ none with the exact UI-SPEC labels and emits `recurrenceUntil`/`recurrenceCount` correctly (and neither when "Never"). - Inline validation messages use the exact UI-SPEC copy. - `exclusiveEndToInclusiveDate` pre-fill at ~line 199 is unchanged; an all-day edit round-trip test shows no day drift (D-05 holds). - EventForm test suite green. End auto-tracks start with a floor; recurrence is boundable in the form; all-day edit stays drift-free; D-07 mapping confirmed 1:1. Task 2: Series-edit confirmation prompt, gated on hasRrule (D-08, D-09) apps/pwa/src/components/SeriesEditPrompt.tsx, apps/pwa/src/components/EventForm.tsx - apps/pwa/src/components/DeleteConfirmationDialog.tsx — the existing bottom-sheet(phone)/dialog(desktop) pattern, focus trap, Escape-to-cancel, role="dialog"/aria-modal — the exact analog to mirror - apps/pwa/src/components/EventForm.tsx — the submit handler (where Save fires) + how `occurrence` is available; the edit-mode branch (`isEdit`) - apps/pwa/src/api/client.ts — `CalendarOccurrence.hasRrule` (from Plan 05) — the gate signal - .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 6" (layout, ≤767px sheet / ≥768px dialog max-width 480px, focus trap, Escape=Cancel) + §"Copywriting Contract" (heading "Edit recurring series", body "This will update all occurrences of this event.", confirm "Update series" accent-filled, "Cancel" ghost) + §"EventForm primary CTAs" (Edit recurring occurrence CTA = "Update series") - .planning/phases/06-ux-polish/06-RESEARCH.md §"Focus 3 — Confirmation prompt (D-09)" — render when editMode && occurrence.hasRrule && Save tapped; PUT replaces master VEVENT (no RECURRENCE-ID) Create `SeriesEditPrompt.tsx` mirroring `DeleteConfirmationDialog`'s responsive sheet/dialog, focus trap, and Escape-to-cancel, with `role="dialog"`, `aria-modal="true"`, `aria-labelledby` → the heading. Use the exact UI-SPEC Surface 6 copy: heading "Edit recurring series", body "This will update all occurrences of this event.", primary accent-filled "Update series", ghost "Cancel" (NO destructive color — this is an edit). In `EventForm.tsx`, when in edit mode AND `occurrence?.hasRrule === true`, tapping Save opens `SeriesEditPrompt` instead of submitting directly; confirming "Update series" runs the existing edit submit (the same PATCH `/api/events/:uid/edit` that PUTs the master VEVENT wholesale — no RECURRENCE-ID, per D-08); Cancel returns to the form without submitting. For non-recurring or create mode, Save submits directly as today. Set the edit-recurring CTA label to "Update series" per UI-SPEC. Commit `feat(06-06): add whole-series edit confirmation prompt`. cd apps/pwa && pnpm test -- run 2>&1 | tail -3 - `SeriesEditPrompt` renders the exact UI-SPEC copy with focus trap + Escape=cancel + role="dialog"/aria-modal. - In edit mode with `occurrence.hasRrule === true`, Save opens the prompt; confirming runs the existing whole-series PATCH; canceling does not submit. - Non-recurring / create-mode Save behavior is unchanged (no prompt). - PWA suite green. Recurring-occurrence edits are confirmed via a whole-series prompt; master-VEVENT PUT path unchanged; no per-occurrence edit introduced. Task 3: All-day visual distinction — Schedule-X override (999.6, D-12) apps/pwa/src/styles/index.css - apps/pwa/src/styles/index.css — the existing `.sx__*` override section (the documented Schedule-X selector overrides) to extend with an all-day rule - apps/pwa/src/lib/hydrateEvents.ts + apps/pwa/src/lib/calendarConfig.ts — how `_familySync.color` / `calendarId` color propagates to Schedule-X chips (so the member color already drives the fill) - .planning/phases/06-ux-polish/06-UI-SPEC.md §"Surface 3" — treatment contract: all-day = full-width filled rounded pill (border-radius 4px), white label, font-weight 600, `--text-label-size`; override selector `.sx__all-day-event`; timed events keep their existing partial-fill chip; color comes from the existing calendarId color config (no per-event inline override) Add a `.sx__all-day-event` override to the Schedule-X section of `index.css` per UI-SPEC Surface 3: render all-day chips as a full-width rounded pill (`border-radius: 4px`), white (`#FFFFFF`) label text, `font-weight: 600`, `font-size: var(--text-label-size)`, with the member color as solid background fill sourced from the existing `calendarId` color config (do NOT add per-event inline styles — the color already propagates via `buildCalendarConfig`). Leave timed-event chip styling untouched so the contract holds: all-day = solid filled pill, timed = partial-fill chip with colored border accent. Keep all existing `.sx__*` rules intact. Commit `feat(06-06): distinct all-day event pill styling`. grep -v '^#' apps/pwa/src/styles/index.css | grep -c 'sx__all-day-event' | grep -qx 1 && cd apps/pwa && pnpm test -- run 2>&1 | tail -3 - `.sx__all-day-event` override present exactly once with full-width pill + white bold label per UI-SPEC. - Existing `.sx__*` layout rules unchanged. - PWA suite green (no test regression from the CSS addition). All-day chips render as distinct filled pills; timed chips unchanged. Task 4: playwright-cli — end-tracking, recurrence bound, series-edit prompt, all-day distinction (verification only — no files modified) Verification task (no code changes). Using the playwright-cli skill against desktop Chromium (dev stack host-side per docs/deployment.md, DEV_AUTH_BYPASS=true), exercise the six behaviors: (1) end-tracking — move a timed event's start and confirm the end follows preserving 1h and never lands before start; repeat all-day (day-span preserved); (2) recurrence bound — set Weekly, confirm the "Ends" control appears, pick "On date" and save a bounded series (occurrences stop), then "After N times" N=3 → 3 occurrences, and "Never" stays unbounded; (3) FREQ persistence — create a Daily recurrence and confirm occurrences render daily not weekly; (4) series edit — edit an existing recurring occurrence, tap Save, confirm the focus-trapped "Edit recurring series" prompt (Escape cancels) and that "Update series" applies across occurrences; (5) all-day distinction — confirm all-day events render as full-width filled pills visually distinct from timed chips; (6) all-day no-drift — edit an existing all-day event and save twice, confirming it does not grow by a day. This is a blocking human-verify checkpoint — pause for operator confirmation. - .claude/skills/playwright-cli/SKILL.md — drive desktop Chromium, interact with the event form and calendar - docs/deployment.md §"Running locally (host-side, no Docker)" — dev run command (DEV_AUTH_BYPASS=true) - .planning/phases/06-ux-polish/06-UI-SPEC.md — Surfaces 3/4/5/6 acceptance behavior EventForm end-tracking, the "Ends" recurrence-bound control, the whole-series edit confirmation prompt, and the all-day filled-pill visual treatment. 1. End-tracking (D-04): open New Event, set a 1h timed event, then move the start date/time forward — confirm the end follows, preserving 1h, and never lands before the start. Repeat for an all-day event (day-span preserved). 2. Recurrence bound (D-06): set recurrence to Weekly, confirm the "Ends" control appears; pick "On date" and a date, save, and confirm the created series is bounded (occurrences stop at/after the date, not an endless/2-month bar). Try "After N times" with N=3 and confirm 3 occurrences. Confirm "Never" is unbounded as before. 3. FREQ persistence (D-07): create a Daily recurrence and confirm occurrences render daily (not weekly). 4. Series edit (D-08/D-09): open an existing recurring occurrence, edit the title/time, tap Save — confirm the "Edit recurring series" prompt appears (focus-trapped, Escape cancels), confirm "Update series" applies the change across occurrences. 5. All-day distinction (999.6): confirm all-day events render as full-width filled pills visually distinct from timed chips at a glance. 6. All-day edit no-drift (D-05): edit an existing all-day event and save twice — confirm it does not grow by a day. End follows start with a floor; recurrence is boundable and FREQ persists; series-edit prompt gates whole-series edits; all-day pills are visually distinct; all-day edits do not drift. Type "approved" or describe which behavior failed. - All six behaviors above observed correctly in desktop Chromium. The full event-form polish set verified live via playwright-cli.

<threat_model>

Trust Boundaries

Boundary Description
client → API (PATCH /api/events/:uid/edit) Whole-series edit PUTs the master VEVENT back to Fastmail; the new recurrenceUntil/recurrenceCount cross here (validated server-side in Plan 02).
user input → form state Recurrence bound date/count are user inputs shaped in the form before submit.

STRIDE Threat Register

Threat ID Category Component Disposition Mitigation Plan
T-06-06-input Tampering recurrence bound inputs (EventForm) mitigate Form-side validation (count ≥ 1, until ≥ start) plus the authoritative server-side Zod validation from Plan 02 (recurrenceUntil max-10, recurrenceCount int≥1) — the client check is UX, the server check is the enforcement boundary. Defense in depth; no raw passthrough. (ASVS V5.)
T-06-06-series Tampering whole-series edit PUT (EventForm → existing /edit route) mitigate Reuses the existing edit route's ownership + objectUrl/etag lookup (unchanged from Phase 3) — the prompt only gates the UX; it adds no new privilege. The PUT replaces the master VEVENT for the caller's own event only; access scope is the existing per-user filter.
T-06-06-xss Tampering / XSS all-day pill label, prompt copy accept All-day labels and prompt text render as plain-text JSX children (existing EventForm XSS posture, T-03-15) — no dangerouslySetInnerHTML; the CSS override sets presentation only. No new injection surface.
T-06-06-SC Tampering npm installs accept No package installs (zero new deps).

</threat_model>

- `cd apps/pwa && pnpm test -- run components/EventForm` green; full `pnpm --filter @familysync/pwa test` green. - `grep -n "computeNewTimedEnd\|computeNewAllDayEnd" apps/pwa/src/components/EventForm.tsx` present; `grep -n "recurrenceUntil\|recurrenceCount" apps/pwa/src/components/EventForm.tsx` present. - `grep -c 'sx__all-day-event' apps/pwa/src/styles/index.css` returns 1. - playwright-cli verifies all six event-form behaviors.

<success_criteria>

  • D-03/D-04 (criterion 2): end follows start with a floor.
  • D-06 (criterion 2): recurrence is boundable; D-07: FREQ persists.
  • D-08/D-09 (criterion 3): whole-series edit behind a confirmation prompt.
  • D-05 (criterion 2): all-day edit off-by-one stays fixed.
  • 999.6/D-12 (criterion 1): all-day events visually distinct. </success_criteria>
Create `.planning/phases/06-ux-polish/06-06-SUMMARY.md` when done (note playwright-cli observations for each behavior).