style(13-03): apply Prettier formatting across repo

Mechanical reformat — no logic changes. 398 files changed, 19125
insertions(+), 16457 deletions(-). Prettier 3.8.4 with .prettierrc
(singleQuote:true, semi:true, tabWidth:2, trailingComma:all,
printWidth:100). Isolated per D-13-08 for reviewability.
This commit is contained in:
Lucas Berger
2026-06-11 20:35:18 -04:00
parent 4bc0445173
commit 982438dc10
398 changed files with 19050 additions and 16382 deletions
@@ -8,31 +8,31 @@
## File Classification
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
|-------------------|------|-----------|----------------|---------------|
| `apps/api/src/db/schema.ts` | model | CRUD | self (modify) | exact |
| `apps/api/src/broker/expand.ts` | utility | transform | `apps/api/src/broker/sync.ts` | role-match |
| `apps/api/src/routes/events.ts` | route | request-response | self (modify) + `apps/api/src/routes/me.ts` | exact |
| `apps/api/src/auth/devBypass.ts` | middleware | request-response | `apps/api/src/auth/middleware.ts` | role-match |
| `apps/api/src/index.ts` | config | request-response | self (modify) | exact |
| `apps/api/tests/broker/expand.test.ts` | test | transform | `apps/api/tests/broker/poller.test.ts` | role-match |
| `apps/api/tests/routes/events.test.ts` | test | request-response | `apps/api/tests/health.test.ts` | role-match |
| `apps/pwa/vitest.config.ts` | config | — | `apps/api/vitest.config.ts` | role-match |
| `apps/pwa/src/styles/tokens.css` | utility | — | none | no analog |
| `apps/pwa/src/styles/tokens.ts` | utility | — | none | no analog |
| `apps/pwa/src/styles/index.css` | utility | — | none | no analog |
| `apps/pwa/src/lib/calendarConfig.ts` | utility | transform | `apps/pwa/src/api/client.ts` | partial |
| `apps/pwa/src/lib/hydrateEvents.ts` | utility | transform | `apps/pwa/src/api/client.ts` | partial |
| `apps/pwa/src/lib/colorUtils.ts` | utility | transform | `apps/pwa/src/App.tsx` (ColorSwatch) | partial |
| `apps/pwa/src/store/calendarStore.ts` | store | event-driven | none | no analog |
| `apps/pwa/src/components/CalendarShell.tsx` | component | request-response | `apps/pwa/src/App.tsx` | role-match |
| `apps/pwa/src/components/EventDetailPopover.tsx` | component | request-response | `apps/pwa/src/App.tsx` (MemberBadge) | partial |
| `apps/pwa/src/components/AppNav.tsx` | component | — | `apps/pwa/src/App.tsx` | partial |
| `apps/pwa/src/components/ViewToolbar.tsx` | component | event-driven | `apps/pwa/src/App.tsx` | partial |
| `apps/pwa/src/components/ColorLegend.tsx` | component | — | `apps/pwa/src/App.tsx` (MemberBadge) | partial |
| `apps/pwa/src/components/SkeletonCalendar.tsx` | component | — | `apps/pwa/src/App.tsx` (loading state) | partial |
| `apps/pwa/src/api/client.ts` | utility | request-response | self (modify) | exact |
| `apps/pwa/src/main.tsx` | config | — | self (modify) | exact |
| New/Modified File | Role | Data Flow | Closest Analog | Match Quality |
| ------------------------------------------------ | ---------- | ---------------- | ------------------------------------------- | ------------- |
| `apps/api/src/db/schema.ts` | model | CRUD | self (modify) | exact |
| `apps/api/src/broker/expand.ts` | utility | transform | `apps/api/src/broker/sync.ts` | role-match |
| `apps/api/src/routes/events.ts` | route | request-response | self (modify) + `apps/api/src/routes/me.ts` | exact |
| `apps/api/src/auth/devBypass.ts` | middleware | request-response | `apps/api/src/auth/middleware.ts` | role-match |
| `apps/api/src/index.ts` | config | request-response | self (modify) | exact |
| `apps/api/tests/broker/expand.test.ts` | test | transform | `apps/api/tests/broker/poller.test.ts` | role-match |
| `apps/api/tests/routes/events.test.ts` | test | request-response | `apps/api/tests/health.test.ts` | role-match |
| `apps/pwa/vitest.config.ts` | config | — | `apps/api/vitest.config.ts` | role-match |
| `apps/pwa/src/styles/tokens.css` | utility | — | none | no analog |
| `apps/pwa/src/styles/tokens.ts` | utility | — | none | no analog |
| `apps/pwa/src/styles/index.css` | utility | — | none | no analog |
| `apps/pwa/src/lib/calendarConfig.ts` | utility | transform | `apps/pwa/src/api/client.ts` | partial |
| `apps/pwa/src/lib/hydrateEvents.ts` | utility | transform | `apps/pwa/src/api/client.ts` | partial |
| `apps/pwa/src/lib/colorUtils.ts` | utility | transform | `apps/pwa/src/App.tsx` (ColorSwatch) | partial |
| `apps/pwa/src/store/calendarStore.ts` | store | event-driven | none | no analog |
| `apps/pwa/src/components/CalendarShell.tsx` | component | request-response | `apps/pwa/src/App.tsx` | role-match |
| `apps/pwa/src/components/EventDetailPopover.tsx` | component | request-response | `apps/pwa/src/App.tsx` (MemberBadge) | partial |
| `apps/pwa/src/components/AppNav.tsx` | component | — | `apps/pwa/src/App.tsx` | partial |
| `apps/pwa/src/components/ViewToolbar.tsx` | component | event-driven | `apps/pwa/src/App.tsx` | partial |
| `apps/pwa/src/components/ColorLegend.tsx` | component | — | `apps/pwa/src/App.tsx` (MemberBadge) | partial |
| `apps/pwa/src/components/SkeletonCalendar.tsx` | component | — | `apps/pwa/src/App.tsx` (loading state) | partial |
| `apps/pwa/src/api/client.ts` | utility | request-response | self (modify) | exact |
| `apps/pwa/src/main.tsx` | config | — | self (modify) | exact |
---
@@ -43,6 +43,7 @@
**Analog:** self
**Add to `calendarEvents` table — Drizzle column pattern** (lines 84105 of current file):
```typescript
// New columns to add — follow existing column declaration style exactly:
hasRrule: boolean('has_rrule').default(false).notNull(),
@@ -56,6 +57,7 @@ index('idx_calendar_events_has_rrule').on(t.hasRrule),
```
**Import pattern** (lines 111 of existing schema.ts):
```typescript
import {
mysqlTable,
@@ -67,7 +69,7 @@ import {
boolean,
index,
unique,
} from 'drizzle-orm/mysql-core'
} from 'drizzle-orm/mysql-core';
```
---
@@ -77,55 +79,57 @@ import {
**Analog:** `apps/api/src/broker/sync.ts`
**Imports pattern** (lines 18 of sync.ts):
```typescript
import ICAL from 'ical.js'
import { eq } from 'drizzle-orm'
import { db } from '../db/client.js'
import { calendars, calendarEvents } from '../db/schema.js'
import ICAL from 'ical.js';
import { eq } from 'drizzle-orm';
import { db } from '../db/client.js';
import { calendars, calendarEvents } from '../db/schema.js';
```
**ICAL.parse + Component pipeline pattern** (lines 7891 of sync.ts):
```typescript
let parsed: ReturnType<typeof ICAL.parse>
let parsed: ReturnType<typeof ICAL.parse>;
try {
parsed = ICAL.parse(obj.data as string)
parsed = ICAL.parse(obj.data as string);
} catch {
// Malformed VCALENDAR — skip but do not crash the sync
continue
continue;
}
const comp = new ICAL.Component(parsed)
const vevent = comp.getFirstSubcomponent('vevent')
if (!vevent) continue
const comp = new ICAL.Component(parsed);
const vevent = comp.getFirstSubcomponent('vevent');
if (!vevent) continue;
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null
const dtstart = vevent.getFirstPropertyValue('dtstart') as ICAL.Time | null;
```
**allDay detection pattern** (lines 8898 of sync.ts):
```typescript
// D-13 / Pitfall #3: isDate=true → DATE column; isDate=false → TIMESTAMP column
const allDay: boolean = dtstart?.isDate ?? false
const allDay: boolean = dtstart?.isDate ?? false;
```
**Error handling pattern** (lines 7578 of sync.ts):
```typescript
try {
parsed = ICAL.parse(obj.data as string)
parsed = ICAL.parse(obj.data as string);
} catch {
continue // malformed VCALENDAR — skip silently
continue; // malformed VCALENDAR — skip silently
}
```
**VTIMEZONE registration — must come before RecurExpansion** (from RESEARCH.md Pattern 1):
```typescript
// CRITICAL: Register VTIMEZONE before constructing ICAL.RecurExpansion
for (const vtz of comp.getAllSubcomponents('vtimezone')) {
const tzid = vtz.getFirstPropertyValue('tzid') as string
const tzid = vtz.getFirstPropertyValue('tzid') as string;
if (tzid && !ICAL.TimezoneService.has(tzid)) {
ICAL.TimezoneService.register(
tzid,
new ICAL.Timezone({ component: vtz, tzid }),
)
ICAL.TimezoneService.register(tzid, new ICAL.Timezone({ component: vtz, tzid }));
}
}
```
@@ -137,40 +141,43 @@ for (const vtz of comp.getAllSubcomponents('vtimezone')) {
**Analog:** `apps/api/src/routes/me.ts` + current `events.ts`
**Route file structure pattern** (lines 129 of me.ts):
```typescript
import { Hono } from 'hono'
import { getAuth } from '../auth/middleware.js'
import { upsertUser } from '../auth/user.js'
export const meRouter = new Hono()
```typescript
import { Hono } from 'hono';
import { getAuth } from '../auth/middleware.js';
import { upsertUser } from '../auth/user.js';
export const meRouter = new Hono();
meRouter.get('/', async (c) => {
const auth = await getAuth(c)
const auth = await getAuth(c);
if (!auth) {
return c.json({ error: 'Unauthorized' }, 401)
return c.json({ error: 'Unauthorized' }, 401);
}
// ... business logic
return c.json({ user: { id, displayName, color } })
})
return c.json({ user: { id, displayName, color } });
});
```
**Zod query param validation pattern** — follow `@hono/zod-validator` (from CLAUDE.md stack; no existing example yet — planner must scaffold):
```typescript
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { zValidator } from '@hono/zod-validator';
import { z } from 'zod';
const eventsQuerySchema = z.object({
start: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
end: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
})
});
eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
const { start, end } = c.req.valid('query')
const { start, end } = c.req.valid('query');
// ...
})
});
```
**Drizzle join pattern** (from sync.ts lines 59, 99 + schema.ts foreign key pattern):
```typescript
// Pattern: db.select().from(table).where(eq(...)).limit(1)
// For join: db.select().from(calendarEvents)
@@ -180,13 +187,14 @@ eventsRouter.get('/', zValidator('query', eventsQuerySchema), async (c) => {
```
**Error handling pattern** (lines 1626 of health.ts):
```typescript
try {
// ...
return c.json({ ok: true, db: 'up' })
return c.json({ ok: true, db: 'up' });
} catch (err) {
console.error('[health] DB round-trip failed:', err)
return c.json({ ok: false, db: 'down' }, 503)
console.error('[health] DB round-trip failed:', err);
return c.json({ ok: false, db: 'down' }, 503);
}
```
@@ -197,27 +205,29 @@ try {
**Analog:** `apps/api/src/auth/middleware.ts`
**Middleware export pattern** (lines 2426 of middleware.ts):
```typescript
// middleware.ts uses re-export; devBypass.ts uses named function export
export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-auth'
export { oidcAuthMiddleware, processOAuthCallback, getAuth } from '@hono/oidc-auth';
```
**Hono middleware handler signature** (from Hono docs + RESEARCH.md Pattern 5):
```typescript
import type { MiddlewareHandler } from 'hono'
import type { MiddlewareHandler } from 'hono';
export function devAuthBypass(): MiddlewareHandler {
// Hard production guard FIRST — before reading any env var
if (process.env.NODE_ENV === 'production') {
return async (_c, next) => next()
return async (_c, next) => next();
}
if (process.env.DEV_AUTH_BYPASS !== 'true') {
return async (_c, next) => next()
return async (_c, next) => next();
}
return async (c, next) => {
c.set('user', DEV_USER)
await next()
}
c.set('user', DEV_USER);
await next();
};
}
```
@@ -228,27 +238,29 @@ export function devAuthBypass(): MiddlewareHandler {
**Analog:** self
**Middleware mount order pattern** (lines 1429 of index.ts):
```typescript
// OIDC callback BEFORE auth guard (T-02-02)
app.get('/callback', (c) => processOAuthCallback(c))
app.get('/callback', (c) => processOAuthCallback(c));
// Unauthenticated routes BEFORE the guard
app.route('/health', healthRouter)
app.route('/health', healthRouter);
// Auth guard on /api/*
app.use('/api/*', oidcAuthMiddleware())
app.use('/api/*', oidcAuthMiddleware());
// Protected routes after guard
app.route('/api/me', meRouter)
app.route('/api/events', eventsRouter)
app.route('/api/me', meRouter);
app.route('/api/events', eventsRouter);
```
**Dev bypass mount pattern** — devBypass must be mounted BEFORE oidcAuthMiddleware:
```typescript
// In dev: swap oidcAuthMiddleware for devAuthBypass when bypass is active
// The bypass short-circuits the OIDC redirect entirely
app.use('/api/*', devAuthBypass()) // no-op passthrough when NODE_ENV=production or flag not set
app.use('/api/*', oidcAuthMiddleware())
app.use('/api/*', devAuthBypass()); // no-op passthrough when NODE_ENV=production or flag not set
app.use('/api/*', oidcAuthMiddleware());
// Note: devAuthBypass sets c.set('user', DEV_USER) so oidcAuthMiddleware is still called
// but getAuth(c) will find the injected user. See RESEARCH.md Pattern 5 for alternate approach.
```
@@ -260,45 +272,49 @@ app.use('/api/*', oidcAuthMiddleware())
**Analog:** `apps/api/tests/broker/poller.test.ts`
**Test file structure** (lines 114 of poller.test.ts):
```typescript
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest'
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
// vi.mock hoisted to module top by Vitest
vi.mock('../../src/broker/sync.js', () => ({
syncCalendar: mockSyncCalendar,
}))
}));
```
**describe/it/expect pattern** (lines 71121 of poller.test.ts):
```typescript
describe('broker poller — runPoll', () => {
beforeEach(() => {
vi.clearAllMocks()
vi.clearAllMocks();
// reset arrays and mock implementations
})
});
it('skips syncCalendar when ctag is unchanged', async () => {
const { runPoll } = await import('../../src/broker/poller.js')
const { runPoll } = await import('../../src/broker/poller.js');
// arrange
await runPoll()
await runPoll();
// assert
expect(mockSyncCalendar).not.toHaveBeenCalled()
})
})
expect(mockSyncCalendar).not.toHaveBeenCalled();
});
});
```
**Error resilience test pattern** (lines 176195 of poller.test.ts):
```typescript
it('handles decryptPassword failure gracefully without crashing the poller', async () => {
;(decryptPassword as Mock).mockImplementationOnce(() => {
throw new Error('Decryption failed')
})
await expect(runPoll()).resolves.not.toThrow()
expect(mockSyncCalendar).not.toHaveBeenCalled()
})
(decryptPassword as Mock).mockImplementationOnce(() => {
throw new Error('Decryption failed');
});
await expect(runPoll()).resolves.not.toThrow();
expect(mockSyncCalendar).not.toHaveBeenCalled();
});
```
**Fixture files** — create in `apps/api/tests/fixtures/` (new directory):
- `weekly-dst.ics` — weekly RRULE spanning March DST (America/New_York)
- `allday-birthday.ics` — DATE-type annual event, no DTEND
- `exdate-series.ics` — weekly series with one EXDATE
@@ -310,24 +326,25 @@ it('handles decryptPassword failure gracefully without crashing the poller', asy
**Analog:** `apps/api/tests/health.test.ts`
**Route test pattern** (lines 138 of health.test.ts):
```typescript
import { describe, it, expect, vi } from 'vitest'
import { describe, it, expect, vi } from 'vitest';
vi.mock('../src/db/client.js', () => ({
db: {
execute: vi.fn().mockResolvedValue([[{ '1': 1 }]]),
},
}))
}));
describe('GET /health', () => {
it('returns 200 with { ok: true, db: "up" } when DB round-trip succeeds', async () => {
const { app } = await import('../src/index.js')
const res = await app.request('/health')
expect(res.status).toBe(200)
const body = await res.json() as { ok: boolean; db: string }
expect(body.ok).toBe(true)
})
})
const { app } = await import('../src/index.js');
const res = await app.request('/health');
expect(res.status).toBe(200);
const body = (await res.json()) as { ok: boolean; db: string };
expect(body.ok).toBe(true);
});
});
```
**app.request() pattern for Hono route tests** — use `app.request('/api/events?start=2026-06-01&end=2026-07-01')` following the same import-in-test pattern.
@@ -340,14 +357,14 @@ describe('GET /health', () => {
```typescript
// Copy this exactly, add jsdom environment for React:
import { defineConfig } from 'vitest/config'
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'jsdom', // differs from API (node)
environment: 'jsdom', // differs from API (node)
globals: true,
},
})
});
```
---
@@ -357,32 +374,38 @@ export default defineConfig({
**Analog:** self
**Existing function pattern to copy** (lines 2234 of client.ts):
```typescript
export async function fetchMe(): Promise<MeResponse> {
const res = await fetch('/api/me', {
credentials: 'include',
})
});
if (!res.ok) {
throw new Error(`GET /api/me failed: ${res.status}`)
throw new Error(`GET /api/me failed: ${res.status}`);
}
return res.json() as Promise<MeResponse>
return res.json() as Promise<MeResponse>;
}
```
**New `fetchEvents` must follow same shape:**
```typescript
// Replace the existing fetchEvents (no-window version) with a windowed version:
export interface CalendarOccurrence { /* from shared types */ }
export interface OccurrencesResponse { occurrences: CalendarOccurrence[] }
export interface CalendarOccurrence {
/* from shared types */
}
export interface OccurrencesResponse {
occurrences: CalendarOccurrence[];
}
export async function fetchEvents(start: string, end: string): Promise<OccurrencesResponse> {
const res = await fetch(`/api/events?start=${start}&end=${end}`, {
credentials: 'include',
})
});
if (!res.ok) {
throw new Error(`GET /api/events failed: ${res.status}`)
throw new Error(`GET /api/events failed: ${res.status}`);
}
return res.json() as Promise<OccurrencesResponse>
return res.json() as Promise<OccurrencesResponse>;
}
```
@@ -393,23 +416,26 @@ export async function fetchEvents(start: string, end: string): Promise<Occurrenc
**Analog:** `apps/pwa/src/api/client.ts` (typed transform pattern)
**Interface definition pattern** (lines 1216 of client.ts):
```typescript
export interface MeUser {
id: number
displayName: string | null
color: string
id: number;
displayName: string | null;
color: string;
}
```
**Function export pattern** (lines 2234 of client.ts):
```typescript
export async function fetchMe(): Promise<MeResponse> { ... }
// → hydrateEvents follows same: export function hydrateEvents(occurrences: CalendarOccurrence[]): ScheduleXEvent[]
```
**Temporal polyfill import** — must be registered before any Temporal usage:
```typescript
import 'temporal-polyfill/global' // registers Temporal on globalThis; import in main.tsx first
import 'temporal-polyfill/global'; // registers Temporal on globalThis; import in main.tsx first
```
---
@@ -419,14 +445,16 @@ import 'temporal-polyfill/global' // registers Temporal on globalThis; import i
**Analog:** `apps/pwa/src/api/client.ts` (typed constants + factory function)
**Exported constant pattern** (lines 1216 of client.ts as reference for typed exports):
```typescript
export const WEEK_START_DAY = 0 // 0 = Sunday; Schedule-X uses 7 = Sunday (translate before passing)
export const WEEK_START_DAY = 0; // 0 = Sunday; Schedule-X uses 7 = Sunday (translate before passing)
```
**Key translation note** — document inline per RESEARCH.md:
```typescript
// WEEK_START_DAY=0 (JS/date-fns Sunday) → Schedule-X firstDayOfWeek=7 (Temporal Sunday)
const sxFirstDay = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY
const sxFirstDay = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY;
```
---
@@ -436,6 +464,7 @@ const sxFirstDay = WEEK_START_DAY === 0 ? 7 : WEEK_START_DAY
**Analog:** `apps/pwa/src/App.tsx` (ColorSwatch inline style, lines 1834)
**Color inline style pattern to extend** (lines 1834 of App.tsx):
```typescript
function ColorSwatch({ color }: { color: string }) {
return (
@@ -451,15 +480,20 @@ function ColorSwatch({ color }: { color: string }) {
```
**Target function signatures:**
```typescript
// container = main hex at 15% opacity blended over white
export function hexToContainer(hex: string): string // returns CSS hex or rgba
export function hexToContainer(hex: string): string; // returns CSS hex or rgba
// onContainer = main hex darkened 40%
export function hexToOnContainer(hex: string): string
export function hexToOnContainer(hex: string): string;
// convenience: all three for Schedule-X lightColors
export function deriveScheduleXColors(main: string): { main: string; container: string; onContainer: string }
export function deriveScheduleXColors(main: string): {
main: string;
container: string;
onContainer: string;
};
```
---
@@ -471,14 +505,14 @@ export function deriveScheduleXColors(main: string): { main: string; container:
```typescript
// State shape from UI-SPEC § State Management Contract:
interface CalendarStore {
selectedView: string // persisted in localStorage per breakpointGroup
selectedDate: string // ISO string; not persisted
openEventId: string | null // null = popover closed
calendarRange: { start: string; end: string } // drives TanStack Query key
setSelectedView: (view: string) => void
setSelectedDate: (date: string) => void
setOpenEventId: (id: string | null) => void
setCalendarRange: (range: { start: string; end: string }) => void
selectedView: string; // persisted in localStorage per breakpointGroup
selectedDate: string; // ISO string; not persisted
openEventId: string | null; // null = popover closed
calendarRange: { start: string; end: string }; // drives TanStack Query key
setSelectedView: (view: string) => void;
setSelectedDate: (date: string) => void;
setOpenEventId: (id: string | null) => void;
setCalendarRange: (range: { start: string; end: string }) => void;
}
```
@@ -489,26 +523,29 @@ interface CalendarStore {
**Analog:** `apps/pwa/src/App.tsx`
**TanStack Query usage pattern** (lines 5863 of App.tsx):
```typescript
const meQuery = useQuery({
queryKey: ['me'],
queryFn: fetchMe,
retry: false,
staleTime: 5 * 60 * 1000,
})
});
```
**Events query — extend this pattern:**
```typescript
const eventsQuery = useQuery({
queryKey: ['events', start, end],
queryFn: () => fetchEvents(start, end),
retry: 2,
staleTime: 5 * 60 * 1000,
})
});
```
**Loading/error conditional render pattern** (lines 7792 of App.tsx):
```typescript
{meQuery.isLoading && (
<div style={{ color: '#666', marginBottom: '1rem' }}>Loading...</div>
@@ -522,6 +559,7 @@ const eventsQuery = useQuery({
```
**Component file structure** (App.tsx overall shape):
- Inline interfaces at top
- Sub-components declared before default export
- Default export is the root component
@@ -534,6 +572,7 @@ const eventsQuery = useQuery({
**Analog:** `apps/pwa/src/App.tsx` (MemberBadge component, lines 3655)
**Component prop interface pattern** (lines 3638 of App.tsx):
```typescript
function MemberBadge({ user }: { user: MeUser }) {
return (
@@ -541,15 +580,17 @@ function MemberBadge({ user }: { user: MeUser }) {
```
**Target interface:**
```typescript
interface EventDetailPopoverProps {
eventId: string | null // null = closed
onClose: () => void
eventId: string | null; // null = closed
onClose: () => void;
// event data resolved from Zustand openEventId → TanStack Query cache lookup
}
```
**Accessibility pattern** from UI-SPEC:
- Focus trap while open; Escape closes
- Close button: `aria-label="Close"`; min 44px touch target
- Never use `dangerouslySetInnerHTML` for event title/description (XSS guard)
@@ -561,6 +602,7 @@ interface EventDetailPopoverProps {
**Analog:** `apps/pwa/src/App.tsx` loading state (lines 7780)
**Loading pattern to replace:**
```typescript
{meQuery.isLoading && (
<div style={{ color: '#666', marginBottom: '1rem' }}>Loading...</div>
@@ -568,17 +610,23 @@ interface EventDetailPopoverProps {
```
**Skeleton shimmer approach** — CSS animation, no third-party library:
```css
/* In tokens.css or inline: */
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
0% {
background-position: -200% 0;
}
100% {
background-position: 200% 0;
}
}
/* Apply: background: linear-gradient(90deg, var(--color-surface-dim), var(--color-border-subtle), var(--color-surface-dim));
background-size: 200% 100%; animation: shimmer 1.5s infinite; */
```
**aria-busy pattern** per UI-SPEC:
```tsx
<div aria-busy="true" aria-label="Loading calendar">
{/* shimmer placeholders */}
@@ -592,6 +640,7 @@ interface EventDetailPopoverProps {
**Analog:** self
**Current structure** (lines 121 of main.tsx):
```typescript
import React from 'react'
import ReactDOM from 'react-dom/client'
@@ -617,10 +666,11 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
```
**Add before all other imports** (Temporal polyfill must be first):
```typescript
import 'temporal-polyfill/global' // registers Temporal on globalThis FIRST
import '@schedule-x/theme-default/dist/index.css' // Schedule-X layout engine CSS
import './styles/tokens.css' // token overrides (must come after SX CSS)
import 'temporal-polyfill/global'; // registers Temporal on globalThis FIRST
import '@schedule-x/theme-default/dist/index.css'; // Schedule-X layout engine CSS
import './styles/tokens.css'; // token overrides (must come after SX CSS)
```
---
@@ -628,91 +678,112 @@ import './styles/tokens.css' // token overrides (must come after S
## Shared Patterns
### Authentication Guard (all API routes)
**Source:** `apps/api/src/index.ts` lines 2429
```typescript
app.use('/api/*', oidcAuthMiddleware())
app.route('/api/me', meRouter)
app.route('/api/events', eventsRouter)
app.use('/api/*', oidcAuthMiddleware());
app.route('/api/me', meRouter);
app.route('/api/events', eventsRouter);
```
**Apply to:** All new/modified route files. Dev bypass mounts before this, not instead.
### Hono Route Error Handling
**Source:** `apps/api/src/routes/health.ts` lines 1626
```typescript
try {
await db.execute(sql`SELECT 1`)
return c.json({ ok: true, db: 'up' })
await db.execute(sql`SELECT 1`);
return c.json({ ok: true, db: 'up' });
} catch (err) {
console.error('[health] DB round-trip failed:', err)
return c.json({ ok: false, db: 'down' }, 503)
console.error('[health] DB round-trip failed:', err);
return c.json({ ok: false, db: 'down' }, 503);
}
```
**Apply to:** `routes/events.ts` — wrap the windowed query + expansion in try/catch, return 503 on DB error.
### Drizzle Upsert Pattern
**Source:** `apps/api/src/broker/sync.ts` lines 3956
```typescript
await db
.insert(calendars)
.values({ ... })
.onDuplicateKeyUpdate({ set: { ... } })
```
**Apply to:** Any schema migration that adds columns — upsert pattern unchanged.
### D-13 allDay Discrimination
**Source:** `apps/api/src/broker/sync.ts` lines 8898
```typescript
const allDay: boolean = dtstart?.isDate ?? false
const allDay: boolean = dtstart?.isDate ?? false;
// dtstartDate: for all-day, convert YYYY-MM-DD → Date at midnight UTC
const dtstartDateValue: Date | null =
allDay && dtstart ? new Date(dtstart.toString().slice(0, 10) + 'T00:00:00Z') : null
const dtstartUtcValue: Date | null = !allDay && dtstart ? dtstart.toJSDate() : null
allDay && dtstart ? new Date(dtstart.toString().slice(0, 10) + 'T00:00:00Z') : null;
const dtstartUtcValue: Date | null = !allDay && dtstart ? dtstart.toJSDate() : null;
```
**Apply to:** `broker/expand.ts` — preserve the same discrimination when building CalendarOccurrence output. All-day `start` field must be `'YYYY-MM-DD'` (not a datetime string). Timed `start` must be a timezone-offset ISO string.
### TanStack Query Usage
**Source:** `apps/pwa/src/App.tsx` lines 5870
```typescript
const meQuery = useQuery({
queryKey: ['me'],
queryFn: fetchMe,
retry: false,
staleTime: 5 * 60 * 1000,
})
});
```
**Apply to:** All data-fetching components. Events query uses `retry: 2`. Server data never enters Zustand.
### Fetch Client with Credentials
**Source:** `apps/pwa/src/api/client.ts` lines 2234
```typescript
const res = await fetch('/api/me', { credentials: 'include' })
const res = await fetch('/api/me', { credentials: 'include' });
if (!res.ok) {
throw new Error(`GET /api/me failed: ${res.status}`)
throw new Error(`GET /api/me failed: ${res.status}`);
}
return res.json() as Promise<MeResponse>
return res.json() as Promise<MeResponse>;
```
**Apply to:** All new `client.ts` functions (`fetchEvents`). The `credentials: 'include'` is required for the OIDC session cookie.
### CSS Token Usage in Components
**Source:** `apps/pwa/src/App.tsx` lines 3755 (inline style approach)
```typescript
style={{
background: '#f0f9ff', // ← Phase 1: hardcoded
border: `2px solid ${user.color}`,
}}
```
**Apply to (Phase 2 rule):** Replace all hardcoded hex/px values with `var(--token-name)` CSS custom properties. The existing App.tsx hardcoded values must also be migrated. No hardcoded colors in any Phase 2 component.
---
## No Analog Found
| File | Role | Data Flow | Reason |
|------|------|-----------|--------|
| `apps/pwa/src/styles/tokens.css` | utility | — | No CSS token layer exists; Phase 2 introduces it from scratch |
| `apps/pwa/src/styles/tokens.ts` | utility | — | No TypeScript token mirror exists |
| `apps/pwa/src/styles/index.css` | utility | — | No global CSS exists; current App.tsx uses inline styles only |
| `apps/pwa/src/store/calendarStore.ts` | store | event-driven | No Zustand store exists in codebase yet; first Zustand usage |
| File | Role | Data Flow | Reason |
| ------------------------------------- | ------- | ------------ | ------------------------------------------------------------- |
| `apps/pwa/src/styles/tokens.css` | utility | — | No CSS token layer exists; Phase 2 introduces it from scratch |
| `apps/pwa/src/styles/tokens.ts` | utility | — | No TypeScript token mirror exists |
| `apps/pwa/src/styles/index.css` | utility | — | No global CSS exists; current App.tsx uses inline styles only |
| `apps/pwa/src/store/calendarStore.ts` | store | event-driven | No Zustand store exists in codebase yet; first Zustand usage |
---