--- phase: 5 slug: web-push-notifications status: draft shadcn_initialized: false preset: none created: 2026-06-09 --- # Phase 5 — UI Design Contract > Visual and interaction contract for Phase 5: Web Push Notifications. > Generated by gsd-ui-researcher. Verified by gsd-ui-checker. > > **Brownfield note:** All tokens, patterns, and idioms are carried from the > established design system in `apps/pwa/src/styles/tokens.css` (Phase 2). > This phase adds three new UI surfaces — permission prompt, settings toggle > sheet, and permission-denied banner — all built from existing tokens. > No new visual language is introduced. --- ## 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)) | Source: `apps/pwa/src/styles/tokens.css`, `apps/pwa/src/styles/tokens.ts` --- ## Spacing Scale 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 | 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) Source: `apps/pwa/src/styles/tokens.css` — unchanged from Phase 2. --- ## Typography 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) | Source: `apps/pwa/src/styles/tokens.css` — unchanged from Phase 2. --- ## Color 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 | 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 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 - `--color-border` (#E2E4E9): banner bottom border, sheet borders, toggle border - `--color-overlay` (rgba(0,0,0,0.32)): sheet backdrop (matches WalkthroughSheet) - `--color-focus-ring` (#4A90D9): focus outline on all interactive elements Source: `apps/pwa/src/styles/tokens.css`. --- ## UI Surfaces Three new surfaces for this phase. All built from existing tokens and idioms. ### Surface 1: Post-Install Permission Prompt (D-08) **What it is:** A bottom sheet that appears immediately after PWA install is confirmed (or on first standalone launch). It replaces or follows the install walkthrough. Its job is to explain push notifications and trigger the OS permission request on a user tap. **Location:** Rendered inside `InstallPrompt.tsx` (or a sibling mounted in the 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` - `padding: var(--space-6)` - `boxShadow: 0 -4px 24px rgba(0,0,0,0.15)` - `zIndex: 1000` - Backdrop: `position: fixed; inset: 0; background: var(--color-overlay); zIndex: 999` - 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" [Body] "Get notified when events are coming up or your family makes changes." [Primary CTA] "Enable Notifications" — full-width, 48px min-height [Secondary] "Not now" — ghost text button, 44px min-height ``` **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) - Persisted: `localStorage.pushPermissionDismissed = '1'` when "Not now" tapped **localStorage keys:** `pushPermissionDismissed` — "Not now" persists, prompt does not re-show on next launch if dismissed. (Re-shows only if permission goes from `denied` → re-granted externally; silent re-subscribe handles that case per D-10.) --- ### Surface 2: Notification Settings Toggle (D-09) **What it is:** A single master on/off toggle for all FamilySync push notifications. Accessible from the user avatar in `AppNav.tsx` (both phone and desktop). **Location trigger:** The user avatar (`div` with `role="img"`, currently 44px tap area in `PhoneNav`) is promoted to a `