Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
24 KiB
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: monospaceoverride - Section labels (uppercase caps, e.g. "STEP 2 OF 5"): Label (13px/600) with
text-transform: uppercase; letter-spacing: 0.06em(AdminPagesectionLabelStylepattern)
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 linevar(--color-border-subtle)#eceef2 — section dividers within stepsvar(--color-text-primary)#111318 — headings, field valuesvar(--color-text-secondary)#6b7280 — descriptions, helper text, "Back" buttonvar(--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 whiteCheckicon (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 tovar(--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):
- Welcome
- Secrets
- Database
- OIDC
- 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 (
Copyicon 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
.envfile." — 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:
Loader2icon (16px,var(--color-member-0),animation: spin 1s linear infinite) + Body (15px/400) status text invar(--color-text-secondary)— inline row - Success:
CheckCircleicon (16px,var(--color-text-secondary)) + success text Body (15px/400) invar(--color-text-secondary) - Failure:
AlertCircleicon (16px,var(--color-destructive)) + error text Body (15px/400) invar(--color-destructive)— same pattern as CredentialSheetFAILURE_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 1–4; "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.ymlto 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.ymlenvironment 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:
SESSION_SECRET— label "Session secret", 64-char hex stringAPP_PASSWORD_ENCRYPTION_KEY— label "Encryption key", 64-char hex stringVAPID_PUBLIC_KEY— label "VAPID public key"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.ymland 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."
- OIDC: label "Authelia issuer discovery",
- "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 tabrel="noopener noreferrer"
- Link: accent-colored,
- 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, setsapp_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:
- On app load,
GET /api/setup/statusis fetched (before any other /api route, mounts outside OIDC guard). - If response is
{ setupComplete: false }→ redirect entire app to/setup(full-page takeover, no AppNav/BottomTabBar rendered). - If response is
{ setupComplete: true }→ normal app boot continues. - If
/setupis 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 1–4) | "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 asrole="listitem"; active step hasaria-current="step" - Step card:
role="group"witharia-labelledbypointing to the step headingid - Heading hierarchy:
<h1>for page title,<h2>for step heading - All inputs: explicit
<label htmlFor>association (same CredentialSheet pattern) - Disabled buttons:
disabledattribute (not just pointer-events: none) - Copy button:
aria-label="Copy {field name}", swaps toaria-label="Copied"for 2 seconds - Validation state row:
role="status"witharia-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>withuserSelect: allfor easy selection — never in atype="password"input (they must be visible to copy) - App password in Step 5:
type="password"— never visible - No
dangerouslySetInnerHTMLanywhere 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