---
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
