Files
familysync/.planning/milestones/v1.1-phases/10-admin-role-settings/10-UI-SPEC.md
T
2026-06-18 22:21:38 -04:00

14 KiB

phase, slug, status, shadcn_initialized, preset, created
phase slug status shadcn_initialized preset created
10 admin-role-settings draft false none 2026-06-13

Phase 10 — Admin Role & Settings UI Design Contract

Visual and interaction contract for the admin role, credential management, shared-calendar designation, and member self-service credential onboarding surfaces. Generated by gsd-ui-researcher.


Design System

Property Value
Tool none — custom CSS variables only
Preset not applicable
Component library none (hand-authored inline styles, var(--token) pattern throughout)
Icon library lucide-react 1.17.0
Font system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif (var(--font-family-base))

Source: apps/pwa/src/styles/tokens.css (Phase 2, D-01/D-02). No shadcn, no Tailwind — all values are CSS custom properties declared in tokens.css and referenced inline.


Spacing Scale

All spacing must reference var(--space-N) tokens, never hard-coded px. Multiples of 4px.

Token Value Usage
--space-1 4px Icon gaps, tight inline padding, border-radius on small elements
--space-2 8px Compact row padding, section-label bottom margin, badge gap
--space-3 12px Row internal padding (toggle rows, member rows), gap between icon and label
--space-4 16px Input horizontal padding, button horizontal padding, nav horizontal padding
--space-6 24px Sheet/page padding, heading bottom margin, section separation
--space-8 32px Major layout gaps between sections
--space-12 48px Page-level top/bottom breathing room on the /admin route

Exceptions: 44px minimum touch target on all interactive elements (buttons, radio pills, credential rows) — applied as minWidth: 44px; minHeight: 44px inline, not a spacing token. Destructive confirm button uses minHeight: 48px per the existing DeleteConfirmationDialog precedent.


Typography

All values reference existing var(--text-*) tokens from tokens.css. No new sizes or weights.

Role Size Weight Line Height Usage in this phase
Body 15px (var(--text-body-size)) 400 (var(--text-body-weight)) 1.5 (var(--text-body-line-height)) Member name, credential status description, instruction copy, field labels
Label 13px (var(--text-label-size)) 400 (var(--text-label-weight)) 1.4 (var(--text-label-line-height)) Section headers (uppercased), status badges, calendar picker option text, helper text under password field
Heading 18px (var(--text-heading-size)) 600 (var(--text-heading-weight)) 1.25 (var(--text-heading-line-height)) Page heading "Admin Settings", sheet headings ("Rotate Credential", "Set Shared Calendar"), confirmation dialog heading
Display 24px (var(--text-display-size)) 600 (var(--text-display-weight)) 1.2 (var(--text-display-line-height)) Not used in this phase — reserved for app name in AppNav

Section labels (e.g. "MEMBERS", "SHARED CALENDAR") follow the established SettingsSheet pattern: 13px / weight 600 / var(--color-text-muted) / textTransform: uppercase / letterSpacing: 0.06em.


Color

All values reference existing tokens from tokens.css. No new colors introduced.

Role Value Usage
Dominant (60%) #ffffff (var(--color-surface)) /admin page background, sheet backgrounds, dialog backgrounds
Secondary (30%) #f7f7f8 (var(--color-surface-dim)) Member rows background on hover/focus, info/hint banners, credential-setup onboarding card
Accent (10%) #4a90d9 (var(--color-member-0)) Save/confirm CTA buttons, active radio selection border, focus ring (var(--color-focus-ring)), selected calendar radio indicator
Destructive #dc2626 (var(--color-destructive)) "Remove credential" destructive action button only

Accent reserved for: Save button ("Save Credential"), selected-calendar radio indicator, text links ("Get an app password" → Fastmail doc link), focus ring on inputs and interactive elements. Never used for nav chrome, page background, or passive text.

Status indicators (member credential state) use the established muted/secondary palette, not accent:

  • Credential set: var(--color-text-secondary) + a check icon (lucide CheckCircle, size 16)
  • No credential: var(--color-text-muted) + warning icon (lucide AlertCircle, size 16)

Surfaces & Interaction Patterns

Surface 1 — /admin Route (full page)

