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

512 lines
24 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: 12
slug: initial-setup-wizard
status: draft
shadcn_initialized: false
preset: none
created: 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](https://app.fastmail.com/settings/security/devicetokens) — 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