diff --git a/.planning/phases/06-ux-polish/06-RESEARCH.md b/.planning/phases/06-ux-polish/06-RESEARCH.md new file mode 100644 index 0000000..8352b47 --- /dev/null +++ b/.planning/phases/06-ux-polish/06-RESEARCH.md @@ -0,0 +1,770 @@ +# Phase 6: UX Polish — Research + +**Researched:** 2026-06-10 +**Domain:** React PWA / Hono API — form UX, ical.js recurrence, OIDC auth gating, CSS animation +**Confidence:** HIGH on code-verified claims; MEDIUM on library API specifics (Context7) + +--- + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions + +- **D-01:** Phase 6 = the six polish items only (999.2/3/6/7/8/9). Do NOT pull in 999.4 or 999.5. +- **D-02:** 999.4 (reminders/VALARM) and 999.5 (provider setup) move to milestone 1.1. 999.1 also remains backlog/1.1 candidate. +- **D-03:** End-tracking is a general bug — applies to one-time, single-day, and timed events alike. +- **D-04:** Preserve current duration on any start change (timed: delta; all-day: day-span). Floor: end never strands behind start. +- **D-05:** All-day-edit off-by-one already fixed at `EventForm.tsx:199` (`exclusiveEndToInclusiveDate`). Verify it holds; do NOT re-implement. +- **D-06:** Add recurrence bound — "repeat until " (RRULE `UNTIL`) and/or "for N occurrences" (`COUNT`). Each occurrence's duration ties to start→end delta, NOT the recurrence span. +- **D-07:** Verify/fix FREQ-persistence bug — confirm the dropdown correctly writes the selected FREQ. +- **D-08:** Whole-series edit only (title/time/RRULE on master VEVENT). Per-occurrence RECURRENCE-ID stays deferred to v1.x. +- **D-09:** Series-edit confirmation/prompt UX delegated to `/gsd-ui-phase`. +- **D-10:** Gate app render on auth state — no calendar shell/skeleton/"Sign-in required" flash before Authelia. Show "Signing you in…" splash while unauthenticated. +- **D-11:** Centralize 401/opaqueredirect detection from ANY query or mutation. Typed `SessionExpiredError`, single TanStack Query error handler, re-arm `maybeRedirectToLogin()` on session expiry mid-use. +- **D-12:** Visual refresh delegated to `/gsd-ui-phase` invoking the `frontend-design` skill. +- **D-13:** Hoist `@keyframes spin` to global stylesheet so all sync indicators animate. + +### Claude's Discretion / Delegated to UI-Phase + +- All-day visual treatment (999.6): behavior locked; concrete CSS treatment delegated to `/gsd-ui-phase`. +- Series-edit prompt UX (999.9): delegated to `/gsd-ui-phase` (see D-09). +- Splash/interstitial copy: "Signing you in…" / "Your session expired — signing you back in…" are starting points. + +### Deferred Ideas (OUT OF SCOPE) + +- 999.4 — Event reminder/VALARM options → milestone 1.1 +- 999.5 — First-login provider setup → milestone 1.1 +- 999.1 — Calendar provider abstraction → backlog/milestone 1.1 candidate +- Per-occurrence (RECURRENCE-ID) and "this and following" recurring edits → v1.x + + +--- + +## Summary + +This is a polish phase on code that was shipped in Phases 3 and 5. Every item has a diagnosed root cause from Phase 3 Gate 2 live use. The research task is to confirm the exact fix targets in the real code, resolve library API specifics that the planner cannot safely assume, and define the verification architecture. + +Six independent fix areas, each self-contained but sharing the EventForm and client.ts touch points: + +1. **D-04/D-07 — EventForm: end-tracking + FREQ-persistence.** Both are EventForm state bugs. End-tracking is a missing `onChange` handler on `startDate`/`startTime` that recomputes end. FREQ-persistence is a suspected state-initialization issue (see §FREQ-Persistence Diagnosis below — the `RRULE_PRESETS` map and the API route are clean; the bug origin is narrowed to form state reset behavior). +2. **D-06/D-08 — RRULE UNTIL/COUNT + whole-series edit.** Spans PWA → API → expansion. Requires a schema extension on `CalendarOccurrence` to expose `hasRrule` (currently absent from the type on both sides). +3. **D-10/D-11 — Auth gating.** A single `CalendarShell` refactor plus typed `SessionExpiredError` in `client.ts` serves both items. +4. **D-13 — Spinner.** The `@keyframes spin` IS already in `tokens.css` (lines 140–147). The bug is that `PushPermissionPrompt.tsx` contains a redundant local ` +``` +This is redundant (the global definition already covers it) and slightly noisy but not the cause of animation failures. Remove it. + +**Bug 2 — Missing `@keyframes pulse`** [VERIFIED: codebase read]: `LiveSyncIndicator.tsx:69` uses: +```tsx +animation: 'pulse 1.4s ease-in-out infinite' +``` +`@keyframes pulse` does NOT exist in `tokens.css` or `index.css`. The reconnecting dot never animates. Must add to `tokens.css` per UI-SPEC.md: +```css +@keyframes pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.4; } +} +``` + +**Bug 3 — Spinner in `SyncStateToast`** [VERIFIED: codebase read]: `SyncStateToast.tsx:156–161` uses `animation: 'spin 1s linear infinite'`. Since `tokens.css` loads before components, this works. BUT confirm that the `SyncStateToast` Loader2 spinner IS actually spinning — the potential failure mode from the CONTEXT.md claim does not apply here because `tokens.css` is always loaded. The spinner should work. The `@keyframes spin` in `PushPermissionPrompt` is a `