Phase 18: Auto timezone detection and ability to change timezone #21
@@ -0,0 +1,511 @@
|
|||||||
|
---
|
||||||
|
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 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.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 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 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
|
||||||
Reference in New Issue
Block a user