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.
This commit is contained in:
@@ -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 `<button>` that opens a Settings bottom sheet.
|
||||
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`
|
||||
- `background: var(--color-surface-raised)` (#FFFFFF)
|
||||
- `borderRadius: 12px 12px 0 0`
|
||||
- `padding: var(--space-6)`
|
||||
- `zIndex: 301` (matches CreateListSheet z-index layer)
|
||||
- Backdrop at `zIndex: 300`, click to close
|
||||
|
||||
**Sheet contents:**
|
||||
```
|
||||
[Heading row]
|
||||
"Settings" (font: heading 18px/600)
|
||||
[X button, 44px touch target, aria-label="Close settings"]
|
||||
|
||||
[Section label]
|
||||
"Notifications" (font: label 13px/600, --color-text-muted, uppercase, letter-spacing 0.06em)
|
||||
(matches the "Calendars" section label idiom in DesktopNav)
|
||||
|
||||
[Toggle row]
|
||||
[Bell icon, 20px, --color-text-secondary, aria-hidden]
|
||||
[Column]
|
||||
"FamilySync Notifications" (font: body 15px/400, --color-text-primary)
|
||||
"Reminders, event changes, list updates" (font: label 13px/400, --color-text-secondary)
|
||||
[Toggle switch, right-aligned]
|
||||
on: track #4A90D9 (var(--color-member-0)), thumb #FFFFFF, 44px touch target
|
||||
off: track #E2E4E9 (var(--color-border)), thumb #FFFFFF
|
||||
disabled (when Notification.permission === 'denied'): track #E2E4E9, opacity 0.5
|
||||
aria-checked, role="switch", aria-label="FamilySync Notifications"
|
||||
|
||||
[Permission-denied hint — only when Notification.permission === 'denied']
|
||||
[AlertCircle icon, 16px, --color-destructive]
|
||||
"Notifications are blocked in your browser settings." (font: label 13px/400, --color-text-secondary)
|
||||
"How to enable" (inline link, --color-focus-ring, underline, opens OS settings or shows instructions)
|
||||
```
|
||||
|
||||
**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
|
||||
|
||||
---
|
||||
|
||||
### Surface 3: Permission-Denied Banner (D-10)
|
||||
|
||||
**What it is:** A persistent non-dismissible inline banner shown at the top of
|
||||
the app (below AppNav/BottomTabBar, above content) when `Notification.permission
|
||||
=== 'denied'` and the user previously had notifications enabled.
|
||||
|
||||
**When shown:** Only when OS permission is `'denied'` AND `localStorage.notificationsEnabled`
|
||||
was previously `'1'`. Silent re-subscribe covers expired subscriptions (D-10) — this
|
||||
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)`
|
||||
- `background: var(--color-surface-raised)`
|
||||
- `borderBottom: 1px solid var(--color-border)`
|
||||
- `fontFamily: var(--font-family-base)`
|
||||
|
||||
**Contents:**
|
||||
```
|
||||
[AlertCircle icon, 24px, --color-destructive, aria-hidden]
|
||||
[Column, flex: 1]
|
||||
"Notifications blocked" (font: label 13px/600, --color-text-primary)
|
||||
"Re-enable in your browser settings." (font: label 13px/400, --color-text-secondary)
|
||||
+ " How to enable" (inline button/link, --color-focus-ring, underline)
|
||||
```
|
||||
|
||||
No dismiss button — the banner persists until OS permission is restored. (The user
|
||||
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
|
||||
3. Tap Notifications
|
||||
4. Allow notifications for FamilySync
|
||||
- Android/Chrome: links to `chrome://settings/content/notifications` cannot be linked directly; show a sheet with instructions to open Chrome Settings → Site Settings → Notifications → Allow FamilySync.
|
||||
|
||||
---
|
||||
|
||||
## Notification Content Contract
|
||||
|
||||
This is not a UI surface but defines the exact string templates that the push
|
||||
notification payload must match. The executor must use these templates verbatim
|
||||
in the server-side push dispatch.
|
||||
|
||||
### Event reminder (NOTIF-01)
|
||||
|
||||
```
|
||||
title: "{EventTitle}"
|
||||
body: "Starts in 15 min"
|
||||
tag: "reminder-{eventUid}"
|
||||
data: { url: "/calendar?date={YYYY-MM-DD}&event={eventUid}" }
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
title: "Dentist"
|
||||
body: "Starts in 15 min"
|
||||
```
|
||||
|
||||
### Event change — new event (NOTIF-03, new)
|
||||
|
||||
```
|
||||
title: "{ActorName} added an event"
|
||||
body: "{EventTitle} · {formattedTime}"
|
||||
tag: "event-change-{eventUid}"
|
||||
data: { url: "/calendar?date={YYYY-MM-DD}&event={eventUid}" }
|
||||
```
|
||||
|
||||
Example:
|
||||
```
|
||||
title: "Lucas added an event"
|
||||
body: "Soccer practice · Wed 3 pm"
|
||||
```
|
||||
|
||||
### Event change — modified event (NOTIF-03, change)
|
||||
|
||||
```
|
||||
title: "{ActorName} updated an event"
|
||||
body: "{EventTitle} · {formattedTime}"
|
||||
tag: "event-change-{eventUid}"
|
||||
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}"
|
||||
tag: "event-change-{eventUid}"
|
||||
data: { url: "/calendar" }
|
||||
```
|
||||
|
||||
### List change (NOTIF-02, coalesced per D-01/D-02/D-03)
|
||||
|
||||
```
|
||||
title: "{ActorName} updated {ListName}"
|
||||
body: "{N} change{s}"
|
||||
tag: "list-change-{listId}"
|
||||
data: { url: "/lists/{listId}" }
|
||||
```
|
||||
|
||||
Examples:
|
||||
```
|
||||
title: "Lucas updated Groceries"
|
||||
body: "3 changes"
|
||||
|
||||
title: "Lucas updated Groceries"
|
||||
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)
|
||||
|
||||
---
|
||||
|
||||
## Tap-to-Open Deep Links (D-14)
|
||||
|
||||
| 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}` |
|
||||
|
||||
The `notificationclick` service-worker handler calls `clients.openWindow(event.notification.data.url)`.
|
||||
|
||||
---
|
||||
|
||||
## 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" |
|
||||
|
||||
No destructive actions in this phase. The toggle is not destructive — it silently
|
||||
unregisters the push subscription without a confirmation dialog.
|
||||
|
||||
---
|
||||
|
||||
## Interaction States
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
### 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 |
|
||||
|
||||
---
|
||||
|
||||
## Z-Index Layering
|
||||
|
||||
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) |
|
||||
|
||||
Permission prompt uses `zIndex: 999` for backdrop, `zIndex: 1000` for sheet — matching
|
||||
the existing `WalkthroughSheet` in `InstallPrompt.tsx`.
|
||||
|
||||
---
|
||||
|
||||
## Component Inventory
|
||||
|
||||
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 |
|
||||
|
||||
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`
|
||||
|
||||
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 |
|
||||
|
||||
---
|
||||
|
||||
## Registry Safety
|
||||
|
||||
| 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.
|
||||
|
||||
---
|
||||
|
||||
## Checker Sign-Off
|
||||
|
||||
- [ ] Dimension 1 Copywriting: PASS
|
||||
- [ ] Dimension 2 Visuals: PASS
|
||||
- [ ] Dimension 3 Color: PASS
|
||||
- [ ] Dimension 4 Typography: PASS
|
||||
- [ ] Dimension 5 Spacing: PASS
|
||||
- [ ] Dimension 6 Registry Safety: PASS
|
||||
|
||||
**Approval:** pending
|
||||
Reference in New Issue
Block a user