--- phase: 02-calendar-display plan: "03" subsystem: pwa-foundation tags: [token-layer, schedule-x, temporal, color-utils, calendar-config, hydrate-events, zustand, fetch-client] dependency_graph: requires: ["02-01"] provides: [css-token-layer, sx-color-overrides, colorUtils, calendarConfig, hydrateEvents, calendarStore, windowed-fetchEvents, temporal-polyfill-global] affects: ["02-04", "02-05"] tech_stack: added: - "@schedule-x/calendar@4.6.0" - "@schedule-x/react@4.1.0" - "@schedule-x/theme-default@4.6.0" - "@schedule-x/event-modal@4.6.0" - "@schedule-x/events-service@4.6.0" - "temporal-polyfill@0.3.2" - "lucide-react@1.17.0" patterns: - CSS custom property token layer (clean theme, D-01/D-02) - Schedule-X --sx-color-* override via cascade (imported after theme-default in main.tsx) - Temporal polyfill-first import order in main.tsx - Alpha-blend-over-white for event chip container colors - WEEK_START_DAY=0 (JS) to SX_FIRST_DAY_OF_WEEK=7 (Temporal) translation - hydrateEvents calendarId routing: isShared ? 'shared' : String(ownerUserId) - Zustand UI store with localStorage view persistence per breakpoint group - Windowed fetchEvents(start,end) with credentials:include key_files: created: - apps/pwa/src/styles/tokens.css - apps/pwa/src/styles/tokens.ts - apps/pwa/src/styles/index.css - apps/pwa/src/lib/colorUtils.ts - apps/pwa/src/lib/colorUtils.test.ts - apps/pwa/src/lib/calendarConfig.ts - apps/pwa/src/lib/hydrateEvents.ts - apps/pwa/src/store/calendarStore.ts modified: - apps/pwa/package.json - apps/pwa/src/main.tsx - apps/pwa/src/api/client.ts - apps/pwa/src/lib/hydrateEvents.test.ts - apps/pwa/src/lib/calendarConfig.test.ts - apps/pwa/src/components/EventProof.tsx - pnpm-lock.yaml decisions: - "calendarId routing uses isShared/ownerUserId (never String(calendarId)) to match buildCalendarConfig keys" - "SX_FIRST_DAY_OF_WEEK = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY encoded as exported constant" - "fetchEventsLegacy() preserves EventProof compilation until Plan 05 removes the component" - "calendarStore initializes calendarRange to today's month +-7-day buffer for initial TanStack Query fetch" - "tokens.css imported via index.css (not directly in main.tsx) to preserve correct cascade order" metrics: duration: "6m 56s" completed: "2026-06-05" tasks_completed: 3 files_created: 8 files_modified: 7 --- # Phase 02 Plan 03: PWA Foundation — Token Layer, Color Utils, Calendar Config, Hydration, Store Summary CSS custom-property token layer with Schedule-X overrides, hex-blend color utilities, firstDayOfWeek 0 to 7 translation, Temporal-based event hydration with all-day PlainDate guard and ownership-routed calendarId, Zustand UI store with localStorage view persistence, and windowed fetchEvents. ## What Was Built ### Task 1: Schedule-X Stack + Token Layer + main.tsx Import Order **Installed packages** in `apps/pwa`: - `@schedule-x/calendar@4.6.0`, `@schedule-x/react@4.1.0`, `@schedule-x/theme-default@4.6.0` - `@schedule-x/event-modal@4.6.0`, `@schedule-x/events-service@4.6.0` - `temporal-polyfill@0.3.2`, `lucide-react@1.17.0` **`apps/pwa/src/styles/tokens.css`** — CSS custom properties declaring: - Surface/border/text palette: `--color-surface`, `--color-surface-dim`, `--color-surface-raised`, `--color-border`, `--color-border-subtle`, `--color-text-primary/secondary/muted`, `--color-focus-ring`, `--color-overlay` - Calendar colors: `--color-member-0..5` + `--color-shared-family: #F25C7A` + `--color-destructive` - Spacing scale: `--space-1..12` (multiples of 4px) - Typography: `--font-family-base`, `--text-body/label/heading/display-size/weight/line-height` - Breakpoints: `--bp-phone: 0px`, `--bp-tablet: 768px`, `--bp-desktop: 1280px` - Schedule-X overrides: all `--sx-color-*` vars mapped to project tokens; `--sx-font-family` - `@keyframes shimmer` for SkeletonCalendar **`apps/pwa/src/styles/tokens.ts`** — TypeScript mirror of all token values for inline-style props; `as const` typed. **`apps/pwa/src/styles/index.css`** — imports tokens.css + minimal global reset. **`apps/pwa/src/main.tsx`** — updated with load-bearing import order: 1. `import 'temporal-polyfill/global'` (must be first) 2. `import '@schedule-x/theme-default/dist/index.css'` (SX layout CSS) 3. `import './styles/index.css'` (token overrides win cascade) ### Task 2: colorUtils + calendarConfig — RED Stubs Turned GREEN **`apps/pwa/src/lib/colorUtils.ts`** exports: - `hexToContainer(hex)` — alpha blends at 15% opacity over white - `hexToOnContainer(hex)` — darkens 40% (channel multiply by 0.6) - `deriveScheduleXColors(main)` returning `{ main, container, onContainer }` **`apps/pwa/src/lib/calendarConfig.ts`** exports: - `WEEK_START_DAY = 0` (JS Sunday convention) - `SX_FIRST_DAY_OF_WEEK = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY` - `buildCalendarConfig(members)` returning `{ firstDayOfWeek: 7, calendars }` with `'shared'` (rose) + per-member entries keyed by `String(userId)` `calendarConfig.test.ts` (Plan 01 RED stubs) — all 4 assertions now GREEN. ### Task 3: hydrateEvents + calendarStore + windowed fetchEvents — RED Stubs Turned GREEN **`apps/pwa/src/lib/hydrateEvents.ts`**: - `allDay:true` uses `Temporal.PlainDate.from(occ.start)` (guards all-day date shift) - `allDay:false` uses `Temporal.ZonedDateTime.from(occ.start/end)` - `calendarId = occ.isShared ? 'shared' : String(occ.ownerUserId)` — never `String(occ.calendarId)` - `_familySync: { uid, color, isShared }` carried for popover rendering `hydrateEvents.test.ts` (Plan 01 RED stubs) — all 4 assertions now GREEN. **`apps/pwa/src/store/calendarStore.ts`** Zustand store: - `selectedView` — from localStorage per breakpoint group; D-05 defaults - `calendarRange` — month ± 7-day buffer for initial TanStack Query key - `openEventId`, `selectedDate` — UI-only, not persisted **`apps/pwa/src/api/client.ts`**: - Added `CalendarOccurrence`, `OccurrencesResponse`, `fetchEvents(start, end)` - Kept deprecated `CalendarEvent`, `EventsResponse`, `fetchEventsLegacy()` for EventProof.tsx (removed Plan 05) ## Verification Results ``` Test Files 3 passed (3) Tests 18 passed (18) tsc --noEmit: clean (0 errors) ``` All Wave 1 RED stubs are GREEN: - `calendarConfig.test.ts` — 4/4 pass - `hydrateEvents.test.ts` — 4/4 pass - `colorUtils.test.ts` — 10/10 pass ## Deviations from Plan ### Auto-fixed Issues **1. [Rule 2 - Missing Critical Functionality] Added temporal-polyfill/global to hydrateEvents.test.ts** - **Found during:** Task 3 test run - **Issue:** Plan 01 RED stub lacked `import 'temporal-polyfill/global'`; jsdom has no native Temporal - **Fix:** Added as first import in `hydrateEvents.test.ts` - **Files modified:** `apps/pwa/src/lib/hydrateEvents.test.ts` - **Commit:** f377d7c **2. [Rule 2 - Missing Critical Functionality] Added fetchEventsLegacy() to preserve EventProof** - **Found during:** Task 3 — updating client.ts - **Issue:** EventProof.tsx called no-arg `fetchEvents()` and used `CalendarEvent` fields not on `CalendarOccurrence` - **Fix:** Added `fetchEventsLegacy()` (deprecated) + updated EventProof to use it; plan says it is replaced in Plan 05 - **Files modified:** `apps/pwa/src/api/client.ts`, `apps/pwa/src/components/EventProof.tsx` - **Commit:** f377d7c ## Known Stubs None — all artifacts are fully wired. Plan 04 mounts Schedule-X and consumes these modules. ## Threat Flags No new threat surface beyond the plan's threat model. All packages pre-approved in RESEARCH.md with no postinstall scripts. ## Self-Check: PASSED Files created: - [x] apps/pwa/src/styles/tokens.css - [x] apps/pwa/src/styles/tokens.ts - [x] apps/pwa/src/styles/index.css - [x] apps/pwa/src/lib/colorUtils.ts - [x] apps/pwa/src/lib/colorUtils.test.ts - [x] apps/pwa/src/lib/calendarConfig.ts - [x] apps/pwa/src/lib/hydrateEvents.ts - [x] apps/pwa/src/store/calendarStore.ts Commits: - [x] 0911a23 — Task 1: Schedule-X stack + token layer + main.tsx - [x] 43554f4 — Task 2: colorUtils + calendarConfig; calendarConfig stubs GREEN - [x] f377d7c — Task 3: hydrateEvents + calendarStore + windowed fetchEvents; all stubs GREEN