Files
familysync/.planning/milestones/v1.1-phases/11-per-event-reminders/11-UI-SPEC.md
T
2026-06-18 22:21:38 -04:00

9.8 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
11 per-event-reminders draft false none 2026-06-13

Phase 11 — UI Design Contract

Visual and interaction contract for frontend phases. Generated by gsd-ui-researcher, verified by gsd-ui-checker.


Design System

Property Value
Tool none — custom CSS tokens (tokens.css), no shadcn
Preset not applicable
Component library none — inline React.CSSProperties style objects
Icon library lucide-react (already in use: X, Loader2)
Font system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif

Source: apps/pwa/src/styles/tokens.css, apps/pwa/src/components/EventForm.tsx


Spacing Scale

Declared values (must be multiples of 4). Pre-populated from tokens.css.

Token Value Usage
--space-1 4px Icon gaps, label margin-bottom, helper-text margin-top
--space-2 8px Compact element spacing
--space-3 12px Select horizontal padding (matching Recurrence picker)
--space-4 16px Field margin-bottom (fieldStyle), general element spacing
--space-6 24px Section padding
--space-8 32px Layout gaps
--space-12 48px Major section breaks

Exceptions: 44px minimum touch target height on the reminder <select> (matches minHeight: '44px' already declared on inputStyle).


Typography

Pre-populated from tokens.css. No new type ramp needed — reminder picker reuses existing roles.

Role Size Weight Line Height Usage in this phase
Body 15px (--text-body-size) 400 (--text-body-weight) 1.5 Select option text, helper text values
Label 13px (--text-label-size) 400 (--text-label-weight) 1.4 "Reminder" field label, "Custom (kept)" hint text, error copy
Heading 18px (--text-heading-size) 600 (--text-heading-weight) 1.25 Not directly used by this phase
Display 24px (--text-display-size) 600 (--text-display-weight) 1.2 Not directly used by this phase

Only 2 weights in use: 400 (regular) and 600 (semibold).


Color

Pre-populated from tokens.css. No new tokens introduced in this phase.

Role Value Usage
Dominant (60%) --color-surface: #ffffff Form background, select background
Secondary (30%) --color-surface-dim: #f7f7f8 Modal backdrop surface (already used)
Accent (10%) --color-focus-ring: #4a90d9 Focus ring on reminder select when keyboard-navigated
Destructive --color-destructive: #dc2626 Validation error copy if picker submission fails

Accent reserved for: focus ring on the reminder <select> only. Calendar member colors (--color-member-*) and shared family color (--color-shared-family) are unchanged and not used by the reminder picker UI.


Component Inventory

This phase adds exactly one new UI element to EventForm.tsx. All styling follows the established inline React.CSSProperties pattern from the Recurrence picker.

Reminder <select> (new in Phase 11)

  • Placement: After the Recurrence <select> block (~line 885 in EventForm.tsx), before the recurrence-bound control.
  • Label text: Reminder
  • Element ID: event-reminder
  • Pattern: Identical to the Recurrence picker — <div style={fieldStyle}>, <label htmlFor="event-reminder" style={labelStyle}>, <select style={{...inputStyle, padding: '0 var(--space-3)', cursor: 'pointer'}}>

Timed-event options (shown when !allDay)

Option value Display label
null None
5 5 minutes before
10 10 minutes before
15 15 minutes before
30 30 minutes before
60 1 hour before
120 2 hours before
1440 1 day before
2880 2 days before

All-day options (shown when allDay, swapped — D-02/D-03)

Option value Display label
null None
0 Same day (9 AM)
1440 1 day before (9 AM)
2880 2 days before (9 AM)
10080 1 week before (9 AM)

Default: null (None) for both cases — D-01.

Off-preset / Custom alarm handling (D-07/D-08)

  • Single simple relative alarm not in preset list: Render a synthetic option "N min before" or "N hours before" (humanized, see Copywriting table) appended to the option list, selected by default. If user changes selection, the synthetic option is removed.
  • Absolute-time trigger or multiple alarms: Render a read-only disabled <option value="__custom__">Custom (kept)</option> selected by default. The <select> itself is NOT disabled — the user can still choose a preset, which replaces the custom alarm.
  • Preserve on no-change: When picker value remains "__custom__" or the synthetic off-preset option on save, pass a sentinel (e.g. "no-change") in the payload so the outbox worker leaves the original VALARM intact.

