231 lines
20 KiB
Markdown
231 lines
20 KiB
Markdown
---
|
||
phase: 06-ux-polish
|
||
plan: 06
|
||
type: execute
|
||
wave: 2
|
||
depends_on: ["06-01", "06-02", "06-03", "06-05"]
|
||
files_modified:
|
||
- apps/pwa/src/components/EventForm.tsx
|
||
- apps/pwa/src/components/SeriesEditPrompt.tsx
|
||
- apps/pwa/src/styles/index.css
|
||
autonomous: false
|
||
requirements: []
|
||
must_haves:
|
||
truths:
|
||
- "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)"
|
||
artifacts:
|
||
- path: "apps/pwa/src/components/EventForm.tsx"
|
||
provides: "start onChange handlers that call computeNewTimedEnd/computeNewAllDayEnd; recurrence-bound control; hasRrule-gated series-edit confirmation"
|
||
contains: "computeNewTimedEnd"
|
||
- path: "apps/pwa/src/components/SeriesEditPrompt.tsx"
|
||
provides: "whole-series edit confirmation sheet/dialog (focus trap, Escape=cancel)"
|
||
contains: "Update series"
|
||
- path: "apps/pwa/src/styles/index.css"
|
||
provides: "Schedule-X all-day chip override (full-width filled pill)"
|
||
contains: "sx__all-day-event"
|
||
key_links:
|
||
- from: "apps/pwa/src/components/EventForm.tsx"
|
||
to: "apps/pwa/src/lib/eventDateTime.ts"
|
||
via: "start onChange → computeNewTimedEnd / computeNewAllDayEnd"
|
||
pattern: "computeNewTimedEnd|computeNewAllDayEnd"
|
||
- from: "apps/pwa/src/components/EventForm.tsx"
|
||
to: "apps/pwa/src/api/client.ts"
|
||
via: "payload carries recurrenceUntil/recurrenceCount; occurrence.hasRrule gates the prompt"
|
||
pattern: "recurrenceUntil|recurrenceCount|hasRrule"
|
||
---
|
||
|
||
<objective>
|
||
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 01–03.
|
||
Output: EventForm end-tracking handlers + recurrence-bound control + series-edit prompt trigger; `SeriesEditPrompt.tsx`; all-day Schedule-X override in `index.css`.
|
||
</objective>
|
||
|
||
<execution_context>
|
||
@$HOME/.claude/gsd-core/workflows/execute-plan.md
|
||
@$HOME/.claude/gsd-core/templates/summary.md
|
||
</execution_context>
|
||
|
||
<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
|
||
</context>
|
||
|
||
<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>
|
||
|
||
<tasks>
|
||
|
||
<task type="auto" tdd="true">
|
||
<name>Task 1: End-tracking wiring + recurrence-bound control in EventForm (D-04, D-06, D-07, D-05 verify)</name>
|
||
<files>apps/pwa/src/components/EventForm.tsx, apps/pwa/src/components/EventForm.test.tsx</files>
|
||
<read_first>
|
||
- apps/pwa/src/components/EventForm.tsx — start date input `onChange` (~line 671) and start time input `onChange` (~line 684); state block (199–210); the reset `useEffect` (~232–262); the submit handler payload construction (~355–370, the `...(isEdit ? {} : { recurrence })` pattern); the recurrence `<select>` (~768–772)
|
||
- 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)
|
||
</read_first>
|
||
<behavior>
|
||
- 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.
|
||
</behavior>
|
||
<action>
|
||
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 `<select>`, 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 `<select>` 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`.
|
||
</action>
|
||
<verify>
|
||
<automated>cd apps/pwa && pnpm test -- run components/EventForm</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- 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.
|
||
</acceptance_criteria>
|
||
<done>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.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 2: Series-edit confirmation prompt, gated on hasRrule (D-08, D-09)</name>
|
||
<files>apps/pwa/src/components/SeriesEditPrompt.tsx, apps/pwa/src/components/EventForm.tsx</files>
|
||
<read_first>
|
||
- 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)
|
||
</read_first>
|
||
<action>
|
||
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`.
|
||
</action>
|
||
<verify>
|
||
<automated>cd apps/pwa && pnpm test -- run 2>&1 | tail -3</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `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.
|
||
</acceptance_criteria>
|
||
<done>Recurring-occurrence edits are confirmed via a whole-series prompt; master-VEVENT PUT path unchanged; no per-occurrence edit introduced.</done>
|
||
</task>
|
||
|
||
<task type="auto">
|
||
<name>Task 3: All-day visual distinction — Schedule-X override (999.6, D-12)</name>
|
||
<files>apps/pwa/src/styles/index.css</files>
|
||
<read_first>
|
||
- 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)
|
||
</read_first>
|
||
<action>
|
||
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`.
|
||
</action>
|
||
<verify>
|
||
<automated>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</automated>
|
||
</verify>
|
||
<acceptance_criteria>
|
||
- `.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).
|
||
</acceptance_criteria>
|
||
<done>All-day chips render as distinct filled pills; timed chips unchanged.</done>
|
||
</task>
|
||
|
||
<task type="checkpoint:human-verify" gate="blocking">
|
||
<name>Task 4: playwright-cli — end-tracking, recurrence bound, series-edit prompt, all-day distinction</name>
|
||
<files>(verification only — no files modified)</files>
|
||
<action>
|
||
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.
|
||
</action>
|
||
<read_first>
|
||
- .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
|
||
</read_first>
|
||
<what-built>
|
||
EventForm end-tracking, the "Ends" recurrence-bound control, the whole-series edit confirmation prompt, and the all-day filled-pill visual treatment.
|
||
</what-built>
|
||
<how-to-verify>
|
||
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.
|
||
</how-to-verify>
|
||
<verify>
|
||
<human-check>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.</human-check>
|
||
</verify>
|
||
<resume-signal>Type "approved" or describe which behavior failed.</resume-signal>
|
||
<acceptance_criteria>
|
||
- All six behaviors above observed correctly in desktop Chromium.
|
||
</acceptance_criteria>
|
||
<done>The full event-form polish set verified live via playwright-cli.</done>
|
||
</task>
|
||
|
||
</tasks>
|
||
|
||
<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>
|
||
|
||
<verification>
|
||
- `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.
|
||
</verification>
|
||
|
||
<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>
|
||
|
||
<output>
|
||
Create `.planning/phases/06-ux-polish/06-06-SUMMARY.md` when done (note playwright-cli observations for each behavior).
|
||
</output>
|