Files
familysync/.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md
T
Lucas BergerandClaude Sonnet 4.6 0f3c3784e6 docs(12): revise UI-SPEC for collect-and-write wizard model
Drop Step 2 "Generate Secrets" (secrets move to pre-boot helper script per D-05),
rework Step 2 to collect non-secret config via form fields (app URL, OIDC issuer,
client_id, VAPID public key) written to app_config per D-02, and reduce wizard
from 5 steps to 4. All design tokens, surfaces, and a11y contracts unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-15 13:09:25 -04:00

25 KiB
Raw Blame History

phase, slug, status, shadcn_initialized, preset, created, updated
phase slug status shadcn_initialized preset created updated
12 initial-setup-wizard draft false none 2026-06-14 2026-06-15

Phase 12 — UI Design Contract: Initial Setup Wizard

Visual and interaction contract for the first-run setup wizard. Generated by gsd-ui-researcher. Verified by gsd-ui-checker.

Revision note (2026-06-15): CONTEXT.md (D-04/D-05/D-06) supersedes the original validate-only model. Step 2 "Generate Secrets" is dropped (generation is pre-boot via repo helper script). Steps 34 are reworked to collect config via input fields, not just validate env. All other design tokens, surfaces, and a11y contracts are unchanged.


Context & Audience

This wizard is operator-facing, not end-user-facing. The operator is the person standing up the self-hosted FamilySync instance on Unraid — technical enough to edit docker-compose.yml, but not necessarily a developer. The wizard renders for an unauthenticated visitor (it is the one screen in the app shown outside the Authelia/OIDC guard). It must be calm, legible, and unintimidating.

This is the only full-page standalone UI in the app. It does NOT render inside the existing AppNav + BottomTabBar shell. It owns its entire viewport.

All design tokens are inherited from the existing system (apps/pwa/src/styles/tokens.css). No new tokens are introduced.


Design System

Property Value
Tool none (existing CSS custom properties)
Preset not applicable
Component library none (hand-rolled inline styles, project convention)
Icon library lucide-react (already installed — KeyRound, CheckCircle, AlertCircle, Loader2, Copy, Check, ShieldCheck)
Font system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif (var(--font-family-base))

Source: apps/pwa/src/styles/tokens.css — pre-populated from existing codebase.


Spacing Scale

Uses the existing 4px-based scale. No new tokens. Values from tokens.css:

Token Value Usage
--space-1 4px Icon gaps, label-to-input gap
--space-2 8px Compact element spacing, badge gap
--space-3 12px Input padding (vertical), row gaps
--space-4 16px Default element spacing, card padding, input padding (horizontal)
--space-6 24px Sheet/card padding, section gap
--space-8 32px Between major sections
--space-12 48px Page top/bottom padding (AdminPage pattern)

Exceptions:

  • Wizard card max-width: 540px (slightly wider than CredentialSheet 480px to accommodate multi-field steps).
  • Step indicator touch targets: 44px minimum (accessibility).

Typography

All values from tokens.css. No new sizes or weights.

Role Size Weight Line Height Variable
Body 15px 400 1.5 var(--text-body-size) / var(--text-body-weight) / var(--text-body-line-height)
Label 13px 400 1.4 var(--text-label-size) / var(--text-label-weight) / var(--text-label-line-height)
Heading 18px 600 1.25 var(--text-heading-size) / var(--text-heading-weight) / var(--text-heading-line-height)
Display 24px 600 1.2 var(--text-display-size) / var(--text-display-weight) / var(--text-display-line-height)

Usage in this phase:

  • Wizard page title ("FamilySync Setup"): Display (24px/600/1.2)
  • Step heading (e.g. "OIDC & App URL"): Heading (18px/600/1.25)
  • Step description / helper text: Body (15px/400/1.5)
  • Field labels, step counter, status badges: Label (13px/400/1.4) — labels use weight 600
  • Section labels (uppercase caps, e.g. "STEP 2 OF 4"): Label (13px/600) with text-transform: uppercase; letter-spacing: 0.06em (AdminPage sectionLabelStyle pattern)

