From 3723286e9ee8e13151796bb8a50a5bd78f5dc93d Mon Sep 17 00:00:00 2001 From: Lucas Berger Date: Tue, 9 Jun 2026 18:03:18 -0400 Subject: [PATCH] docs(05): UI design contract for web-push-notifications phase Specifies three new surfaces (permission prompt, settings sheet, permission-denied banner) and notification copy templates for NOTIF-01/02/03, all built from the existing Phase 2 token system. --- .../05-web-push-notifications/05-UI-SPEC.md | 492 ++++++++++++++++++ 1 file changed, 492 insertions(+) create mode 100644 .planning/phases/05-web-push-notifications/05-UI-SPEC.md diff --git a/.planning/phases/05-web-push-notifications/05-UI-SPEC.md b/.planning/phases/05-web-push-notifications/05-UI-SPEC.md new file mode 100644 index 0000000..0bd949a --- /dev/null +++ b/.planning/phases/05-web-push-notifications/05-UI-SPEC.md @@ -0,0 +1,492 @@ +--- +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 `