A dedicated route at /admin, gated by isAdmin from /api/me. Non-admin users redirected to /calendar immediately on mount.

Layout matches the existing app shell: AppNav persistent sidebar (desktop) or top bar (mobile), BottomTabBar (mobile). The /admin content area uses var(--color-surface) background with var(--space-12) top/bottom padding and var(--space-6) horizontal padding on mobile.

Desktop: content in a centered column, maxWidth: 640px, margin: 0 auto.

The admin route does NOT appear in AppNav nav links by default. Entry point: a new "Admin" NavLink in the desktop sidebar and a new tab in the mobile BottomTabBar, rendered only when meQuery.data?.isAdmin === true. Use lucide ShieldCheck icon (size 18) for the Admin nav entry, matching the CalendarDays/List pattern in AppNav.

Surface 2 — Member Credential List Section

A section within /admin labeled "MEMBERS" (section-label style).

Each member renders as a row:

  • Avatar color swatch (32px circle, var(--color-member-N)) + member display name at body size
  • Credential status badge at label size: "Credential set" (muted green check) or "No credential" (muted warning)
  • An action button: "Rotate" (if credential exists) or "Add credential" (if none)
  • Entire row: minHeight: 44px, background var(--color-surface), bottom border 1px solid var(--color-border-subtle)

Clicking "Rotate" or "Add credential" opens Surface 3 (credential sheet) for that member.

Admin can rotate any member's credential. Members can only manage their own (self-service path via Surface 4 — same sheet, member-scoped API endpoint).

Surface 3 — Credential Sheet (admin-managed rotation)

A bottom sheet on mobile (same pattern as SettingsSheet: role="dialog", aria-modal, zIndex 301, backdrop zIndex 300, borderRadius: 12px 12px 0 0, padding: var(--space-6)). Centered modal on desktop (maxWidth: 480px).