Color

All values from tokens.css. No new hex values.

Role Value Variable Usage
Dominant (60%) #ffffff var(--color-surface) Page background, card background
Secondary (30%) #f7f7f8 var(--color-surface-dim) Step sidebar/tracker background, inactive step indicator
Accent (10%) #4a90d9 var(--color-member-0) Primary CTA buttons, active step indicator fill, spinner, links
Destructive #dc2626 var(--color-destructive) Validation failure border + helper text (same CredentialSheet pattern)

Accent reserved for:

  • Primary action buttons ("Continue", "Complete Setup") — filled background
  • Active wizard step indicator (filled circle)
  • Inline spinner (Loader2) during async validation
  • Hyperlinks (e.g. "Get an app password")
  • Focus ring (var(--color-focus-ring): #4a90d9)

Additional semantic colors (not new — already in tokens.css):

  • var(--color-border) #e2e4e9 — card border, input border (default), step connector line
  • var(--color-border-subtle) #eceef2 — section dividers within steps
  • var(--color-text-primary) #111318 — headings, field values
  • var(--color-text-secondary) #6b7280 — descriptions, helper text, "Back" button
  • var(--color-text-muted) #9ca3af — completed step labels, placeholder text, inactive step numbers

Surface Architecture

The wizard is a standalone full-page route (/setup) mounted in a separate React root or an App-level gate (see Routing section). It renders none of the AppNav / BottomTabBar / SetupBanner chrome.

Surface 1 — Wizard Page Shell

  • Background: var(--color-surface) (#ffffff)
  • Layout: vertically centered column, min-height: 100dvh
  • Content column: maxWidth: 540px, margin: 0 auto, padding: var(--space-12) var(--space-6) (48px top/bottom, 24px sides)
  • Page title "FamilySync Setup": Display (24px/600), color: var(--color-text-primary), marginBottom: var(--space-2) (8px)
  • Page subtitle "Let's get your instance ready.": Body (15px/400), color: var(--color-text-secondary), marginBottom: var(--space-8) (32px)

Surface 2 — Step Indicator

Linear step tracker shown above the active step card.

  • Horizontal row of 4 step circles connected by lines
  • Completed step: filled circle var(--color-member-0) with white Check icon (16px)
  • Active step: filled circle var(--color-member-0) with white step number (13px/600)
  • Upcoming step: circle with var(--color-border) 2px border, var(--color-text-muted) step number
  • Connector line: 1px var(--color-border) between circles; completed segment fills to var(--color-member-0)
  • Step label below each circle: 13px/400, var(--color-text-muted) (upcoming/completed), var(--color-text-primary) (active)
  • Circle size: 28px diameter; connector height: 1px; minimum row height: 44px touch target achieved by centering in a 44px tall row

Step labels (4 steps total):

  1. Welcome
  2. Instance
  3. Calendar
  4. Complete

Surface 3 — Step Card

The active step's input/content area. One card rendered at a time.

  • Background: var(--color-surface) (#ffffff)
  • Border: 1px solid var(--color-border) (#e2e4e9)
  • Border-radius: 8px (2× var(--space-2))
  • Padding: var(--space-6) (24px) all sides
  • Box-shadow: 0 1px 4px rgba(0,0,0,0.06) (subtle lift)
  • Step heading: Heading (18px/600), color: var(--color-text-primary), marginBottom: var(--space-2) (8px)
  • Step description: Body (15px/400), color: var(--color-text-secondary), lineHeight: 1.5, marginBottom: var(--space-6) (24px)
  • Field group spacing: var(--space-4) (16px) between fields

Surface 4 — Input Field

Standard text input used across steps 23 to collect config.

  • Width: 100%, box-sizing: border-box
  • Padding: var(--space-3, 12px) var(--space-4, 16px) (matches CredentialSheet pattern)
  • Border: 1px solid var(--color-border) default; 1px solid var(--color-destructive) on validation error
  • Border-radius: var(--space-1, 4px) (4px)
  • Font: 15px/400, var(--color-text-primary), var(--font-family-base)
  • Background: var(--color-surface)
  • Label above: 13px/600, var(--color-text-primary), marginBottom: var(--space-1) (4px)
  • Helper text below: 13px/400, var(--color-text-secondary)

Surface 5 — Validation State Row

Shown after a validation request is triggered (DB connectivity / OIDC discovery / CalDAV PROPFIND).

  • Pending: Loader2 icon (16px, var(--color-member-0), animation: spin 1s linear infinite) + Body (15px/400) status text in var(--color-text-secondary) — inline row
  • Success: CheckCircle icon (16px, var(--color-text-secondary)) + success text Body (15px/400) in var(--color-text-secondary)
  • Failure: AlertCircle icon (16px, var(--color-destructive)) + error text Body (15px/400) in var(--color-destructive) — same pattern as CredentialSheet FAILURE_TEXT
  • Layout: display: flex; alignItems: center; gap: var(--space-2, 8px) (CredentialSheet pattern)
  • Container: role="status" with aria-live="polite"

Surface 6 — Action Row

Bottom of each step card.

  • Layout: display: flex; justifyContent: flex-end; gap: var(--space-3, 12px) (CredentialSheet pattern)
  • "Back" button: ghost (no background, no border), Label (13px/600), color: var(--color-text-secondary), minHeight: 44px, padding: 0 var(--space-4) — hidden on Step 1 (no back from Welcome)
  • "Continue" / "Complete Setup" button: filled, background: var(--color-member-0) (#4a90d9), color: #ffffff, Label (13px/600), minHeight: 44px, padding: 0 var(--space-6), borderRadius: var(--space-1) (4px), transition: background 0.15s ease — disabled state: background: var(--color-border) (#e2e4e9), cursor: default (AdminPage / CredentialSheet pattern)
  • "Continue" label on steps 13; "Complete Setup" label on step 4

Surface 7 — Terminal "Setup Complete" Screen

Replaces the wizard card after step 4 completes successfully.

  • Icon: ShieldCheck (48px, var(--color-member-0)) centered
  • Heading: "Setup complete" — Display (24px/600), centered, marginTop: var(--space-4), color: var(--color-text-primary)
  • Body: "Your FamilySync instance is ready. Sign in to continue." — Body (15px/400), color: var(--color-text-secondary), centered, marginTop: var(--space-2)
  • "Sign in" button: filled accent button (same style as Continue), centered, marginTop: var(--space-6), navigates to / (triggers OIDC redirect)
  • No step indicator visible on this screen (step indicator hidden once setup complete)

Surface 8 — "Already Locked" Screen

Shown when the operator navigates to /setup after setup_complete = true (423 from API).

  • Icon: ShieldCheck (48px, var(--color-text-muted)) centered
  • Heading: "Setup already complete" — Heading (18px/600), centered, color: var(--color-text-primary)
  • Body: "This instance has already been configured. Sign in to continue." — Body (15px/400), color: var(--color-text-secondary), centered
  • "Sign in" link: accent-colored text link (no button chrome), 15px/600, navigates to /

Wizard Steps — Detailed Interaction Contract

REVISION (2026-06-15, CONTEXT.md D-04/D-05): The original 5-step model included a "Generate Secrets" step (Step 2). This step is dropped. Secret generation (SESSION_SECRET, APP_PASSWORD_ENCRYPTION_KEY, VAPID public/private keys) is done pre-boot via the npm run generate-secrets repo helper script. The wizard never generates, displays, or requests acknowledgement of secrets. Steps are now 4 total.

Step 1: Welcome

Purpose: orient the operator; no inputs; no validation.

  • Heading: "Welcome to FamilySync Setup"
  • Description: "This wizard will guide you through configuring your self-hosted instance. Before continuing, run npm run generate-secrets from the repo to generate your instance secrets and add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia) and a Fastmail account with an app password. This takes about 5 minutes."
  • Informational note block (Surface 3 — inside the card, background: var(--color-surface-dim), border-radius: 4px, padding: var(--space-3) var(--space-4), marginBottom: var(--space-4)):
    • Label (13px/600, var(--color-text-primary)): "Before you start"
    • Body: "Run npm run generate-secrets and add the output to your Docker environment block. These secrets cannot be recovered if lost."
  • No input fields.
  • Continue button: always enabled.

Step 2: Instance Configuration

Purpose: collect non-secret runtime config that the wizard writes to app_config. No secrets are collected here. Validates DB connectivity and OIDC discovery.

  • Heading: "Instance Configuration"
  • Description: "Enter your instance's connection details. These are written to the database — not your environment file."

Fields (collected and written to app_config):

  1. App URL

    • Label: "App URL"
    • Type: text, placeholder: https://familysync.example.com
    • Helper: "The public URL where FamilySync is reachable."
    • app_config key: app_url
  2. OIDC Issuer

    • Label: "OIDC issuer URL"
    • Type: text, placeholder: https://auth.example.com
    • Helper: "Your Authelia instance URL. FamilySync will fetch /.well-known/openid-configuration from this URL."
    • app_config key: oidc_issuer
  3. OIDC Client ID

    • Label: "OIDC client ID"
    • Type: text, placeholder: familysync
    • Helper: "The client ID registered in Authelia for this application."
    • app_config key: oidc_client_id
  4. VAPID Public Key

    • Label: "VAPID public key"
    • Type: text, placeholder: BH… (URL-safe base64, 87 chars)
    • Helper: "Paste the VAPID_PUBLIC_KEY value from npm run generate-secrets."
    • app_config key: vapid_public_key

Validations (triggered by "Save & Validate" button):

Two sequential checks run after the operator taps the action button:

  1. DatabasePOST /api/setup/validate/db

    • Validation state row (Surface 5):
      • Pending: "Testing database connection…"
      • Success: "Database connection verified."
      • Failure: "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again."
  2. OIDC discoveryPOST /api/setup/validate/oidc (runs after DB success)

    • Validation state row (Surface 5):
      • Pending: "Checking OIDC discovery…"
      • Success: "OIDC discovery resolved."
      • Failure: "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server."
  • Action button label on this step: "Save & Validate" (primary filled, full-width on this step — replace the normal right-aligned action row with a full-width button above the validation state rows, then normal Continue/Back row appears below once both pass)
  • Continue appears (enabled) only when both validation rows show success and config has been saved (POST /api/setup/config call completes before validation begins).
  • Back is available.

Step 3: Calendar Credential

Purpose: set the first member's Fastmail app password; validate against CalDAV PROPFIND. Reuses the CredentialSheet interaction pattern (same fields, same validation feedback, same copy).

  • Heading: "Fastmail Credential"
  • Description: "Add the Fastmail app password for the first household member. This credential is validated against Fastmail CalDAV before saving. The password is never stored in plain text."
  • Fields (same as CredentialSheet):
    • Fastmail email (type="email", autoComplete="email", placeholder="user@fastmail.com")
    • App password (type="password", autoComplete="new-password")
  • Helper text below fields: "Enter the Fastmail app password scoped to Calendars/CalDAV. Get an app password — choose the 'Calendars & Contacts (CalDAV)' scope."
    • Link: accent-colored, text-decoration: underline, opens in new tab rel="noopener noreferrer"
  • Validation state row (Surface 5):
    • Pending: "Validating against CalDAV…" (Loader2 spinner)
    • Success: "Credential verified." — Continue becomes enabled
    • Failure: "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again." (in var(--color-destructive))
  • Continue button label on this step: "Complete Setup"
  • On continue: POST /api/setup/complete — provisions the pre-OIDC local user + credential, flips app_config.setup_complete, redirects to Surface 7 (Terminal Screen)
  • Back is available.

Step 4 — no longer a step card; becomes the Terminal Screen

After POST /api/setup/complete succeeds, the wizard card is replaced by Surface 7 (Terminal "Setup Complete" screen). There is no separate "Step 4" card — the terminal screen IS the completion state.


Routing & App-Level Gate

The wizard lives at route /setup. App.tsx must add a gate at startup:

  1. On app load, GET /api/setup/status is fetched (before any other /api route, mounts outside OIDC guard).
  2. If response is { setupComplete: false } → redirect entire app to /setup (full-page takeover, no AppNav/BottomTabBar rendered).
  3. If response is { setupComplete: true } → normal app boot continues.
  4. If /setup is navigated directly after setup is complete (API returns 423) → render Surface 8 ("Already Locked").

The /setup route does NOT render inside the normal App shell. It replaces it entirely (or is handled before <BrowserRouter> routes — implementation choice for planner, but the design contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeniedBanner).


Copywriting Contract

Element Copy
Page title "FamilySync Setup"
Page subtitle "Let's get your instance ready."
Primary CTA (steps 12) "Continue"
Step 2 action button "Save & Validate"
Primary CTA (step 3) "Complete Setup"
Secondary action "Back"
Terminal heading "Setup complete"
Terminal body "Your FamilySync instance is ready. Sign in to continue."
Terminal CTA "Sign in"
Locked heading "Setup already complete"
Locked body "This instance has already been configured. Sign in to continue."
Locked link "Sign in"
Step 1 heading "Welcome to FamilySync Setup"
Step 1 description "This wizard will guide you through configuring your self-hosted instance. Before continuing, run npm run generate-secrets from the repo to generate your instance secrets and add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia) and a Fastmail account with an app password. This takes about 5 minutes."
Step 1 pre-start label "Before you start"
Step 1 pre-start body "Run npm run generate-secrets and add the output to your Docker environment block. These secrets cannot be recovered if lost."
Step 2 heading "Instance Configuration"
Step 2 description "Enter your instance's connection details. These are written to the database — not your environment file."
Step 2 field: App URL label "App URL"
Step 2 field: App URL placeholder "https://familysync.example.com"
Step 2 field: App URL helper "The public URL where FamilySync is reachable."
Step 2 field: OIDC issuer label "OIDC issuer URL"
Step 2 field: OIDC issuer placeholder "https://auth.example.com"
Step 2 field: OIDC issuer helper "Your Authelia instance URL. FamilySync will fetch /.well-known/openid-configuration from this URL."
Step 2 field: OIDC client ID label "OIDC client ID"
Step 2 field: OIDC client ID placeholder "familysync"
Step 2 field: OIDC client ID helper "The client ID registered in Authelia for this application."
Step 2 field: VAPID public key label "VAPID public key"
Step 2 field: VAPID public key placeholder "BH…"
Step 2 field: VAPID public key helper "Paste the VAPID_PUBLIC_KEY value from npm run generate-secrets."
Step 2 DB pending "Testing database connection…"
Step 2 DB success "Database connection verified."
Step 2 DB failure "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again."
Step 2 OIDC pending "Checking OIDC discovery…"
Step 2 OIDC success "OIDC discovery resolved."
Step 2 OIDC failure "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server."
Step 3 heading "Fastmail Credential"
Step 3 description "Add the Fastmail app password for the first household member. This credential is validated against Fastmail CalDAV before saving. The password is never stored in plain text."
Step 3 helper text "Enter the Fastmail app password scoped to Calendars/CalDAV."
Step 3 helper link text "Get an app password"
Step 3 helper link suffix " — choose the 'Calendars & Contacts (CalDAV)' scope."
Step 3 pending "Validating against CalDAV…"
Step 3 success "Credential verified."
Step 3 failure "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again."
Empty state N/A — every step has explicit content
Error state (network/unexpected) "Something went wrong. Please try again." (generic fallback, shown in Surface 5 failure style)
Destructive actions None — wizard has no destructive actions. "Complete Setup" is irreversible in effect but not destructive; no confirmation dialog required.

Accessibility Contract

  • role="main" on the wizard content column
  • Step indicator: role="list" with each step as role="listitem"; active step has aria-current="step"
  • Step card: role="group" with aria-labelledby pointing to the step heading id
  • Heading hierarchy: <h1> for page title, <h2> for step heading
  • All inputs: explicit <label htmlFor> association (same CredentialSheet pattern)
  • Disabled buttons: disabled attribute (not just pointer-events: none)
  • Validation state row: role="status" with aria-live="polite" so screen readers announce results without focus movement
  • Escape key: no sheet to close on this page; Escape has no effect in wizard
  • Focus management: on step advance, focus moves to the new step heading (tabIndex={-1} pattern from CredentialSheet heading)
  • Minimum touch targets: 44px on all interactive elements (minHeight: 44px, minWidth: 44px)
  • Focus ring: var(--color-focus-ring) (#4a90d9), 2px outline, 2px offset on all focusable elements (same project convention)

Responsive Behavior

The wizard is desktop-first (operator is typically on a desktop browser when standing up infrastructure), but must be usable on a phone if needed.

  • Desktop (≥768px): card centered at maxWidth 540px; step indicator spans full card width
  • Phone (<768px): card fills viewport minus 24px horizontal padding; step indicator uses short labels (1, 2, 3, 4) or icon-only to avoid overflow; no bottom tab bar (not rendered at all on wizard page)
  • No BottomTabBar, no AppNav on this page at any breakpoint

Security Display Rules

These are hard UI rules, not implementation notes:

  • The wizard never displays generated secret values. Secrets (SESSION_SECRET, APP_PASSWORD_ENCRYPTION_KEY, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY) are generated pre-boot by the npm run generate-secrets helper and pasted into Docker env by the operator. The wizard only collects the VAPID public key (non-secret) as a form field.
  • App password in Step 3: type="password" — never visible
  • No dangerouslySetInnerHTML anywhere on this page (T-05-24 project convention)

Registry Safety

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

No third-party registries. All components are hand-rolled following existing project convention. lucide-react icons are already installed (KeyRound, ShieldCheck, Loader2, CheckCircle, AlertCircle, Copy, Check are all available or trivially added from the existing lucide-react dependency).


Pre-Population Sources

Decision Source Value
Spacing scale apps/pwa/src/styles/tokens.css --space-1 through --space-12
Typography scale apps/pwa/src/styles/tokens.css 4 sizes (13/15/18/24px), 2 weights (400/600)
Color palette apps/pwa/src/styles/tokens.css All hex values
Component library apps/pwa (convention) Hand-rolled inline styles, no shadcn
Icon library apps/pwa imports lucide-react
Button style CredentialSheet.tsx / AdminPage.tsx Filled accent for primary, ghost for secondary
Input style CredentialSheet.tsx 12px/16px padding, 4px border-radius, destructive border on error
Section label style AdminPage.tsx 13px/600/uppercase/0.06em letter-spacing
Card padding AdminPage.tsx var(--space-12) top/bottom, var(--space-6) horizontal
Credential copy CredentialSheet.tsx Same field layout, same validation feedback pattern
Step 3 fields CredentialSheet.tsx Exact field structure, labels, helper text, link
Step count (4 not 5) CONTEXT.md D-04/D-05 Step 2 "Generate Secrets" dropped — generation pre-boot
Step 2 input fields CONTEXT.md D-02 Non-secret config collected in wizard, written to app_config
No secrets in wizard CONTEXT.md D-01/D-05 Kernel secrets (DB, SESSION, ENCRYPTION_KEY, VAPID_PRIVATE) stay in env
Step labels Claude's Discretion (CONTEXT.md) Welcome / Instance / Calendar / Complete

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