Files
familysync/.planning/phases/03-event-write-back-pwa-install/03-UI-SPEC.md
T
Lucas Berger 6f5110266d docs(03): UI design contract
Establishes Phase 3 visual and interaction contract for event write-back
(EventForm, SyncStateToast, DeleteConfirmationDialog, calendar picker) and
PWA install surfaces (iOS A2HS walkthrough, Android beforeinstallprompt).
Inherits Phase 2 token layer in full; activates --color-destructive for
delete actions.
2026-06-05 16:39:15 -04:00

479 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
phase: 3
slug: event-write-back-pwa-install
status: draft
shadcn_initialized: false
preset: none
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.
> Verified by gsd-ui-checker before execution begins.
>
> **Inheritance note:** Phase 3 inherits the full Phase 2 token layer
> (`apps/pwa/src/styles/tokens.css`) without modification. All tokens below
> are already committed in that file. This spec extends the Phase 2 contract
> with write-path UI (EventForm, sync-state feedback, calendar picker), PWA
> install surfaces (iOS walkthrough, Android prompt), and the destructive
> delete confirmation pattern.
---
## 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` |
No `components.json` exists. No shadcn initialization needed — the token layer
is sufficient for Phase 3's form and overlay surfaces.
---
## Token Layer (inherited — no new tokens required)
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 |
|-------|-----|---------------|
| `--color-destructive` | `#DC2626` | Delete button label + icon; destructive confirmation text |
No new CSS custom properties are introduced in Phase 3. Component styles reference
existing `--color-*`, `--space-*`, and `--text-*` tokens only.
---
## Color
_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 |
**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.
**Primary action button color:** `--color-text-primary` (#111318) background — a dark, neutral filled button. This is intentional: the accent palette is member-color-semantic. CTA buttons use the neutral dark fill, not an accent color.
---
## Spacing Scale
_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 |
**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).
---
## Typography
_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) |
**Weights declared:** 400 (regular) and 600 (semibold). No other weights.
**Form input text:** 15px body weight (400). This matches the body token and is large enough for comfortable iOS touch input.
**Error/helper text under inputs:** 13px label weight, `--color-destructive` for validation errors, `--color-text-muted` for neutral helpers.
---
## Component Inventory
### EventDetailPopover (extended — Phase 2 component)
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
**Footer layout:** flex row, space-between. Both buttons use the label type (13px/400).
### EventForm (new component)
Mounted as a modal overlay. On phone: full-screen bottom sheet (same pattern as EventDetailPopover). On tablet/desktop: centered dialog (max-width 480px, 8px radius, shadow).
**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 styling:**
- Input border: 1px solid `--color-border`; on focus: 2px `--color-focus-ring`, 2px offset
- Input border-radius: `--space-1` (4px)
- Background: `--color-surface`
- Label: 13px/400, `--color-text-secondary`, `--space-1` below label
- Input text: 15px/400, `--color-text-primary`
- 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`
- In-flight (after Save tapped, before API response): Save button shows Loader2 spinner (16px, `#FFFFFF`), disabled state. Label changes to "Saving…".
### CalendarPicker (within EventForm — conditional, D-02)
Hidden entirely when the member has exactly one writable calendar (prevents the non-technical member from ever seeing a choice that doesn't exist yet). Visible only when the `calendars` API returns more than one writable record.
### SyncStateToast (new component)
Displayed after a write operation is accepted (D-05). NOT a blocking modal.
**Position:** Bottom of screen, above the tab bar / nav area on phone; bottom-right on tablet/desktop. Persists until terminal state is reached.
**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` |
**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
- Auto-dismiss on `done` after 2 seconds. `failed`/`dead` states persist until dismissed (requires user tap).
- `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
### DeleteConfirmationDialog (new component)
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):
- "Cancel" — ghost button, `--color-text-secondary`, 44px height
- "Delete" — filled button, `--color-destructive` background, `#FFFFFF` label, Trash2 icon (16px), 48px height
No checkbox, no "don't ask again". Every delete requires explicit confirmation (two-person household, accidental deletes are high-cost).
### InstallPrompt (new component)
Single component handling both iOS and Android flows. Renders nothing when already installed (`display-mode: standalone`).
**iOS walkthrough (triggered by `isIOSSafariNonStandalone()`):**
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"
- Body (13px/400, `--color-text-secondary`): "Add to your Home Screen for the best experience."
- "How to install" button: text link style, 13px, `--color-focus-ring` blue, opens the full walkthrough sheet
- 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
2. "Tap the Share button" — annotated iOS screenshot (Share icon highlighted)
3. "Scroll down and tap 'Add to Home Screen'" — annotated iOS screenshot
4. "Tap 'Add' in the top right" — annotated iOS screenshot
5. "Open FamilySync from your Home Screen — it opens without the browser bar"
- Screenshot annotations: orange (#F5A623`--color-member-2`) highlight circle / arrow overlay on each screenshot
- "Done" button closes the sheet
**Android install prompt (triggered by `beforeinstallprompt`):**
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
---
## 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" |
**Destructive actions and confirmation patterns:**
| 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.
---
## Interaction Contract
### EventForm — open / close
- Create: tapped from a "New Event" FAB (floating action button, phone) or a toolbar button (tablet/desktop). Opens as bottom sheet (phone) or centered dialog (tablet/desktop).
- Edit: tapped from "Edit" in EventDetailPopover footer. EventDetailPopover closes, EventForm opens with fields pre-populated.
- Escape key (keyboard) or tap-backdrop: dismisses form. No confirmation required for unsaved new events. No confirmation required for unsaved edits (low-cost; user can re-open).
- Save: calls `POST /api/events/create` or `PATCH /api/events/:uid/edit`. Returns 202 immediately (D-05). SyncStateToast appears. Form closes.
### EventForm — all-day toggle behavior
- Toggling "All day" ON: hides start-time and end-time inputs. End date auto-advances to match start date if end date is before start date.
- Toggling "All day" OFF: shows time inputs with default values (start: 09:00, end: 10:00).
- No animation; show/hide is instantaneous.
### EventForm — calendar picker (conditional, D-02)
- Hidden when member has exactly 1 writable calendar.
- Shown when member has 2 writable calendars (personal + shared Family).
- Default selection: last-used calendar (D-01). First-time default: personal calendar.
- Calendar move (edit mode): if user changes the calendar selection, the API handler produces a delete-from-old + create-on-new pair (D-04). No special UI — the form treats it as a normal save.
### EventForm — recurrence
- Default: "None" (selected).
- Selecting any recurrence preset applies a whole-series RRULE (D-11).
- In edit mode on a recurring event: recurrence field shows the current RRULE preset (if it maps to a simple preset) or "Custom" (read-only, if the existing RRULE doesn't match any preset). Custom RRULE editing is not available in v1.
- No "edit this occurrence / edit all" fork in v1 (D-11 / CAL-09 deferred).
### Sync-state feedback
- After Save: form closes immediately. SyncStateToast shows "Syncing…" with spinner.
- TanStack Query polls `/api/events/sync-status?uid={uid}` at 3-second intervals while status is `pending`.
- On `done`: toast updates to "Saved" (Check icon), auto-dismisses after 2 seconds. `queryClient.invalidateQueries(['events'])` fires to refresh the calendar view.
- On `failed` / `dead`: toast updates to error state, persists until user dismisses. Calendar is NOT refreshed (optimistic event may still show — this is intentional; the user sees what they intended and can retry or dismiss).
- Conflict (`failed` with 412 message): calendar refreshes via `invalidateQueries(['events'])` to show the actual server state. Toast shows conflict copy.
### Delete interaction
1. User taps "Delete" in EventDetailPopover footer.
2. EventDetailPopover remains open; DeleteConfirmationDialog appears above it (z-index higher).
3. User taps "Cancel": dialog closes, popover resumes.
4. User taps "Delete" (red): dialog and popover both close. API call fires (`DELETE /api/events/:uid`). SyncStateToast shows "Syncing…". Calendar optimistically removes the event.
5. On sync `done`: toast auto-dismisses. Event confirmed gone.
6. On sync `failed`: toast shows error. Event MAY reappear in calendar on next refetch (server-authoritative state wins). No silent loss.
### iOS install walkthrough
- Banner is shown once per device per user (localStorage flag `installPromptDismissed`).
- Banner is never shown when `window.matchMedia('(display-mode: standalone)').matches` is true.
- Banner is never shown on non-iOS devices (Android and desktop get the `beforeinstallprompt` flow or nothing).
- Tapping "How to install" opens the walkthrough sheet (full-screen bottom sheet, not a new page).
- Walkthrough sheet has a close button (X, top-right, 44px) and a "Done" button at the bottom.
- Dismissing the banner (X) records the flag and hides the banner permanently. The walkthrough remains accessible from a "?" / "Install" link in AppNav settings (if future phases add a settings surface) — for Phase 3, the banner is the only trigger.
### Android install
- Install banner appears only when `canInstall === true` (the `beforeinstallprompt` event fired).
- Never shown on iOS or desktop.
- Tapping "Install" calls the deferred prompt. On `accepted`: banner disappears permanently, `appinstalled` event fires. On `dismissed`: banner hides for the session (not permanently — the event may re-fire on a future visit).
### Touch targets
All interactive elements in Phase 3 new surfaces: minimum 44×44px. Enforced via `min-height: 44px` and `padding` where needed. Applies to: form buttons, all-day toggle, recurrence options, calendar picker, delete confirmation buttons, install banner buttons, walkthrough step close/done.
### Keyboard / accessibility
- EventForm: focus moves to the Title input when the form opens. Tab order follows DOM order (title → all-day → start date → [start time] → end date → [end time] → [calendar] → recurrence → location → description → cancel → save).
- EventForm: `role="dialog"`, `aria-modal="true"`, `aria-label="New Event"` / `"Edit Event"`.
- Focus trap inside EventForm and DeleteConfirmationDialog while open.
- Escape closes EventForm (no confirmation). Escape closes DeleteConfirmationDialog without deleting.
- All-day toggle: `role="switch"`, `aria-checked`, keyboard-activatable with Space.
- Recurrence picker: `role="radiogroup"` with `role="radio"` options or a `<select>` — either is acceptable.
- Delete button: `aria-label="Delete event"`.
- SyncStateToast: `role="status"` (polite live region) for `pending`/`done`; `role="alert"` (assertive) for `failed`/`dead`.
- Install banner: `role="banner"` (or `role="complementary"`). Dismiss button: `aria-label="Dismiss install prompt"`.
---
## State Management Contract
_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 |
---
## 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 |
**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">
```
**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
---
## 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 |
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 |
---
## 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