Helper text (shown below select, identical style to WR-01 helper)

Style: { fontSize: 'var(--text-label-size)', color: 'var(--color-text-secondary)', marginTop: 'var(--space-1)' }

Shown only when eventFormMode === 'edit' and the event has a custom alarm (either off-preset single or multi/absolute):

"Custom reminder kept — select a preset to replace it."


Notification Push Copy (D-09)

The humanized body replaces the hardcoded Starts in ${minutes} min in reminderScheduler.ts line 157.

Humanized thresholds and wording

Lead window Body copy
< 60 minutes Starts in {N} min (e.g. "Starts in 30 min")
60119 minutes Starts in 1 hour
1201439 minutes Starts in {N} hours (e.g. "Starts in 2 hours")
14402879 minutes (1 day) Starts in 1 day
≥ 2880 minutes Starts in {N} days (e.g. "Starts in 2 days")

All-day events with day-granularity leads use the same thresholds (converted from minutes). "1 week before" → 10080 min → "Starts in 7 days".

Push notification title is unchanged from the current implementation (event title).


Copywriting Contract

Element Copy
Reminder field label Reminder
Picker default option None
Timed preset — 5m 5 minutes before
Timed preset — 10m 10 minutes before
Timed preset — 15m 15 minutes before
Timed preset — 30m 30 minutes before
Timed preset — 1h 1 hour before
Timed preset — 2h 2 hours before
Timed preset — 1d 1 day before
Timed preset — 2d 2 days before
All-day preset — same day Same day (9 AM)
All-day preset — 1d before 1 day before (9 AM)
All-day preset — 2d before 2 days before (9 AM)
All-day preset — 1wk before 1 week before (9 AM)
Off-preset single alarm helper text Custom reminder kept — select a preset to replace it.
Custom (kept) option label Custom (kept)
Push notification — < 60 min Starts in {N} min
Push notification — exactly 1 hour Starts in 1 hour
Push notification — N hours Starts in {N} hours
Push notification — exactly 1 day Starts in 1 day
Push notification — N days Starts in {N} days

No empty state: the reminder picker always shows options (default None). No destructive actions in this phase — reminder selection is non-destructive; unsaved changes are discarded on form close (same as all other EventForm fields).

Error state: If the event PUT fails (network/server error), the existing EventForm error toast pattern handles it — no reminder-specific error copy needed.


Interaction Contract

State machine for the picker

allDay = false → show timed presets (None / 5m / 10m / 15m / 30m / 1h / 2h / 1d / 2d)
allDay = true  → swap to all-day presets (None / Same day / 1d before / 2d before / 1wk before)
allDay toggles → reset picker to None (do not carry over a timed value to all-day or vice versa)

Edit mode: loading existing reminder

reminderLeadMinutes from occurrence:
  null          → select "None" (default)
  matches preset → select matching option
  number not in preset list (and single simple relative VALARM) → add synthetic option, select it
  absolute/multi alarm → add "Custom (kept)" option (disabled), select it; show helper text

Accessibility

  • id="event-reminder" on the <select>, htmlFor="event-reminder" on the label (matches Recurrence pattern exactly).
  • <select> participates in the existing focus trap (no special handling needed — it is a native focusable element).
  • The "Custom (kept)" <option> uses disabled attribute to prevent re-selection after the user picks a preset, but the <select> itself remains enabled.
  • Minimum touch target: 44px height via minHeight: '44px' on inputStyle (inherited, already declared).

No new UI surfaces

This phase adds no new modals, sheets, toasts, or pages. All changes are:

  1. One new <select> field inside the existing EventForm.tsx.
  2. One optional helper text <div> below the select (edit mode + custom alarm only).
  3. Push notification body copy change in reminderScheduler.ts (server-side, no UI surface).

Registry Safety

Registry Blocks Used Safety Gate
shadcn official none — shadcn not initialized not applicable
third-party none not applicable

No third-party component registry blocks used. Phase 11 adds no new npm dependencies on the frontend. All UI is composed from native HTML elements styled with existing project tokens.


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