Contents:

  1. Heading: "Rotate Credential" (if existing) or "Add Credential" (if none) — 18px/600
  2. Member name as subtitle — 15px/400/var(--color-text-secondary)
  3. Password field (type="password", autocomplete="new-password"):
    • Label: "App password" — 13px/600/var(--color-text-primary)
    • Input: full-width, padding: var(--space-3) var(--space-4), border: 1px solid var(--color-border), borderRadius: var(--space-1), fontSize: var(--text-body-size), color: var(--color-text-primary), background: var(--color-surface). Error state border: var(--color-destructive).
    • Never pre-filled; never echoed back after save.
  4. Helper text below field — 13px/400/var(--color-text-secondary): "Enter the Fastmail app password scoped to Calendars/CalDAV. Get an app password — choose the 'Calendars & Contacts (CalDAV)' scope." The link opens in a new tab (target="_blank" rel="noopener noreferrer").
  5. Validation: "Validating against CalDAV…" inline status (13px, muted, spinner Loader2 size 16) replaces helper text during the PROPFIND call. On failure: red 13px error below field ("Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again."). On success: sheet closes.
  6. Actions row (right-aligned, gap var(--space-3)):
    • Cancel: ghost button (background none, 13px/600/var(--color-text-secondary), minHeight 44px)
    • Save Credential: filled accent button (var(--color-member-0) background, #ffffff text, 13px/600, minHeight 44px, padding: 0 var(--space-4), borderRadius: var(--space-1)). Disabled while field empty or during validation.

Surface 4 — Self-Service Credential Onboarding (member-scoped)

Triggered when a member with needsProviderSetup: true (from /api/me) loads the app. Rendered as a dismissable banner or inline card above the calendar content — NOT a modal (user should be able to continue using the app without completing it immediately).

Card/banner style: background: var(--color-surface-dim), border: 1px solid var(--color-border), borderRadius: var(--space-2), padding: var(--space-4), margin: var(--space-4).

Contents:

  • Icon: lucide KeyRound size 20, var(--color-member-0)
  • Heading: "Set up your calendar" — 15px/600/var(--color-text-primary)
  • Body: "To sync your Fastmail calendar, you need to add an app password. This takes about a minute." — 13px/400/var(--color-text-secondary)
  • CTA button: "Set up now" — same accent-filled style as Surface 3 Save button, minHeight 44px

Clicking "Set up now" opens the same credential sheet (Surface 3) but scoped to the current user only, with heading "Add your calendar credential" and simplified copy ("App password for your Fastmail account").

The banner has no X/dismiss button — it stays visible until the credential is successfully saved (needsProviderSetup becomes false after save).

Surface 5 — Shared Calendar Picker Section

A section within /admin labeled "SHARED CALENDAR" (section-label style), below the Members section.

Body copy (15px/400/secondary): "The shared family calendar is visible to all members in the same color lane."

Radio group — one row per synced calendar:

  • Each row: minHeight: 44px, flexbox, gap: var(--space-3), padding: var(--space-2) 0
  • Radio indicator: a 20px circle — unfilled with 2px solid var(--color-border) when unselected; filled with var(--color-member-0) + inner 8px white dot when selected
  • Calendar name at body size (15px/400/var(--color-text-primary))
  • "Currently shared" label (13px/400/var(--color-member-0)) on the currently active selection

Selection is exclusive single-select (D-06). Selecting a row immediately highlights it; a "Save" button below the list confirms the write (two-tap, prevents accidental mis-selection).

Save button: full-width on mobile, right-aligned on desktop; accent-filled style, minHeight: 44px. Disabled until selection differs from current saved value.

If no calendars have been synced yet: empty state (see Copywriting Contract below).


Copywriting Contract

Element Copy
Admin nav label "Admin"
/admin page heading "Admin Settings"
Members section label "MEMBERS"
Shared calendar section label "SHARED CALENDAR"
Credential status — set "Credential set"
Credential status — missing "No credential"
Admin credential row CTA — existing "Rotate"
Admin credential row CTA — none "Add credential"
Credential sheet heading — admin rotation (existing) "Rotate Credential"
Credential sheet heading — admin add (none) "Add Credential"
Credential sheet heading — self-service "Add your calendar credential"
Credential sheet member subtitle "{DisplayName}"
Password field label "App password"
Password field helper text "Enter the Fastmail app password scoped to Calendars/CalDAV. [Get an app password] — choose the 'Calendars & Contacts (CalDAV)' scope."
CalDAV validation in-progress "Validating against CalDAV…"
CalDAV validation failure "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again."
Save credential button "Save Credential"
Cancel button "Cancel"
Self-service banner heading "Set up your calendar"
Self-service banner body "To sync your Fastmail calendar, you need to add an app password. This takes about a minute."
Self-service CTA button "Set up now"
Shared calendar save button "Save"
Shared calendar currently-active label "Currently shared"
Shared calendar empty state heading "No calendars synced yet"
Shared calendar empty state body "Calendars sync automatically. Check back after the first sync completes."
Calendars picker loading "Loading calendars…"
Generic save error "Something went wrong. Please try again."
403 non-admin redirect (silent redirect — no error copy shown to the non-admin user)

Destructive actions in this phase:

Action Trigger Confirmation approach
Rotating/replacing a credential Tapping "Rotate" then "Save Credential" with a new value Two-step: open sheet (step 1) + explicit "Save Credential" tap (step 2). No separate confirmation dialog — overwrite is acknowledged by the user filling and submitting the new value. The existing credential is never displayed; losing it is not destructive (a new one replaces it).

No hard-delete of credentials in this phase. "Remove credential" is listed as the destructive color token usage but the action itself is deferred — only add/rotate is in scope for Phase 10.


Accessibility Contracts

  • All interactive elements: minWidth: 44px; minHeight: 44px (WCAG 2.5.5 Target Size).
  • Credential sheet: role="dialog", aria-modal="true", aria-label matching the heading, Escape closes.
  • Password input: type="password", autocomplete="new-password", never autocomplete="current-password".
  • Radio group for calendar picker: each row has role="radio" or wraps a native <input type="radio"> in a visually styled label; aria-checked on custom implementations.
  • Admin nav entry: aria-label="Admin settings" on the NavLink/button.
  • Self-service banner: role="status" or aria-live="polite" so screen readers announce it on load.
  • Validation error messages: associated to their input via aria-describedby.
  • Focus returns to the trigger element when a sheet closes.

Registry Safety

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

No third-party component registries. All components hand-authored using the existing inline-style pattern. No new npm dependencies for UI are required beyond lucide-react (already installed at 1.17.0).


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