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
@@ -22,13 +22,13 @@ created: 2026-06-09
## Design System
| Property | Value |
|----------|-------|
| Tool | none (custom CSS tokens, no shadcn) |
| Preset | not applicable |
| Component library | none (inline styles via CSS custom properties) |
| Icon library | lucide-react (existing; already used in InstallPrompt, AppNav, BottomTabBar) |
| Font | system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif (var(--font-family-base)) |
| Property | Value |
| ----------------- | ---------------------------------------------------------------------------------------------- |
| Tool | none (custom CSS tokens, no shadcn) |
| Preset | not applicable |
| Component library | none (inline styles via CSS custom properties) |
| Icon library | lucide-react (existing; already used in InstallPrompt, AppNav, BottomTabBar) |
| Font | system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif (var(--font-family-base)) |
Source: `apps/pwa/src/styles/tokens.css`, `apps/pwa/src/styles/tokens.ts`
@@ -38,17 +38,18 @@ Source: `apps/pwa/src/styles/tokens.css`, `apps/pwa/src/styles/tokens.ts`
Declared values (multiples of 4 — carried verbatim from tokens.css):
| Token | Value | Usage |
|-------|-------|-------|
| --space-1 | 4px | Icon gaps, border-radius on buttons |
| --space-2 | 8px | Label-to-input gap, compact element spacing |
| --space-3 | 12px | Banner internal padding, step gaps |
| --space-4 | 16px | Default element padding, input/button padding |
| --space-6 | 24px | Sheet padding, section gaps |
| --space-8 | 32px | Larger section breaks |
| --space-12 | 48px | Major section breaks |
| Token | Value | Usage |
| ---------- | ----- | --------------------------------------------- |
| --space-1 | 4px | Icon gaps, border-radius on buttons |
| --space-2 | 8px | Label-to-input gap, compact element spacing |
| --space-3 | 12px | Banner internal padding, step gaps |
| --space-4 | 16px | Default element padding, input/button padding |
| --space-6 | 24px | Sheet padding, section gaps |
| --space-8 | 32px | Larger section breaks |
| --space-12 | 48px | Major section breaks |
Exceptions:
- Touch targets: minimum `44px` height/width on all interactive elements (matches InstallPrompt pattern)
- Bottom sheet border-radius: `12px 12px 0 0` (matches WalkthroughSheet and CreateListSheet pattern)
@@ -60,12 +61,12 @@ Source: `apps/pwa/src/styles/tokens.css` — unchanged from Phase 2.
Carried verbatim from tokens.css — no new type roles added:
| Role | Size | Weight | Line Height | Usage in Phase 5 |
|------|------|--------|-------------|------------------|
| Body | 15px (var(--text-body-size)) | 400 (var(--text-body-weight)) | 1.5 (var(--text-body-line-height)) | Permission prompt explainer text, settings row description |
| Label | 13px (var(--text-label-size)) | 400 (var(--text-label-weight)) | 1.4 (var(--text-label-line-height)) | Banner subtitle, toggle state label, secondary notification copy |
| Heading | 18px (var(--text-heading-size)) | 600 (var(--text-heading-weight)) | 1.25 (var(--text-heading-line-height)) | Settings sheet heading, permission prompt heading |
| Display | 24px (var(--text-display-size)) | 600 (var(--text-display-weight)) | 1.2 (var(--text-display-line-height)) | App name in nav (unchanged) |
| Role | Size | Weight | Line Height | Usage in Phase 5 |
| ------- | ------------------------------- | -------------------------------- | -------------------------------------- | ---------------------------------------------------------------- |
| Body | 15px (var(--text-body-size)) | 400 (var(--text-body-weight)) | 1.5 (var(--text-body-line-height)) | Permission prompt explainer text, settings row description |
| Label | 13px (var(--text-label-size)) | 400 (var(--text-label-weight)) | 1.4 (var(--text-label-line-height)) | Banner subtitle, toggle state label, secondary notification copy |
| Heading | 18px (var(--text-heading-size)) | 600 (var(--text-heading-weight)) | 1.25 (var(--text-heading-line-height)) | Settings sheet heading, permission prompt heading |
| Display | 24px (var(--text-display-size)) | 600 (var(--text-display-weight)) | 1.2 (var(--text-display-line-height)) | App name in nav (unchanged) |
Source: `apps/pwa/src/styles/tokens.css` — unchanged from Phase 2.
@@ -75,14 +76,15 @@ Source: `apps/pwa/src/styles/tokens.css` — unchanged from Phase 2.
Carried from tokens.css — no new colors added:
| Role | Value | Usage |
|------|-------|-------|
| Dominant (60%) | #FFFFFF (var(--color-surface)) | Sheet background, banner background, page background |
| Secondary (30%) | #F7F7F8 (var(--color-surface-dim)) | Toggle track (off state), sheet backdrop dim |
| Accent (10%) | #4A90D9 (var(--color-member-0)) | Enable Notifications CTA button, toggle track (on state), "How to enable" link text |
| Destructive | #DC2626 (var(--color-destructive)) | Permission-denied banner icon, "Notifications blocked" state |
| Role | Value | Usage |
| --------------- | ---------------------------------- | ----------------------------------------------------------------------------------- |
| Dominant (60%) | #FFFFFF (var(--color-surface)) | Sheet background, banner background, page background |
| Secondary (30%) | #F7F7F8 (var(--color-surface-dim)) | Toggle track (off state), sheet backdrop dim |
| Accent (10%) | #4A90D9 (var(--color-member-0)) | Enable Notifications CTA button, toggle track (on state), "How to enable" link text |
| Destructive | #DC2626 (var(--color-destructive)) | Permission-denied banner icon, "Notifications blocked" state |
Accent reserved for:
- The "Enable Notifications" primary action button in the permission prompt
- The toggle track/thumb in the on state (notifications-enabled)
- The "How to enable" inline link in the permission-denied banner
@@ -90,6 +92,7 @@ Accent reserved for:
Not used on: navigation chrome, sheet headers, settings row labels, or secondary text.
Additional semantic tokens used (not new — from tokens.css):
- `--color-text-primary` (#111318): all primary text
- `--color-text-secondary` (#6B7280): secondary/helper text, dismiss icons
- `--color-text-muted` (#9CA3AF): toggle state label when off
@@ -117,6 +120,7 @@ same location) — conditional on `isInstalled() === true` and
`Notification.permission === 'default'`.
**Layout pattern:** Same bottom sheet as `WalkthroughSheet` in InstallPrompt.tsx:
- `position: fixed; bottom: 0; left: 0; right: 0`
- `background: var(--color-surface)`
- `borderRadius: 12px 12px 0 0`
@@ -127,6 +131,7 @@ same location) — conditional on `isInstalled() === true` and
- Backdrop click does NOT dismiss (permission UX must be explicit — tap or dismiss button)
**Contents:**
```
[Bell icon, 24px, --color-text-secondary]
[Heading] "Stay in the loop"
@@ -136,12 +141,14 @@ same location) — conditional on `isInstalled() === true` and
```
**States:**
- Default: heading + body + Enable button + Not now button
- Loading (after tap, awaiting OS dialog): "Enable Notifications" button shows `Loader2` spinner (20px, lucide-react), disabled, no label change
- Granted (OS resolved granted): sheet closes immediately, no toast
- Denied (OS resolved denied): sheet closes, permission-denied banner appears (Surface 3)
**Accessibility:**
- `role="dialog"`, `aria-modal="true"`, `aria-label="Enable push notifications"`
- Focus trap: first focusable element is "Enable Notifications" button
- Dismiss via "Not now" button only (no backdrop dismiss — intentional)
@@ -161,6 +168,7 @@ area in `PhoneNav`) is promoted to a `<button>` that opens a Settings bottom she
On desktop the same avatar in `DesktopNav` opens the sheet.
**Settings sheet layout:**
- Same bottom sheet idiom as `CreateListSheet` and `WalkthroughSheet`
- `role="dialog"`, `aria-modal="true"`, `aria-label="Settings"`
- `position: fixed; bottom: 0; left: 0; right: 0`
@@ -171,6 +179,7 @@ On desktop the same avatar in `DesktopNav` opens the sheet.
- Backdrop at `zIndex: 300`, click to close
**Sheet contents:**
```
[Heading row]
"Settings" (font: heading 18px/600)
@@ -198,12 +207,14 @@ On desktop the same avatar in `DesktopNav` opens the sheet.
```
**Toggle behavior:**
- `on → off`: calls DELETE /api/push/subscription (unregisters VAPID subscription), sets `localStorage.notificationsEnabled = '0'`
- `off → on` (permission = 'default'): triggers `Notification.requestPermission()` + `pushManager.subscribe()` in the tap handler. On grant: registers subscription. On deny: shows permission-denied hint.
- `off → on` (permission = 'granted'): silently calls `pushManager.subscribe()` + POST /api/push/subscription. No OS dialog.
- `off → on` (permission = 'denied'): toggle does not toggle — shows permission-denied hint inline. The toggle is visually disabled (opacity 0.5).
**Toggle initial state on open:**
- `on` when `localStorage.notificationsEnabled !== '0'` AND `Notification.permission === 'granted'` AND a valid subscription exists
- `off` in all other cases
@@ -220,6 +231,7 @@ was previously `'1'`. Silent re-subscribe covers expired subscriptions (D-10)
banner is ONLY for the OS-revoked case.
**Layout:** Same banner idiom as the install prompt banner in `InstallPrompt.tsx`:
- `role="alert"` (assertive — permission loss is high-priority)
- `display: flex; alignItems: center; gap: var(--space-3)`
- `padding: var(--space-3) var(--space-4)`
@@ -228,6 +240,7 @@ banner is ONLY for the OS-revoked case.
- `fontFamily: var(--font-family-base)`
**Contents:**
```
[AlertCircle icon, 24px, --color-destructive, aria-hidden]
[Column, flex: 1]
@@ -240,6 +253,7 @@ No dismiss button — the banner persists until OS permission is restored. (The
cannot dismiss it since it represents a broken system state that needs resolution.)
**"How to enable" link behavior:**
- iOS: opens a bottom sheet with step-by-step instructions (same WalkthroughSheet idiom):
1. Open Settings on your iPhone
2. Scroll down and tap Safari
@@ -265,6 +279,7 @@ data: { url: "/calendar?date={YYYY-MM-DD}&event={eventUid}" }
```
Example:
```
title: "Dentist"
body: "Starts in 15 min"
@@ -280,6 +295,7 @@ data: { url: "/calendar?date={YYYY-MM-DD}&event={eventUid}" }
```
Example:
```
title: "Lucas added an event"
body: "Soccer practice · Wed 3 pm"
@@ -295,12 +311,14 @@ data: { url: "/calendar?date={YYYY-MM-DD}&event={eventUid}" }
```
Example:
```
title: "Lucas updated an event"
body: "Dentist · moved to Thu 2 pm"
```
For deletion:
```
title: "{ActorName} removed an event"
body: "{EventTitle}"
@@ -318,6 +336,7 @@ data: { url: "/lists/{listId}" }
```
Examples:
```
title: "Lucas updated Groceries"
body: "3 changes"
@@ -329,6 +348,7 @@ body: "1 change"
### Time format rule
`{formattedTime}` uses the member's local timezone.
- Same-day timed events: `"{DayAbbr} {H}:{MM} {am/pm}"` — e.g. "Wed 3:00 pm"
- All-day events: never appear in reminder or change notifications (D-07)
@@ -336,12 +356,12 @@ body: "1 change"
## Tap-to-Open Deep Links (D-14)
| Notification type | Tap destination |
|-------------------|----------------|
| Event reminder | `/calendar?date={YYYY-MM-DD}&event={eventUid}` |
| Notification type | Tap destination |
| --------------------------- | ---------------------------------------------- |
| Event reminder | `/calendar?date={YYYY-MM-DD}&event={eventUid}` |
| Event change (new/modified) | `/calendar?date={YYYY-MM-DD}&event={eventUid}` |
| Event deletion | `/calendar` |
| List change | `/lists/{listId}` |
| Event deletion | `/calendar` |
| List change | `/lists/{listId}` |
The `notificationclick` service-worker handler calls `clients.openWindow(event.notification.data.url)`.
@@ -349,36 +369,36 @@ The `notificationclick` service-worker handler calls `clients.openWindow(event.n
## Copywriting Contract
| Element | Copy |
|---------|------|
| Permission prompt heading | "Stay in the loop" |
| Permission prompt body | "Get notified when events are coming up or your family makes changes." |
| Permission prompt primary CTA | "Enable Notifications" |
| Permission prompt secondary | "Not now" |
| Settings sheet heading | "Settings" |
| Settings section label | "Notifications" |
| Settings toggle label | "FamilySync Notifications" |
| Settings toggle sublabel | "Reminders, event changes, list updates" |
| Settings toggle on label (aria) | "FamilySync Notifications, on" |
| Settings toggle off label (aria) | "FamilySync Notifications, off" |
| Permission-denied banner heading | "Notifications blocked" |
| Permission-denied banner body | "Re-enable in your browser settings." |
| Permission-denied inline link | "How to enable" |
| iOS re-enable step 1 | "Open Settings on your iPhone" |
| iOS re-enable step 2 | "Scroll down and tap Safari" |
| iOS re-enable step 3 | "Tap Notifications" |
| iOS re-enable step 4 | "Allow notifications for FamilySync" |
| Android re-enable step 1 | "Open Chrome on your phone" |
| Android re-enable step 2 | "Tap the three-dot menu → Settings" |
| Android re-enable step 3 | "Tap Site Settings → Notifications" |
| Android re-enable step 4 | "Find FamilySync and tap Allow" |
| Notification body — reminder | "Starts in 15 min" |
| Notification title — new event | "{ActorName} added an event" |
| Notification title — updated event | "{ActorName} updated an event" |
| Notification title — deleted event | "{ActorName} removed an event" |
| Notification title — list change | "{ActorName} updated {ListName}" |
| Notification body — list change (1) | "1 change" |
| Notification body — list change (N) | "{N} changes" |
| Element | Copy |
| ----------------------------------- | ---------------------------------------------------------------------- |
| Permission prompt heading | "Stay in the loop" |
| Permission prompt body | "Get notified when events are coming up or your family makes changes." |
| Permission prompt primary CTA | "Enable Notifications" |
| Permission prompt secondary | "Not now" |
| Settings sheet heading | "Settings" |
| Settings section label | "Notifications" |
| Settings toggle label | "FamilySync Notifications" |
| Settings toggle sublabel | "Reminders, event changes, list updates" |
| Settings toggle on label (aria) | "FamilySync Notifications, on" |
| Settings toggle off label (aria) | "FamilySync Notifications, off" |
| Permission-denied banner heading | "Notifications blocked" |
| Permission-denied banner body | "Re-enable in your browser settings." |
| Permission-denied inline link | "How to enable" |
| iOS re-enable step 1 | "Open Settings on your iPhone" |
| iOS re-enable step 2 | "Scroll down and tap Safari" |
| iOS re-enable step 3 | "Tap Notifications" |
| iOS re-enable step 4 | "Allow notifications for FamilySync" |
| Android re-enable step 1 | "Open Chrome on your phone" |
| Android re-enable step 2 | "Tap the three-dot menu → Settings" |
| Android re-enable step 3 | "Tap Site Settings → Notifications" |
| Android re-enable step 4 | "Find FamilySync and tap Allow" |
| Notification body — reminder | "Starts in 15 min" |
| Notification title — new event | "{ActorName} added an event" |
| Notification title — updated event | "{ActorName} updated an event" |
| Notification title — deleted event | "{ActorName} removed an event" |
| Notification title — list change | "{ActorName} updated {ListName}" |
| Notification body — list change (1) | "1 change" |
| Notification body — list change (N) | "{N} changes" |
No destructive actions in this phase. The toggle is not destructive — it silently
unregisters the push subscription without a confirmation dialog.
@@ -389,31 +409,31 @@ unregisters the push subscription without a confirmation dialog.
### Permission prompt
| State | Visual |
|-------|--------|
| Default | "Enable Notifications" active (--color-member-0 bg, white text) |
| Tapping "Enable Notifications" | Button shows Loader2 spinner, disabled |
| OS granted | Sheet closes, no toast |
| OS denied | Sheet closes, permission-denied banner appears |
| "Not now" tapped | Sheet closes, localStorage flag set, no banner |
| State | Visual |
| ------------------------------ | --------------------------------------------------------------- |
| Default | "Enable Notifications" active (--color-member-0 bg, white text) |
| Tapping "Enable Notifications" | Button shows Loader2 spinner, disabled |
| OS granted | Sheet closes, no toast |
| OS denied | Sheet closes, permission-denied banner appears |
| "Not now" tapped | Sheet closes, localStorage flag set, no banner |
### Settings toggle
| State | Visual |
|-------|--------|
| On | Track: --color-member-0, thumb: white |
| Off | Track: --color-border, thumb: white |
| Disabled (permission denied) | Track: --color-border, opacity 0.5, no pointer events |
| Toggling on (awaiting subscribe) | Loader2 spinner replaces toggle, 20px |
| Toggling off | Immediate visual, subscribe DELETE in background |
| State | Visual |
| -------------------------------- | ----------------------------------------------------- |
| On | Track: --color-member-0, thumb: white |
| Off | Track: --color-border, thumb: white |
| Disabled (permission denied) | Track: --color-border, opacity 0.5, no pointer events |
| Toggling on (awaiting subscribe) | Loader2 spinner replaces toggle, 20px |
| Toggling off | Immediate visual, subscribe DELETE in background |
### Permission-denied banner
| State | Visual |
|-------|--------|
| Shown | Always visible below AppNav when permission === 'denied' and was previously enabled |
| "How to enable" tapped | Opens OS-specific instruction sheet |
| Permission restored externally | Banner disappears on next `Notification.permission` check |
| State | Visual |
| ------------------------------ | ----------------------------------------------------------------------------------- |
| Shown | Always visible below AppNav when permission === 'denied' and was previously enabled |
| "How to enable" tapped | Opens OS-specific instruction sheet |
| Permission restored externally | Banner disappears on next `Notification.permission` check |
---
@@ -421,12 +441,12 @@ unregisters the push subscription without a confirmation dialog.
Matches existing layers (from component audit):
| Layer | z-index | Surface |
|-------|---------|---------|
| Bottom tab bar | 200 | BottomTabBar (existing) |
| Backdrop | 300 | CreateListSheet, Settings sheet backdrop |
| Sheet / Dialog | 301 | CreateListSheet, Settings sheet, permission prompt sheet |
| Overlay dialogs | 1000 | WalkthroughSheet (existing), permission prompt (matches WalkthroughSheet) |
| Layer | z-index | Surface |
| --------------- | ------- | ------------------------------------------------------------------------- |
| Bottom tab bar | 200 | BottomTabBar (existing) |
| Backdrop | 300 | CreateListSheet, Settings sheet backdrop |
| Sheet / Dialog | 301 | CreateListSheet, Settings sheet, permission prompt sheet |
| Overlay dialogs | 1000 | WalkthroughSheet (existing), permission prompt (matches WalkthroughSheet) |
Permission prompt uses `zIndex: 999` for backdrop, `zIndex: 1000` for sheet — matching
the existing `WalkthroughSheet` in `InstallPrompt.tsx`.
@@ -437,15 +457,16 @@ the existing `WalkthroughSheet` in `InstallPrompt.tsx`.
New components this phase:
| Component | File | Reuses |
|-----------|------|--------|
| `PushPermissionPrompt` | `apps/pwa/src/components/PushPermissionPrompt.tsx` | WalkthroughSheet layout, InstallPrompt token pattern |
| `SettingsSheet` | `apps/pwa/src/components/SettingsSheet.tsx` | CreateListSheet layout, AppNav avatar trigger |
| `NotificationToggle` | inside `SettingsSheet.tsx` | inline — no separate file needed |
| `PermissionDeniedBanner` | `apps/pwa/src/components/PermissionDeniedBanner.tsx` | InstallPrompt banner layout |
| `usePushSubscription` | `apps/pwa/src/hooks/usePushSubscription.ts` | new hook — manages subscribe/unsubscribe lifecycle |
| Component | File | Reuses |
| ------------------------ | ---------------------------------------------------- | ---------------------------------------------------- |
| `PushPermissionPrompt` | `apps/pwa/src/components/PushPermissionPrompt.tsx` | WalkthroughSheet layout, InstallPrompt token pattern |
| `SettingsSheet` | `apps/pwa/src/components/SettingsSheet.tsx` | CreateListSheet layout, AppNav avatar trigger |
| `NotificationToggle` | inside `SettingsSheet.tsx` | inline — no separate file needed |
| `PermissionDeniedBanner` | `apps/pwa/src/components/PermissionDeniedBanner.tsx` | InstallPrompt banner layout |
| `usePushSubscription` | `apps/pwa/src/hooks/usePushSubscription.ts` | new hook — manages subscribe/unsubscribe lifecycle |
Modified components:
- `apps/pwa/src/components/InstallPrompt.tsx` — add `PushPermissionPrompt` trigger after install confirms (D-08)
- `apps/pwa/src/components/AppNav.tsx` — promote user avatar div to `<button>` opening `SettingsSheet`
- `apps/pwa/src/App.tsx` — mount `PermissionDeniedBanner` and `SettingsSheet`
@@ -456,24 +477,24 @@ No new routes. No new tabs in `BottomTabBar`. Settings is a sheet, not a route.
## Accessibility Requirements
| Surface | Requirement |
|---------|-------------|
| PushPermissionPrompt | `role="dialog"`, `aria-modal="true"`, focus trap on open |
| SettingsSheet | `role="dialog"`, `aria-modal="true"`, Escape key to close, backdrop click to close |
| NotificationToggle | `role="switch"`, `aria-checked`, `aria-label`, 44px touch target |
| PermissionDeniedBanner | `role="alert"` (assertive live region) |
| All buttons | `minHeight: 44px`, `minWidth: 44px` for touch targets |
| All icon-only buttons | `aria-label` present, icon has `aria-hidden="true"` |
| Push notifications (OS) | `tag` field set to prevent duplicate stacking |
| Surface | Requirement |
| ----------------------- | ---------------------------------------------------------------------------------- |
| PushPermissionPrompt | `role="dialog"`, `aria-modal="true"`, focus trap on open |
| SettingsSheet | `role="dialog"`, `aria-modal="true"`, Escape key to close, backdrop click to close |
| NotificationToggle | `role="switch"`, `aria-checked`, `aria-label`, 44px touch target |
| PermissionDeniedBanner | `role="alert"` (assertive live region) |
| All buttons | `minHeight: 44px`, `minWidth: 44px` for touch targets |
| All icon-only buttons | `aria-label` present, icon has `aria-hidden="true"` |
| Push notifications (OS) | `tag` field set to prevent duplicate stacking |
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable — no shadcn |
| Third-party | none | not applicable |
| Registry | Blocks Used | Safety Gate |
| --------------- | ----------- | -------------------------- |
| shadcn official | none | not applicable — no shadcn |
| Third-party | none | not applicable |
No third-party component registries. All components use the existing inline-style
pattern from the established codebase.