docs(02): create phase plan

This commit is contained in:
Lucas Berger
2026-06-04 15:09:55 -04:00
parent b842955612
commit fc4cc2ccf8
7 changed files with 134 additions and 76 deletions
@@ -24,11 +24,12 @@ user_setup: []
must_haves:
truths:
- "A CSS custom-property token layer (the clean theme) defines all colors, spacing, typography, breakpoints from UI-SPEC; no hard-coded hex/px will be needed by components"
- "A CSS custom-property token layer (the clean theme) defines all colors, spacing, typography, breakpoints from UI-SPEC; no hard-coded hex/px will be needed by components — tuned to stay legible/informational at tablet distance, not ultra-minimal (D-03)"
- "Schedule-X --sx-color-* vars are mapped to project tokens so no Schedule-X default colors bleed through"
- "colorUtils derives Schedule-X lightColors (main/container/onContainer) from a member hex"
- "calendarConfig translates WEEK_START_DAY=0 (Sunday/JS) to Schedule-X firstDayOfWeek=7 (Temporal Sunday) and builds the per-calendar config keyed by String(userId) + 'shared'"
- "hydrateEvents converts all-day occurrences to Temporal.PlainDate and timed occurrences to Temporal.ZonedDateTime"
- "hydrateEvents routes each event's Schedule-X calendarId to 'shared' (isShared) or String(ownerUserId), matching the userId-keyed calendars config — never String(calendarId)"
- "calendarStore (Zustand) holds selectedView (persisted per breakpoint group), selectedDate, openEventId, calendarRange — no server data"
- "fetchEvents(start,end) calls the windowed /api/events with credentials:include and returns OccurrencesResponse"
artifacts:
@@ -39,7 +40,7 @@ must_haves:
provides: "WEEK_START_DAY, firstDayOfWeek translation, buildCalendarConfig()"
exports: ["WEEK_START_DAY", "buildCalendarConfig"]
- path: "apps/pwa/src/lib/hydrateEvents.ts"
provides: "hydrateEvents() ISO→Temporal with all-day PlainDate guard"
provides: "hydrateEvents() ISO→Temporal with all-day PlainDate guard + isShared/ownerUserId calendarId routing"
exports: ["hydrateEvents"]
- path: "apps/pwa/src/store/calendarStore.ts"
provides: "Zustand UI-state store with localStorage view persistence"
@@ -53,6 +54,10 @@ must_haves:
to: "apps/pwa/src/lib/colorUtils.ts"
via: "deriveScheduleXColors() for lightColors"
pattern: "deriveScheduleXColors"
- from: "apps/pwa/src/lib/hydrateEvents.ts"
to: "apps/pwa/src/lib/calendarConfig.ts"
via: "calendarId = isShared ? 'shared' : String(ownerUserId) matches buildCalendarConfig keys"
pattern: "ownerUserId"
---
<objective>
@@ -137,7 +142,7 @@ windowed fetchEvents, updated main.tsx, Schedule-X deps installed.
<action>
Create `apps/pwa/src/lib/colorUtils.ts` exporting `hexToContainer(hex)` (main at 15% opacity blended over #FFFFFF → returns a hex/rgb string), `hexToOnContainer(hex)` (main darkened 40%), and `deriveScheduleXColors(main)` returning `{ main, container, onContainer }`. Implement the math inline (no third-party color lib per RESEARCH Don't-Hand-Roll note — it's simple enough). Write colorUtils.test.ts asserting the derivations for a known hex.
Create `apps/pwa/src/lib/calendarConfig.ts` exporting `export const WEEK_START_DAY = 0` with the inline comment that Schedule-X uses 7=Sunday, a translation `const sxFirstDay = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY` exposed as an exported `SX_FIRST_DAY_OF_WEEK`, the view factory list (`createViewDay`, `createViewWeek`, `createViewMonthGrid`, `createViewMonthAgenda` from @schedule-x/calendar), and `buildCalendarConfig(members: MemberCalendarConfig[])` returning `{ calendars }` keyed by String(userId) plus a reserved `'shared'` entry using deriveScheduleXColors('#F25C7A'). Per-member entries use deriveScheduleXColors(member.color). Limit usage to the confirmed Schedule-X API surface (Pitfall 6). Turn the Plan 01 RED calendarConfig.test.ts green (it asserts the 0→7 translation).
Create `apps/pwa/src/lib/calendarConfig.ts` exporting `export const WEEK_START_DAY = 0` with the inline comment that Schedule-X uses 7=Sunday, a translation `const sxFirstDay = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY` exposed as an exported `SX_FIRST_DAY_OF_WEEK`, the view factory list (`createViewDay`, `createViewWeek`, `createViewMonthGrid`, `createViewMonthAgenda` from @schedule-x/calendar), and `buildCalendarConfig(members: MemberCalendarConfig[])` returning `{ calendars }` keyed by String(userId) plus a reserved `'shared'` entry using deriveScheduleXColors('#F25C7A'). Per-member entries use deriveScheduleXColors(member.color). The `String(userId)` + `'shared'` key scheme here is the routing contract hydrateEvents (Task 3) must match — keep them aligned. Limit usage to the confirmed Schedule-X API surface (Pitfall 6). Turn the Plan 01 RED calendarConfig.test.ts green (it asserts the 0→7 translation).
</action>
<verify>
<automated>cd apps/pwa && pnpm test -- src/lib/colorUtils.test.ts src/lib/calendarConfig.test.ts</automated>
@@ -153,12 +158,13 @@ windowed fetchEvents, updated main.tsx, Schedule-X deps installed.
</task>
<task type="auto" tdd="true">
<name>Task 3: hydrateEvents (Temporal, all-day guard) + Zustand store + windowed fetchEvents</name>
<name>Task 3: hydrateEvents (Temporal, all-day guard, ownership-routed calendarId) + Zustand store + windowed fetchEvents</name>
<files>apps/pwa/src/lib/hydrateEvents.ts, apps/pwa/src/lib/hydrateEvents.test.ts, apps/pwa/src/store/calendarStore.ts, apps/pwa/src/api/client.ts</files>
<read_first>
- apps/pwa/src/lib/hydrateEvents.test.ts (RED stub from Plan 01 — PlainDate vs ZonedDateTime contract)
- apps/pwa/src/lib/hydrateEvents.test.ts (RED stub from Plan 01 — PlainDate-vs-ZonedDateTime AND the calendarId='shared'/String(ownerUserId) routing contract)
- apps/pwa/src/api/client.ts (existing fetchMe pattern + the OLD unwindowed fetchEvents/EventsResponse to replace)
- apps/api/src/broker/expand.ts CalendarOccurrence shape (if Plan 02 merged first) OR .planning/phases/02-calendar-display/02-RESEARCH.md §"Pattern 1" interface (the server JSON contract)
- apps/pwa/src/lib/calendarConfig.ts (Task 2 — buildCalendarConfig keys: String(userId) + 'shared'; hydrateEvents must produce calendarId values that match these keys)
- apps/api/src/broker/expand.ts CalendarOccurrence shape (if Plan 02 merged first: fields incl. calendarId, ownerUserId, isShared) OR .planning/phases/02-calendar-display/02-RESEARCH.md §"Pattern 1" interface (the server JSON contract)
- .planning/phases/02-calendar-display/02-RESEARCH.md §"Pattern 2" (hydrateEvents PlainDate/ZonedDateTime) + §"Pitfall 2/4"
- .planning/phases/02-calendar-display/02-PATTERNS.md §"apps/pwa/src/store/calendarStore.ts" (Zustand state shape) + §"apps/pwa/src/api/client.ts"
- .planning/phases/02-calendar-display/02-UI-SPEC.md §"State Management Contract" + §"View default logic (D-05)"
@@ -166,29 +172,36 @@ windowed fetchEvents, updated main.tsx, Schedule-X deps installed.
<behavior>
- hydrateEvents: occurrence with allDay:true and start '2026-06-15' → start is Temporal.PlainDate (NOT ZonedDateTime — guards Pitfall 2)
- hydrateEvents: timed occurrence → start/end Temporal.ZonedDateTime from the offset-aware ISO string
- hydrateEvents: passes uid/color/isShared through on a _familySync field; calendarId stringified
- hydrateEvents: a shared occurrence (isShared:true) → Schedule-X calendarId === 'shared'
- hydrateEvents: a personal occurrence (isShared:false, ownerUserId:7) → Schedule-X calendarId === '7' (String(ownerUserId)), NOT String(occ.calendarId)
- hydrateEvents: passes uid/color/isShared through on a _familySync field
- calendarStore: setSelectedView persists to localStorage keyed by breakpoint group ('phone' | 'tablet-desktop')
</behavior>
<action>
Create `apps/pwa/src/lib/hydrateEvents.ts` exporting `ScheduleXEvent` interface and `hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent[]`. Branch on `occ.allDay`: if true use `Temporal.PlainDate.from(occ.start)` for start/end (never construct a ZonedDateTime from midnight UTC — Pitfall 2); else `Temporal.ZonedDateTime.from(occ.start/end)`. Stringify calendarId. Carry uid/color/isShared on `_familySync`. Temporal is global via the main.tsx polyfill import; in tests import `'temporal-polyfill/global'` at the top of hydrateEvents.test.ts. Turn the Plan 01 RED hydrateEvents.test.ts green.
Create `apps/pwa/src/lib/hydrateEvents.ts` exporting `ScheduleXEvent` interface and `hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent[]`. Branch on `occ.allDay`: if true use `Temporal.PlainDate.from(occ.start)` for start/end (never construct a ZonedDateTime from midnight UTC — Pitfall 2); else `Temporal.ZonedDateTime.from(occ.start/end)`.
CRITICAL — calendarId routing: set the Schedule-X `calendarId` to `occ.isShared ? 'shared' : String(occ.ownerUserId)`. Do NOT use `String(occ.calendarId)``occ.calendarId` is the DB calendar-row id, but `buildCalendarConfig` keys the calendars config by `String(userId)` plus `'shared'`. A member who owns multiple calendars (e.g. the broker account exposes "Calendar" and "USA Holidays") would otherwise produce a calendarId that matches no config key, and Schedule-X would render those events with no color. Routing by `'shared' | String(ownerUserId)` is the contract that aligns with buildCalendarConfig's keys.
Carry uid/color/isShared on `_familySync`. Temporal is global via the main.tsx polyfill import; in tests import `'temporal-polyfill/global'` at the top of hydrateEvents.test.ts. Turn the Plan 01 RED hydrateEvents.test.ts green — including its shared→'shared' and personal→String(ownerUserId) assertions.
Create `apps/pwa/src/store/calendarStore.ts` exporting `useCalendarStore` (Zustand `create`) with state: `selectedView:string`, `selectedDate:string`, `openEventId:string|null`, `calendarRange:{start:string;end:string}` and setters. selectedView is initialized from localStorage keyed by breakpoint group (`window.matchMedia('(max-width:767px)').matches ? 'phone' : 'tablet-desktop'`), defaulting to 'month-agenda' on phone / 'month-grid' on tablet-desktop (D-05); setSelectedView writes back to localStorage under `calendarView.{group}`. calendarRange defaults to the current month ± 1 week (do NOT depend on Schedule-X onRangeUpdate for the first fetch — A4/Open Q2). Server events NEVER enter this store. Add `zustand` to apps/pwa deps if not already present.
In `apps/pwa/src/api/client.ts`, REPLACE the old unwindowed `fetchEvents()` and its `CalendarEvent`/`EventsResponse` types with: `CalendarOccurrence` interface (mirror the server contract), `OccurrencesResponse { occurrences: CalendarOccurrence[] }`, and `fetchEvents(start:string, end:string): Promise<OccurrencesResponse>` calling `/api/events?start=${start}&end=${end}` with `credentials:'include'` and the same `if(!res.ok) throw` pattern as fetchMe. Note: EventProof.tsx referenced the old fetchEvents — leave EventProof for Plan 05 to remove; if the type change breaks its build, update EventProof minimally to compile (it is replaced in Plan 05).
In `apps/pwa/src/api/client.ts`, REPLACE the old unwindowed `fetchEvents()` and its `CalendarEvent`/`EventsResponse` types with: `CalendarOccurrence` interface (mirror the server contract — include calendarId, ownerUserId, isShared so hydrateEvents can route), `OccurrencesResponse { occurrences: CalendarOccurrence[] }`, and `fetchEvents(start:string, end:string): Promise<OccurrencesResponse>` calling `/api/events?start=${start}&end=${end}` with `credentials:'include'` and the same `if(!res.ok) throw` pattern as fetchMe. Note: EventProof.tsx referenced the old fetchEvents — leave EventProof for Plan 05 to remove; if the type change breaks its build, update EventProof minimally to compile (it is replaced in Plan 05).
</action>
<verify>
<automated>cd apps/pwa && pnpm test -- src/lib/hydrateEvents.test.ts</automated>
<automated>cd apps/pwa && grep -q "Temporal.PlainDate.from" src/lib/hydrateEvents.ts && grep -q "credentials: 'include'" src/api/client.ts && grep -q "calendarRange" src/store/calendarStore.ts && echo SLICE_LIB_OK</automated>
<automated>cd apps/pwa && grep -q "Temporal.PlainDate.from" src/lib/hydrateEvents.ts && grep -q "ownerUserId" src/lib/hydrateEvents.ts && grep -q "credentials: 'include'" src/api/client.ts && grep -q "calendarRange" src/store/calendarStore.ts && echo SLICE_LIB_OK</automated>
<automated>cd apps/pwa && pnpm exec tsc --noEmit</automated>
</verify>
<acceptance_criteria>
- hydrateEvents.test.ts (Plan 01 RED stub) passes: all-day→PlainDate, timed→ZonedDateTime
- hydrateEvents.test.ts (Plan 01 RED stub) passes: all-day→PlainDate, timed→ZonedDateTime, shared→'shared', personal→String(ownerUserId)
- hydrateEvents.ts uses Temporal.PlainDate.from for all-day and never ZonedDateTime for all-day
- hydrateEvents.ts sets calendarId to `occ.isShared ? 'shared' : String(occ.ownerUserId)` (NOT String(occ.calendarId))
- calendarStore exports useCalendarStore with selectedView/selectedDate/openEventId/calendarRange and persists selectedView to localStorage per breakpoint group
- client.ts fetchEvents takes (start,end), hits /api/events?start=&end= with credentials:'include', returns OccurrencesResponse
- client.ts fetchEvents takes (start,end), hits /api/events?start=&end= with credentials:'include', returns OccurrencesResponse; CalendarOccurrence includes ownerUserId + isShared
- tsc --noEmit clean in apps/pwa
</acceptance_criteria>
<done>hydrateEvents (with all-day PlainDate guard), Zustand UI store, and windowed fetchEvents all built; hydrateEvents.test.ts green; PWA typechecks.</done>
<done>hydrateEvents (all-day PlainDate guard + ownership-routed calendarId), Zustand UI store, and windowed fetchEvents all built; hydrateEvents.test.ts green; PWA typechecks.</done>
</task>
</tasks>
@@ -220,6 +233,7 @@ windowed fetchEvents, updated main.tsx, Schedule-X deps installed.
- Clean-theme token layer + Schedule-X overrides present (D-01/D-02)
- firstDayOfWeek 0→7 translation encoded; per-calendar config built from member colors
- All-day Temporal PlainDate guard in place
- hydrateEvents calendarId routes by isShared/ownerUserId to match buildCalendarConfig keys
- Zustand UI store + windowed fetchEvents ready for Plan 04
</success_criteria>
@@ -230,7 +244,7 @@ windowed fetchEvents, updated main.tsx, Schedule-X deps installed.
- token object export (tokens.ts); styles/index.css global reset
- `hexToContainer`, `hexToOnContainer`, `deriveScheduleXColors` (colorUtils.ts)
- `WEEK_START_DAY`, `SX_FIRST_DAY_OF_WEEK`, `buildCalendarConfig`, `MemberCalendarConfig` (calendarConfig.ts)
- `hydrateEvents`, `ScheduleXEvent` (hydrateEvents.ts)
- `hydrateEvents`, `ScheduleXEvent` (hydrateEvents.ts) — calendarId routed by isShared/ownerUserId
- `useCalendarStore` Zustand store + CalendarStore state shape (calendarStore.ts)
- `fetchEvents(start,end)`, `CalendarOccurrence`, `OccurrencesResponse` (client.ts — replaces old unwindowed versions)
- @schedule-x/* + temporal-polyfill + lucide-react + zustand dependencies