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,6 +8,7 @@ created: 2026-06-05
---
# Phase 3 — UI Design Contract
## Event Write-Back + PWA Install
> Visual and interaction contract for Phase 3. Generated by gsd-ui-researcher.
@@ -24,13 +25,13 @@ created: 2026-06-05
## Design System
| Property | Value |
|----------|-------|
| Tool | none — CSS custom-property token layer (Phase 2 D-01/D-02) |
| Preset | not applicable |
| Component library | none — custom components against token layer |
| Icon library | lucide-react (already used: MapPin; Phase 3 adds: Edit2, Trash2, X, Check, AlertCircle, Loader2, Smartphone, Plus) |
| Font | system-ui stack: `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` |
| Property | Value |
| ----------------- | ------------------------------------------------------------------------------------------------------------------ |
| Tool | none — CSS custom-property token layer (Phase 2 D-01/D-02) |
| Preset | not applicable |
| Component library | none — custom components against token layer |
| Icon library | lucide-react (already used: MapPin; Phase 3 adds: Edit2, Trash2, X, Check, AlertCircle, Loader2, Smartphone, Plus) |
| Font | system-ui stack: `system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif` |
No `components.json` exists. No shadcn initialization needed — the token layer
is sufficient for Phase 3's form and overlay surfaces.
@@ -42,8 +43,8 @@ is sufficient for Phase 3's form and overlay surfaces.
All tokens live in `apps/pwa/src/styles/tokens.css`. Phase 3 reuses them verbatim.
The one Phase-2-declared-but-unused token now activates:
| Token | Hex | Phase 3 Usage |
|-------|-----|---------------|
| Token | Hex | Phase 3 Usage |
| --------------------- | --------- | --------------------------------------------------------- |
| `--color-destructive` | `#DC2626` | Delete button label + icon; destructive confirmation text |
No new CSS custom properties are introduced in Phase 3. Component styles reference
@@ -55,13 +56,13 @@ existing `--color-*`, `--space-*`, and `--text-*` tokens only.
_Source: Phase 2 token layer (`tokens.css`), pre-populated — no changes._
| Role | Token | Hex | Usage |
|------|-------|-----|-------|
| Dominant (60%) | `--color-surface` / `--color-surface-dim` | `#FFFFFF` / `#F7F7F8` | Page background, form surface, modal backdrop wash |
| Secondary (30%) | `--color-surface-raised`, `--color-border`, `--color-border-subtle` | `#FFFFFF`, `#E2E4E9`, `#ECEEF2` | Form card shell, input borders, section dividers, popover shells |
| Accent (10%) | Per-member `--color-member-*` + `--color-shared-family` | varies | Event chip fills, color legend swatches, calendar picker swatch — same as Phase 2. Never used on buttons, headings, or nav chrome |
| Destructive | `--color-destructive` | `#DC2626` | Delete action button label + icon ONLY; delete confirmation dialog text |
| Focus ring | `--color-focus-ring` | `#4A90D9` | Keyboard focus on all interactive elements |
| Role | Token | Hex | Usage |
| --------------- | ------------------------------------------------------------------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Dominant (60%) | `--color-surface` / `--color-surface-dim` | `#FFFFFF` / `#F7F7F8` | Page background, form surface, modal backdrop wash |
| Secondary (30%) | `--color-surface-raised`, `--color-border`, `--color-border-subtle` | `#FFFFFF`, `#E2E4E9`, `#ECEEF2` | Form card shell, input borders, section dividers, popover shells |
| Accent (10%) | Per-member `--color-member-*` + `--color-shared-family` | varies | Event chip fills, color legend swatches, calendar picker swatch — same as Phase 2. Never used on buttons, headings, or nav chrome |
| Destructive | `--color-destructive` | `#DC2626` | Delete action button label + icon ONLY; delete confirmation dialog text |
| Focus ring | `--color-focus-ring` | `#4A90D9` | Keyboard focus on all interactive elements |
**Accent reserved for:** event chip fills, color legend swatches, calendar picker color swatches. Accent colors MUST NOT appear on form submit buttons, nav items, headings, input labels, or the iOS/Android install surfaces.
@@ -73,17 +74,18 @@ _Source: Phase 2 token layer (`tokens.css`), pre-populated — no changes._
_Source: Phase 2 token layer — inherited unchanged._
| Token | CSS var | Value | Phase 3 Usage |
|-------|---------|-------|---------------|
| space-1 | `--space-1` | 4px | Icon gap within button labels, tight inline padding |
| space-2 | `--space-2` | 8px | Input label-to-field gap, compact section dividers |
| space-3 | `--space-3` | 12px | Form field internal padding (input horizontal), row gaps in install walkthrough |
| space-4 | `--space-4` | 16px | Default vertical field gap in EventForm, popover section gap |
| space-6 | `--space-6` | 24px | EventForm section padding, modal inner padding, install card padding |
| space-8 | `--space-8` | 32px | Layout gaps; gap between EventForm footer buttons |
| space-12 | `--space-12` | 48px | Major section breaks in iOS install walkthrough |
| Token | CSS var | Value | Phase 3 Usage |
| -------- | ------------ | ----- | ------------------------------------------------------------------------------- |
| space-1 | `--space-1` | 4px | Icon gap within button labels, tight inline padding |
| space-2 | `--space-2` | 8px | Input label-to-field gap, compact section dividers |
| space-3 | `--space-3` | 12px | Form field internal padding (input horizontal), row gaps in install walkthrough |
| space-4 | `--space-4` | 16px | Default vertical field gap in EventForm, popover section gap |
| space-6 | `--space-6` | 24px | EventForm section padding, modal inner padding, install card padding |
| space-8 | `--space-8` | 32px | Layout gaps; gap between EventForm footer buttons |
| space-12 | `--space-12` | 48px | Major section breaks in iOS install walkthrough |
**Exceptions:**
- Touch targets: minimum 44×44px on all interactive elements (iOS HIG). Enforced via `min-height: 44px`. Not a spacing token — a layout constraint.
- Input height: 44px minimum (satisfies touch target + visual weight).
- Delete confirmation dialog action area: min 48px button height (destructive actions warrant extra tap weight).
@@ -94,12 +96,12 @@ _Source: Phase 2 token layer — inherited unchanged._
_Source: Phase 2 token layer — inherited unchanged._
| Role | CSS vars | Size | Weight | Line Height | Phase 3 Usage |
|------|----------|------|--------|-------------|---------------|
| Body | `--text-body-*` | 15px | 400 | 1.5 | Form field values, description textarea, install walkthrough body copy, sync-state toast body |
| Label | `--text-label-*` | 13px | 400 | 1.4 | Input labels, placeholder text, helper text, calendar picker option labels, recurrence preset labels |
| Heading | `--text-heading-*` | 18px | 600 | 1.25 | EventForm modal title ("New Event" / "Edit Event"), install walkthrough step headings, delete confirmation heading |
| Display | `--text-display-*` | 24px | 600 | 1.2 | Not used in Phase 3 new surfaces (reserved for nav/day-view from Phase 2) |
| Role | CSS vars | Size | Weight | Line Height | Phase 3 Usage |
| ------- | ------------------ | ---- | ------ | ----------- | ------------------------------------------------------------------------------------------------------------------ |
| Body | `--text-body-*` | 15px | 400 | 1.5 | Form field values, description textarea, install walkthrough body copy, sync-state toast body |
| Label | `--text-label-*` | 13px | 400 | 1.4 | Input labels, placeholder text, helper text, calendar picker option labels, recurrence preset labels |
| Heading | `--text-heading-*` | 18px | 600 | 1.25 | EventForm modal title ("New Event" / "Edit Event"), install walkthrough step headings, delete confirmation heading |
| Display | `--text-display-*` | 24px | 600 | 1.2 | Not used in Phase 3 new surfaces (reserved for nav/day-view from Phase 2) |
**Weights declared:** 400 (regular) and 600 (semibold). No other weights.
@@ -116,6 +118,7 @@ _Source: Phase 2 token layer — inherited unchanged._
Phase 3 activates the reserved footer action area (line 380, `EventDetailPopover.tsx`).
**Footer — read mode:**
- "Edit" button: left-aligned, `--color-text-primary` label, Edit2 icon (16px), 44px touch target, ghost/text style (no fill)
- "Delete" button: right-aligned, `--color-destructive` label, Trash2 icon (16px), 44px touch target, ghost/text style
@@ -127,20 +130,21 @@ Mounted as a modal overlay. On phone: full-screen bottom sheet (same pattern as
**Fields (in order):**
| Field | Input type | Required | Placeholder / helper |
|-------|-----------|----------|----------------------|
| Title | text input | yes | "Event title" |
| All-day toggle | toggle switch | — | Label: "All day" |
| Start date | date input (or date picker) | yes | — |
| Start time | time input | yes (hidden when all-day) | — |
| End date | date input | yes | — |
| End time | time input | yes (hidden when all-day) | — |
| Calendar | dropdown/select (hidden when member has only 1 writable calendar — D-02) | yes | — |
| Recurrence | segmented control / select: None / Daily / Weekly / Monthly / Yearly | — | Default: None |
| Location | text input | no | "Add location" |
| Description | textarea (3 rows) | no | "Add description" |
| Field | Input type | Required | Placeholder / helper |
| -------------- | ------------------------------------------------------------------------ | ------------------------- | -------------------- |
| Title | text input | yes | "Event title" |
| All-day toggle | toggle switch | — | Label: "All day" |
| Start date | date input (or date picker) | yes | — |
| Start time | time input | yes (hidden when all-day) | — |
| End date | date input | yes | — |
| End time | time input | yes (hidden when all-day) | — |
| Calendar | dropdown/select (hidden when member has only 1 writable calendar — D-02) | yes | — |
| Recurrence | segmented control / select: None / Daily / Weekly / Monthly / Yearly | — | Default: None |
| Location | text input | no | "Add location" |
| Description | textarea (3 rows) | no | "Add description" |
**Field styling:**
- Input border: 1px solid `--color-border`; on focus: 2px `--color-focus-ring`, 2px offset
- Input border-radius: `--space-1` (4px)
- Background: `--color-surface`
@@ -149,15 +153,18 @@ Mounted as a modal overlay. On phone: full-screen bottom sheet (same pattern as
- Error state: border color `--color-destructive`; error message 13px `--color-destructive` below field
**Calendar picker (visible only when >1 writable calendar — D-02):**
- Dropdown showing calendar name + color swatch (8px circle, member color)
- Label: "Calendar"
- Options: personal calendar + "Family" (if shared family calendar exists)
**Recurrence picker:**
- Simple segmented select: "None" | "Daily" | "Weekly" | "Monthly" | "Yearly"
- Whole-series only (D-11). No custom RRULE builder in v1.
**Footer buttons:**
- Cancel: ghost button, `--color-text-secondary`, left/secondary position
- Save: filled button, `--color-text-primary` background, `#FFFFFF` label, right/primary position
- Button height: 44px minimum; border-radius: `--space-1`
@@ -175,14 +182,15 @@ Displayed after a write operation is accepted (D-05). NOT a blocking modal.
**States:**
| Status | Icon | Copy | Color |
|--------|------|------|-------|
| `pending` | Loader2 (spinning, 14px) | "Syncing…" | `--color-text-secondary` |
| `done` | Check (14px) | "Saved" | `#50C878` (member-1 green — success semantic) |
| `failed` | AlertCircle (14px) | "Didn't save — [conflict message or generic]" | `--color-destructive` |
| `dead` | AlertCircle (14px) | "Not saved. Check your connection." | `--color-destructive` |
| Status | Icon | Copy | Color |
| --------- | ------------------------ | --------------------------------------------- | --------------------------------------------- |
| `pending` | Loader2 (spinning, 14px) | "Syncing…" | `--color-text-secondary` |
| `done` | Check (14px) | "Saved" | `#50C878` (member-1 green — success semantic) |
| `failed` | AlertCircle (14px) | "Didn't save — [conflict message or generic]" | `--color-destructive` |
| `dead` | AlertCircle (14px) | "Not saved. Check your connection." | `--color-destructive` |
**Toast styling:**
- Background: `--color-surface-raised`; 1px border `--color-border`; 4px border-radius; subtle shadow
- Padding: `--space-2` vertical, `--space-3` horizontal
- Font: 13px/400 label
@@ -190,6 +198,7 @@ Displayed after a write operation is accepted (D-05). NOT a blocking modal.
- `failed` / `dead` toast includes an "×" dismiss button (X icon, 16px, 44px touch target).
**Conflict-specific toast (`failed` with 412 cause):**
- Copy: "This event changed elsewhere — review the latest version"
- No auto-dismiss; persists until user taps dismiss
@@ -200,6 +209,7 @@ Triggered by the "Delete" button in EventDetailPopover footer.
**Layout:** Centered modal dialog on all breakpoints (max-width 320px). Backdrop: `--color-overlay`.
**Content:**
- Heading (18px/600): "Delete event?"
- Body (15px/400, `--color-text-secondary`): "This will be removed from your Fastmail calendar."
- Actions (flex row, right-aligned):
@@ -217,6 +227,7 @@ Single component handling both iOS and Android flows. Renders nothing when alrea
Trigger: First visit when iOS Safari non-standalone detected. A dismissible banner slides down from below the nav bar. Not a blocking modal.
Banner:
- Background: `--color-surface-raised`; 1px bottom border `--color-border`
- Icon: Smartphone (24px, `--color-text-secondary`)
- Heading (13px/600): "Install FamilySync"
@@ -225,6 +236,7 @@ Banner:
- Dismiss (X icon): right-aligned, 44px touch target; sets `localStorage.installPromptShown = '1'` — banner never shown again
Walkthrough sheet (full-screen bottom sheet on phone):
- Header: "Add to Home Screen" (heading, 18px/600)
- 5 steps with annotated screenshots:
1. "Open FamilySync in Safari" — Safari icon callout
@@ -239,6 +251,7 @@ Walkthrough sheet (full-screen bottom sheet on phone):
Shown only when `canInstall === true` (the event has fired and not yet been dismissed).
A banner identical in layout to the iOS banner, but:
- Body: "Install FamilySync to your Home Screen for the best experience."
- Single CTA button: "Install" (filled, `--color-text-primary` background, 44px, replaces "How to install" link)
- Tapping "Install" calls `triggerInstall()` then dismisses banner
@@ -247,50 +260,50 @@ A banner identical in layout to the iOS banner, but:
## Copywriting Contract
| Element | Copy |
|---------|------|
| Primary CTA — create | "New Event" (+ icon, Plus 16px) |
| Primary CTA — save (create mode) | "Create Event" |
| Primary CTA — save (edit mode) | "Save Changes" |
| Form title — create mode | "New Event" |
| Form title — edit mode | "Edit Event" |
| Save in-flight label | "Saving…" |
| Sync toast — pending | "Syncing…" |
| Sync toast — done | "Saved" |
| Sync toast — failed (generic) | "Didn't save. Try again." |
| Sync toast — failed (conflict / 412) | "This event changed elsewhere — review the latest version" |
| Sync toast — dead | "Not saved. Check your connection." |
| Delete button label | "Delete" |
| Delete confirmation heading | "Delete event?" |
| Delete confirmation body | "This will be removed from your Fastmail calendar." |
| Delete confirmation CTA | "Delete" |
| Delete confirmation cancel | "Cancel" |
| Calendar picker label | "Calendar" |
| Recurrence picker label | "Repeat" |
| Recurrence options | "None" / "Daily" / "Weekly" / "Monthly" / "Yearly" |
| All-day toggle label | "All day" |
| Title input placeholder | "Event title" |
| Location input placeholder | "Add location" |
| Description placeholder | "Add description" |
| Empty title validation error | "Title is required" |
| End-before-start validation error | "End time must be after start" |
| iOS install banner heading | "Install FamilySync" |
| iOS install banner body | "Add to your Home Screen for the best experience." |
| iOS install banner CTA | "How to install" |
| Android install banner body | "Install FamilySync to your Home Screen for the best experience." |
| Android install banner CTA | "Install" |
| iOS walkthrough sheet heading | "Add to Home Screen" |
| iOS walkthrough step 1 | "Open FamilySync in Safari" |
| iOS walkthrough step 2 | "Tap the Share button" |
| iOS walkthrough step 3 | "Scroll down and tap 'Add to Home Screen'" |
| iOS walkthrough step 4 | "Tap 'Add' in the top right" |
| iOS walkthrough step 5 | "Open FamilySync from your Home Screen — it opens without the browser bar" |
| iOS walkthrough close | "Done" |
| Element | Copy |
| ------------------------------------ | -------------------------------------------------------------------------- |
| Primary CTA — create | "New Event" (+ icon, Plus 16px) |
| Primary CTA — save (create mode) | "Create Event" |
| Primary CTA — save (edit mode) | "Save Changes" |
| Form title — create mode | "New Event" |
| Form title — edit mode | "Edit Event" |
| Save in-flight label | "Saving…" |
| Sync toast — pending | "Syncing…" |
| Sync toast — done | "Saved" |
| Sync toast — failed (generic) | "Didn't save. Try again." |
| Sync toast — failed (conflict / 412) | "This event changed elsewhere — review the latest version" |
| Sync toast — dead | "Not saved. Check your connection." |
| Delete button label | "Delete" |
| Delete confirmation heading | "Delete event?" |
| Delete confirmation body | "This will be removed from your Fastmail calendar." |
| Delete confirmation CTA | "Delete" |
| Delete confirmation cancel | "Cancel" |
| Calendar picker label | "Calendar" |
| Recurrence picker label | "Repeat" |
| Recurrence options | "None" / "Daily" / "Weekly" / "Monthly" / "Yearly" |
| All-day toggle label | "All day" |
| Title input placeholder | "Event title" |
| Location input placeholder | "Add location" |
| Description placeholder | "Add description" |
| Empty title validation error | "Title is required" |
| End-before-start validation error | "End time must be after start" |
| iOS install banner heading | "Install FamilySync" |
| iOS install banner body | "Add to your Home Screen for the best experience." |
| iOS install banner CTA | "How to install" |
| Android install banner body | "Install FamilySync to your Home Screen for the best experience." |
| Android install banner CTA | "Install" |
| iOS walkthrough sheet heading | "Add to Home Screen" |
| iOS walkthrough step 1 | "Open FamilySync in Safari" |
| iOS walkthrough step 2 | "Tap the Share button" |
| iOS walkthrough step 3 | "Scroll down and tap 'Add to Home Screen'" |
| iOS walkthrough step 4 | "Tap 'Add' in the top right" |
| iOS walkthrough step 5 | "Open FamilySync from your Home Screen — it opens without the browser bar" |
| iOS walkthrough close | "Done" |
**Destructive actions and confirmation patterns:**
| Action | Trigger | Confirmation approach |
|--------|---------|----------------------|
| Action | Trigger | Confirmation approach |
| ------------ | -------------------------------------------- | ------------------------------------------------- |
| Delete event | "Delete" button in EventDetailPopover footer | DeleteConfirmationDialog modal — explicit two-tap |
No inline delete (single tap). No "undo" toast. Confirmation dialog is mandatory for all deletes.
@@ -380,47 +393,49 @@ All interactive elements in Phase 3 new surfaces: minimum 44×44px. Enforced via
_Extends Phase 2 contract. Server state in TanStack Query; UI state in Zustand._
| State | Owner | Key | Notes |
|-------|-------|-----|-------|
| Event list (read) | TanStack Query | `['events', start, end]` | Invalidated on `done` sync or conflict re-sync |
| Sync status (per UID) | TanStack Query | `['syncStatus', uid]` | `refetchInterval: 3000` while `pending`; disabled on terminal |
| Current user | TanStack Query | `['me']` | Needed to determine writable calendar set |
| Writable calendars | TanStack Query | `['writableCalendars']` | Drives calendar picker visibility (D-02) |
| EventForm open | Zustand | `eventFormOpen` | boolean |
| EventForm mode | Zustand | `eventFormMode` | `'create' \| 'edit'` |
| EventForm prefill UID | Zustand | `eventFormUid` | `string \| null` — UID of event being edited |
| Delete dialog open | Zustand | `deleteDialogOpen` | boolean |
| Delete dialog UID | Zustand | `deleteDialogUid` | `string \| null` |
| Last-synced UID | Zustand | `lastSyncedUid` | Drives SyncStateToast display |
| Install prompt dismissed | localStorage | `installPromptDismissed` | Persistent across sessions |
| State | Owner | Key | Notes |
| ------------------------ | -------------- | ------------------------ | ------------------------------------------------------------- |
| Event list (read) | TanStack Query | `['events', start, end]` | Invalidated on `done` sync or conflict re-sync |
| Sync status (per UID) | TanStack Query | `['syncStatus', uid]` | `refetchInterval: 3000` while `pending`; disabled on terminal |
| Current user | TanStack Query | `['me']` | Needed to determine writable calendar set |
| Writable calendars | TanStack Query | `['writableCalendars']` | Drives calendar picker visibility (D-02) |
| EventForm open | Zustand | `eventFormOpen` | boolean |
| EventForm mode | Zustand | `eventFormMode` | `'create' \| 'edit'` |
| EventForm prefill UID | Zustand | `eventFormUid` | `string \| null` — UID of event being edited |
| Delete dialog open | Zustand | `deleteDialogOpen` | boolean |
| Delete dialog UID | Zustand | `deleteDialogUid` | `string \| null` |
| Last-synced UID | Zustand | `lastSyncedUid` | Drives SyncStateToast display |
| Install prompt dismissed | localStorage | `installPromptDismissed` | Persistent across sessions |
---
## PWA Manifest Contract
| Field | Value |
|-------|-------|
| `name` | FamilySync |
| `short_name` | FamilySync |
| `description` | Family calendar and lists |
| `theme_color` | `#4A90D9` (matches `--color-focus-ring`) |
| `background_color` | `#FFFFFF` |
| `display` | `standalone` |
| `scope` | `/` |
| `start_url` | `/` |
| `icons` | 192×192 PNG, 512×512 PNG, 512×512 maskable PNG, 180×180 apple-touch-icon |
| Field | Value |
| ------------------ | ------------------------------------------------------------------------ |
| `name` | FamilySync |
| `short_name` | FamilySync |
| `description` | Family calendar and lists |
| `theme_color` | `#4A90D9` (matches `--color-focus-ring`) |
| `background_color` | `#FFFFFF` |
| `display` | `standalone` |
| `scope` | `/` |
| `start_url` | `/` |
| `icons` | 192×192 PNG, 512×512 PNG, 512×512 maskable PNG, 180×180 apple-touch-icon |
**Required HTML `<head>` entries (`apps/pwa/index.html`):**
```html
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180">
<meta name="theme-color" content="#4A90D9">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="FamilySync">
<link rel="apple-touch-icon" href="/apple-touch-icon.png" sizes="180x180" />
<meta name="theme-color" content="#4A90D9" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<meta name="apple-mobile-web-app-title" content="FamilySync" />
```
**Service worker — critical denylist (Gate 2):**
The SW must NEVER intercept the OIDC callback. `navigateFallbackDenylist` must include:
- `/^\/callback/` — OIDC authorization code exchange
- `/^\/api\//` — all API calls
- `/^\/health/` — health endpoint
@@ -429,11 +444,11 @@ The SW must NEVER intercept the OIDC callback. `navigateFallbackDenylist` must i
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none — shadcn not initialized | not applicable |
| lucide-react (npm) | Edit2, Trash2, X, Check, AlertCircle, Loader2, Smartphone, Plus (new); MapPin already used in Phase 2 | npm package — standard supply chain; no registry vetting required |
| vite-plugin-pwa (npm) | VitePWA plugin + workbox-window + workbox-build (peer deps) | npm package — in CLAUDE.md recommended stack; pre-approved; standard supply chain |
| Registry | Blocks Used | Safety Gate |
| --------------------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| shadcn official | none — shadcn not initialized | not applicable |
| lucide-react (npm) | Edit2, Trash2, X, Check, AlertCircle, Loader2, Smartphone, Plus (new); MapPin already used in Phase 2 | npm package — standard supply chain; no registry vetting required |
| vite-plugin-pwa (npm) | VitePWA plugin + workbox-window + workbox-build (peer deps) | npm package — in CLAUDE.md recommended stack; pre-approved; standard supply chain |
No third-party shadcn registries. No registry vetting gate required.
@@ -441,28 +456,28 @@ No third-party shadcn registries. No registry vetting gate required.
## Pre-Population Sources
| Decision | Source |
|----------|--------|
| Full token layer (colors, spacing, typography) | `apps/pwa/src/styles/tokens.css` — codebase scan |
| `--color-destructive: #DC2626` | `tokens.css` line 52 — pre-declared in Phase 2 for Phase 3 reuse |
| lucide-react as icon library | Phase 2 UI-SPEC §Design System; confirmed in `EventDetailPopover.tsx` import |
| system-ui font stack | `tokens.css` `--font-family-base` |
| Bottom sheet / popover responsive pattern | Phase 2 UI-SPEC §EventDetailPopover; `EventDetailPopover.tsx` implementation |
| 44px touch target requirement | Phase 2 UI-SPEC §Interaction Contract; iOS HIG |
| Edit/delete surface = EventDetailPopover footer | CONTEXT.md D-10; Phase 2 D-08 |
| Calendar picker hidden when 1 writable calendar | CONTEXT.md D-02 |
| Default calendar = last-used; first-time = personal | CONTEXT.md D-01 |
| Optimistic accept + SyncStateToast | CONTEXT.md D-05/D-06/D-09 |
| Conflict detection + warn (no silent overwrite) | CONTEXT.md D-08 |
| Polling (not SSE) for sync state | CONTEXT.md D-09 (SSE unverified until Phase 4) |
| Recurring: whole-series presets only | CONTEXT.md D-11; REQUIREMENTS.md CAL-07 |
| iOS install = first-visit auto-detect banner | CONTEXT.md §Claude's Discretion; RESEARCH.md Pattern 6 |
| Android install = `beforeinstallprompt` | CONTEXT.md §Claude's Discretion; RESEARCH.md Pattern 7 |
| PWA manifest fields + SW denylist | RESEARCH.md Pattern 5 |
| `theme_color: #4A90D9` | RESEARCH.md Pattern 5 (matches `--color-member-0`) |
| iOS annotated walkthrough 5-step content | RESEARCH.md Pattern 6 |
| Two-tap delete confirmation | Researcher default (destructive, irreversible, two-person household) |
| `role="status"` / `role="alert"` for toast | WCAG live region pattern — researcher default |
| Decision | Source |
| --------------------------------------------------- | ---------------------------------------------------------------------------- |
| Full token layer (colors, spacing, typography) | `apps/pwa/src/styles/tokens.css` — codebase scan |
| `--color-destructive: #DC2626` | `tokens.css` line 52 — pre-declared in Phase 2 for Phase 3 reuse |
| lucide-react as icon library | Phase 2 UI-SPEC §Design System; confirmed in `EventDetailPopover.tsx` import |
| system-ui font stack | `tokens.css` `--font-family-base` |
| Bottom sheet / popover responsive pattern | Phase 2 UI-SPEC §EventDetailPopover; `EventDetailPopover.tsx` implementation |
| 44px touch target requirement | Phase 2 UI-SPEC §Interaction Contract; iOS HIG |
| Edit/delete surface = EventDetailPopover footer | CONTEXT.md D-10; Phase 2 D-08 |
| Calendar picker hidden when 1 writable calendar | CONTEXT.md D-02 |
| Default calendar = last-used; first-time = personal | CONTEXT.md D-01 |
| Optimistic accept + SyncStateToast | CONTEXT.md D-05/D-06/D-09 |
| Conflict detection + warn (no silent overwrite) | CONTEXT.md D-08 |
| Polling (not SSE) for sync state | CONTEXT.md D-09 (SSE unverified until Phase 4) |
| Recurring: whole-series presets only | CONTEXT.md D-11; REQUIREMENTS.md CAL-07 |
| iOS install = first-visit auto-detect banner | CONTEXT.md §Claude's Discretion; RESEARCH.md Pattern 6 |
| Android install = `beforeinstallprompt` | CONTEXT.md §Claude's Discretion; RESEARCH.md Pattern 7 |
| PWA manifest fields + SW denylist | RESEARCH.md Pattern 5 |
| `theme_color: #4A90D9` | RESEARCH.md Pattern 5 (matches `--color-member-0`) |
| iOS annotated walkthrough 5-step content | RESEARCH.md Pattern 6 |
| Two-tap delete confirmation | Researcher default (destructive, irreversible, two-person household) |
| `role="status"` / `role="alert"` for toast | WCAG live region pattern — researcher default |
---