Files
familysync/.planning/phases/12-initial-setup-wizard/12-UI-SPEC.md
T

24 KiB
Raw Blame History

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

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.


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 and generated-secret blocks).
  • Step indicator touch targets: 44px minimum (accessibility).
  • Copy-to-clipboard button: 36px height is acceptable since it is paired with an adjacent textarea (which itself is large enough), but the copy button must have minWidth: 44px.

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. "Generate Secrets"): 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
  • Generated secret value (monospace block): 13px/400/1.4 with font-family: monospace override
  • Section labels (uppercase caps, e.g. "STEP 2 OF 5"): 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, generated-secret block background, inactive step indicator
Accent (10%) #4a90d9 var(--color-member-0) Primary CTA buttons, active step indicator fill, spinner, copy button, 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")
  • Copy-to-clipboard button icon
  • 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 Interaction Contract). It renders none of the AppNav / BottomTabBar / SetupBanner chrome.

Surface 1 — Wizard Page Shell

The wizard page itself.

  • 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 N 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 (5 steps total):

  1. Welcome
  2. Secrets
  3. Database
  4. OIDC
  5. Calendar

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 — Generated-Secret Block

Used in Step 2 (Secrets) for values the operator must copy into env.

  • Background: var(--color-surface-dim) (#f7f7f8)
  • Border: 1px solid var(--color-border-subtle) (#eceef2)
  • Border-radius: 4px (var(--space-1))
  • Padding: var(--space-3) var(--space-4) (12px 16px)
  • Secret value: monospace, 13px/400, var(--color-text-primary), word-break: break-all (VAPID keys are long strings)
  • Label above block: Label (13px/600), var(--color-text-primary)
  • Copy button: icon-only (Copy icon 16px, var(--color-member-0)), positioned top-right inside the block, minWidth: 44px, minHeight: 36px (acceptable — paired with large block)
  • After copy: icon swaps to Check (16px, var(--color-member-0)) for 2 seconds, then reverts
  • Acknowledgement checkbox below each secret block: standard checkbox input, Label (13px/400), "I have copied this value into my .env file." — the Continue button is disabled until all checkboxes on the step are checked.

Surface 5 — Validation State Row

Shown after the operator submits a validation step (DB / OIDC / CalDAV).

  • 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)

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 14; "Complete Setup" label on step 5

Surface 7 — Terminal "Setup Complete" Screen

Replaces the wizard card after step 5 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

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. You'll need: your OIDC client credentials (Authelia), a Fastmail account with an app password, and a copy of your docker-compose.yml to paste generated secrets into. This takes about 5 minutes."
  • No input fields.
  • Continue button: always enabled.

Step 2: Generate Secrets

Purpose: display generated session secret, encryption key, and VAPID keypair; operator copies each into env.

  • Heading: "Generated Secrets"
  • Description: "These values are generated once and displayed now. Copy each into your docker-compose.yml environment block before continuing. They will never be shown again and are not stored in the database."
  • Four generated-secret blocks (Surface 4), each with acknowledgement checkbox:
    1. SESSION_SECRET — label "Session secret", 64-char hex string
    2. APP_PASSWORD_ENCRYPTION_KEY — label "Encryption key", 64-char hex string
    3. VAPID_PUBLIC_KEY — label "VAPID public key"
    4. VAPID_PRIVATE_KEY — label "VAPID private key"
  • Continue button disabled until all 4 checkboxes are checked.
  • No async validation on this step. Secrets are generated client-side or fetched from POST /api/setup/generate (backend choice — UI treats them as string values to display).

Step 3: Database

Purpose: verify the DB connection configured in env is reachable.

  • Heading: "Database Connection"
  • Description: "Verify that the app can reach the MariaDB database configured in your environment. No changes are made — this is a read-only connectivity check."
  • No operator input fields (DB creds come from env, not from this UI).
  • "Test Connection" button (primary filled, full-width on this step — replace normal action row): triggers POST /api/setup/validate/db
  • Validation state row (Surface 5) shown below the description during/after the test:
    • 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-compose.yml and try again."
  • Continue button appears after success; disabled during pending; hidden on failure (operator must retry first).
  • Back is available.

Step 4: OIDC & VAPID

Purpose: verify OIDC discovery resolves and the VAPID keypair in env is structurally valid.

  • Heading: "OIDC & Push"
  • Description: "Verify that the Authelia OIDC issuer is reachable and that the VAPID keypair you copied in Step 2 is in place."
  • Two validation rows, triggered sequentially by "Validate" button:
    • OIDC: label "Authelia issuer discovery", POST /api/setup/validate/oidc
      • Pending: "Checking OIDC discovery…"
      • Success: "OIDC discovery resolved."
      • Failure: "OIDC discovery failed. Check OIDC_ISSUER in your environment and that Authelia is reachable."
    • VAPID: label "VAPID keypair", POST /api/setup/validate/vapid
      • Pending: "Checking VAPID keypair…"
      • Success: "VAPID keypair is valid."
      • Failure: "VAPID private key could not be verified. Ensure you copied both keys from Step 2 into your environment and restarted the container."
  • "Validate" button triggers both checks in sequence.
  • Continue appears (and is enabled) only when both rows show success.
  • Back is available.

Step 5: 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 — promotes operator to admin, sets app_config.setup_complete, redirects to Surface 7 (Terminal Screen)
  • Back is available.

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 14) "Continue"
Primary CTA (step 5) "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. You'll need: your OIDC client credentials (Authelia), a Fastmail account with an app password, and a copy of your docker-compose.yml to paste generated secrets into. This takes about 5 minutes."
Step 2 heading "Generated Secrets"
Step 2 description "These values are generated once and displayed now. Copy each into your docker-compose.yml environment block before continuing. They will never be shown again and are not stored in the database."
Step 2 acknowledgement "I have copied this value into my .env file."
Step 3 heading "Database Connection"
Step 3 description "Verify that the app can reach the MariaDB database configured in your environment. No changes are made — this is a read-only connectivity check."
Step 3 CTA "Test Connection"
Step 3 pending "Testing database connection…"
Step 3 success "Database connection verified."
Step 3 failure "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your docker-compose.yml and try again."
Step 4 heading "OIDC & Push"
Step 4 description "Verify that the Authelia OIDC issuer is reachable and that the VAPID keypair you copied in Step 2 is in place."
Step 4 CTA "Validate"
Step 4 OIDC pending "Checking OIDC discovery…"
Step 4 OIDC success "OIDC discovery resolved."
Step 4 OIDC failure "OIDC discovery failed. Check OIDC_ISSUER in your environment and that Authelia is reachable."
Step 4 VAPID pending "Checking VAPID keypair…"
Step 4 VAPID success "VAPID keypair is valid."
Step 4 VAPID failure "VAPID private key could not be verified. Ensure you copied both keys from Step 2 into your environment and restarted the container."
Step 5 heading "Fastmail Credential"
Step 5 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 5 helper text "Enter the Fastmail app password scoped to Calendars/CalDAV."
Step 5 helper link text "Get an app password"
Step 5 helper link suffix " — choose the 'Calendars & Contacts (CalDAV)' scope."
Step 5 pending "Validating against CalDAV…"
Step 5 success "Credential verified."
Step 5 failure "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again."
Empty state (none for wizard — every step has explicit content) N/A
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. The "Complete Setup" action 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)
  • Copy button: aria-label="Copy {field name}", swaps to aria-label="Copied" for 2 seconds
  • 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)
  • Secret textarea (if used instead of div): readonly, aria-label="{field name} value"

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, 5) 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:

  • Secret values in Surface 4 blocks: displayed in a readonly <textarea> or <div> with userSelect: all for easy selection — never in a type="password" input (they must be visible to copy)
  • App password in Step 5: type="password" — never visible
  • No dangerouslySetInnerHTML anywhere on this page (T-05-24 project convention)
  • Acknowledgement checkboxes enforce operator intent before Continue is enabled; this is a UX gate only (not a security boundary — the secrets are already displayed)

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
Bottom sheet pattern CredentialSheet.tsx 12px 12px 0 0 radius, zIndex 301, backdrop 300
Credential copy CredentialSheet.tsx Same field layout, same validation feedback pattern
Step 5 fields CredentialSheet.tsx Exact field structure, labels, helper text, link

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