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

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

532 lines
25 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
updated: 2026-06-15
---
# Phase 12 — UI Design Contract: Initial Setup Wizard
> Visual and interaction contract for the first-run setup wizard.
> Generated by gsd-ui-researcher. Verified by gsd-ui-checker.
>
> **Revision note (2026-06-15):** CONTEXT.md (D-04/D-05/D-06) supersedes the original
> validate-only model. Step 2 "Generate Secrets" is dropped (generation is pre-boot via repo
> helper script). Steps 34 are reworked to collect config via input fields, not just validate
> env. All other design tokens, surfaces, and a11y contracts are unchanged.
---
## Context & Audience
This wizard is **operator-facing, not end-user-facing**. The operator is the person standing up
the self-hosted FamilySync instance on Unraid — technical enough to edit `docker-compose.yml`,
but not necessarily a developer. The wizard renders for an **unauthenticated visitor** (it is the
one screen in the app shown outside the Authelia/OIDC guard). It must be calm, legible, and
unintimidating.
This is the **only full-page standalone UI** in the app. It does NOT render inside the existing
AppNav + BottomTabBar shell. It owns its entire viewport.
All design tokens are inherited from the existing system (`apps/pwa/src/styles/tokens.css`).
No new tokens are introduced.
---
## Design System
| Property | Value |
|----------|-------|
| Tool | none (existing CSS custom properties) |
| Preset | not applicable |
| Component library | none (hand-rolled inline styles, project convention) |
| Icon library | lucide-react (already installed — `KeyRound`, `CheckCircle`, `AlertCircle`, `Loader2`, `Copy`, `Check`, `ShieldCheck`) |
| Font | system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif (var(--font-family-base)) |
Source: `apps/pwa/src/styles/tokens.css` — pre-populated from existing codebase.
---
## Spacing Scale
Uses the existing 4px-based scale. No new tokens. Values from `tokens.css`:
| Token | Value | Usage |
|-------|-------|-------|
| --space-1 | 4px | Icon gaps, label-to-input gap |
| --space-2 | 8px | Compact element spacing, badge gap |
| --space-3 | 12px | Input padding (vertical), row gaps |
| --space-4 | 16px | Default element spacing, card padding, input padding (horizontal) |
| --space-6 | 24px | Sheet/card padding, section gap |
| --space-8 | 32px | Between major sections |
| --space-12 | 48px | Page top/bottom padding (AdminPage pattern) |
Exceptions:
- Wizard card max-width: 540px (slightly wider than CredentialSheet 480px to accommodate
multi-field steps).
- Step indicator touch targets: 44px minimum (accessibility).
---
## Typography
All values from `tokens.css`. No new sizes or weights.
| Role | Size | Weight | Line Height | Variable |
|------|------|--------|-------------|----------|
| Body | 15px | 400 | 1.5 | var(--text-body-size) / var(--text-body-weight) / var(--text-body-line-height) |
| Label | 13px | 400 | 1.4 | var(--text-label-size) / var(--text-label-weight) / var(--text-label-line-height) |
| Heading | 18px | 600 | 1.25 | var(--text-heading-size) / var(--text-heading-weight) / var(--text-heading-line-height) |
| Display | 24px | 600 | 1.2 | var(--text-display-size) / var(--text-display-weight) / var(--text-display-line-height) |
Usage in this phase:
- Wizard page title ("FamilySync Setup"): Display (24px/600/1.2)
- Step heading (e.g. "OIDC & App URL"): Heading (18px/600/1.25)
- Step description / helper text: Body (15px/400/1.5)
- Field labels, step counter, status badges: Label (13px/400/1.4) — labels use weight 600
- Section labels (uppercase caps, e.g. "STEP 2 OF 4"): Label (13px/600) with
`text-transform: uppercase; letter-spacing: 0.06em` (AdminPage `sectionLabelStyle` pattern)
---
## Color
All values from `tokens.css`. No new hex values.
| Role | Value | Variable | Usage |
|------|-------|----------|-------|
| Dominant (60%) | #ffffff | var(--color-surface) | Page background, card background |
| Secondary (30%) | #f7f7f8 | var(--color-surface-dim) | Step sidebar/tracker background, inactive step indicator |
| Accent (10%) | #4a90d9 | var(--color-member-0) | Primary CTA buttons, active step indicator fill, spinner, links |
| Destructive | #dc2626 | var(--color-destructive) | Validation failure border + helper text (same CredentialSheet pattern) |
Accent reserved for:
- Primary action buttons ("Continue", "Complete Setup") — filled background
- Active wizard step indicator (filled circle)
- Inline spinner (`Loader2`) during async validation
- Hyperlinks (e.g. "Get an app password")
- Focus ring (`var(--color-focus-ring): #4a90d9`)
Additional semantic colors (not new — already in tokens.css):
- `var(--color-border)` #e2e4e9 — card border, input border (default), step connector line
- `var(--color-border-subtle)` #eceef2 — section dividers within steps
- `var(--color-text-primary)` #111318 — headings, field values
- `var(--color-text-secondary)` #6b7280 — descriptions, helper text, "Back" button
- `var(--color-text-muted)` #9ca3af — completed step labels, placeholder text, inactive step numbers
---
## Surface Architecture
The wizard is a **standalone full-page route** (`/setup`) mounted in a separate React root or
an App-level gate (see Routing section). It renders none of the AppNav / BottomTabBar /
SetupBanner chrome.
### Surface 1 — Wizard Page Shell
- Background: `var(--color-surface)` (#ffffff)
- Layout: vertically centered column, `min-height: 100dvh`
- Content column: `maxWidth: 540px`, `margin: 0 auto`,
`padding: var(--space-12) var(--space-6)` (48px top/bottom, 24px sides)
- Page title "FamilySync Setup": Display (24px/600), `color: var(--color-text-primary)`,
`marginBottom: var(--space-2)` (8px)
- Page subtitle "Let's get your instance ready.": Body (15px/400),
`color: var(--color-text-secondary)`, `marginBottom: var(--space-8)` (32px)
### Surface 2 — Step Indicator
Linear step tracker shown above the active step card.
- Horizontal row of 4 step circles connected by lines
- Completed step: filled circle `var(--color-member-0)` with white `Check` icon (16px)
- Active step: filled circle `var(--color-member-0)` with white step number (13px/600)
- Upcoming step: circle with `var(--color-border)` 2px border, `var(--color-text-muted)` step
number
- Connector line: 1px `var(--color-border)` between circles; completed segment fills to
`var(--color-member-0)`
- Step label below each circle: 13px/400, `var(--color-text-muted)` (upcoming/completed),
`var(--color-text-primary)` (active)
- Circle size: 28px diameter; connector height: 1px; minimum row height: 44px touch target
achieved by centering in a 44px tall row
Step labels (4 steps total):
1. Welcome
2. Instance
3. Calendar
4. Complete
### Surface 3 — Step Card
The active step's input/content area. One card rendered at a time.
- Background: `var(--color-surface)` (#ffffff)
- Border: `1px solid var(--color-border)` (#e2e4e9)
- Border-radius: 8px (2× `var(--space-2)`)
- Padding: `var(--space-6)` (24px) all sides
- Box-shadow: `0 1px 4px rgba(0,0,0,0.06)` (subtle lift)
- Step heading: Heading (18px/600), `color: var(--color-text-primary)`,
`marginBottom: var(--space-2)` (8px)
- Step description: Body (15px/400), `color: var(--color-text-secondary)`, `lineHeight: 1.5`,
`marginBottom: var(--space-6)` (24px)
- Field group spacing: `var(--space-4)` (16px) between fields
### Surface 4 — Input Field
Standard text input used across steps 23 to collect config.
- Width: 100%, `box-sizing: border-box`
- Padding: `var(--space-3, 12px) var(--space-4, 16px)` (matches CredentialSheet pattern)
- Border: `1px solid var(--color-border)` default; `1px solid var(--color-destructive)` on
validation error
- Border-radius: `var(--space-1, 4px)` (4px)
- Font: 15px/400, `var(--color-text-primary)`, `var(--font-family-base)`
- Background: `var(--color-surface)`
- Label above: 13px/600, `var(--color-text-primary)`, `marginBottom: var(--space-1)` (4px)
- Helper text below: 13px/400, `var(--color-text-secondary)`
### Surface 5 — Validation State Row
Shown after a validation request is triggered (DB connectivity / OIDC discovery / CalDAV PROPFIND).
- Pending: `Loader2` icon (16px, `var(--color-member-0)`, `animation: spin 1s linear infinite`) +
Body (15px/400) status text in `var(--color-text-secondary)` — inline row
- Success: `CheckCircle` icon (16px, `var(--color-text-secondary)`) + success text Body (15px/400)
in `var(--color-text-secondary)`
- Failure: `AlertCircle` icon (16px, `var(--color-destructive)`) + error text Body (15px/400) in
`var(--color-destructive)` — same pattern as CredentialSheet `FAILURE_TEXT`
- Layout: `display: flex; alignItems: center; gap: var(--space-2, 8px)` (CredentialSheet pattern)
- Container: `role="status"` with `aria-live="polite"`
### Surface 6 — Action Row
Bottom of each step card.
- Layout: `display: flex; justifyContent: flex-end; gap: var(--space-3, 12px)` (CredentialSheet
pattern)
- "Back" button: ghost (no background, no border), Label (13px/600),
`color: var(--color-text-secondary)`, `minHeight: 44px`, `padding: 0 var(--space-4)`
— hidden on Step 1 (no back from Welcome)
- "Continue" / "Complete Setup" button: filled, `background: var(--color-member-0)` (#4a90d9),
`color: #ffffff`, Label (13px/600), `minHeight: 44px`, `padding: 0 var(--space-6)`,
`borderRadius: var(--space-1)` (4px), `transition: background 0.15s ease`
— disabled state: `background: var(--color-border)` (#e2e4e9), `cursor: default`
(AdminPage / CredentialSheet pattern)
- "Continue" label on steps 13; "Complete Setup" label on step 4
### Surface 7 — Terminal "Setup Complete" Screen
Replaces the wizard card after step 4 completes successfully.
- Icon: `ShieldCheck` (48px, `var(--color-member-0)`) centered
- Heading: "Setup complete" — Display (24px/600), centered, `marginTop: var(--space-4)`,
`color: var(--color-text-primary)`
- Body: "Your FamilySync instance is ready. Sign in to continue." — Body (15px/400),
`color: var(--color-text-secondary)`, centered, `marginTop: var(--space-2)`
- "Sign in" button: filled accent button (same style as Continue), centered,
`marginTop: var(--space-6)`, navigates to `/` (triggers OIDC redirect)
- No step indicator visible on this screen (step indicator hidden once setup complete)
### Surface 8 — "Already Locked" Screen
Shown when the operator navigates to `/setup` after `setup_complete = true` (423 from API).
- Icon: `ShieldCheck` (48px, `var(--color-text-muted)`) centered
- Heading: "Setup already complete" — Heading (18px/600), centered,
`color: var(--color-text-primary)`
- Body: "This instance has already been configured. Sign in to continue." — Body (15px/400),
`color: var(--color-text-secondary)`, centered
- "Sign in" link: accent-colored text link (no button chrome), 15px/600,
navigates to `/`
---
## Wizard Steps — Detailed Interaction Contract
> **REVISION (2026-06-15, CONTEXT.md D-04/D-05):** The original 5-step model included a
> "Generate Secrets" step (Step 2). This step is **dropped**. Secret generation (SESSION_SECRET,
> APP_PASSWORD_ENCRYPTION_KEY, VAPID public/private keys) is done pre-boot via the
> `npm run generate-secrets` repo helper script. The wizard never generates, displays, or
> requests acknowledgement of secrets. Steps are now 4 total.
### Step 1: Welcome
Purpose: orient the operator; no inputs; no validation.
- Heading: "Welcome to FamilySync Setup"
- Description: "This wizard will guide you through configuring your self-hosted instance. Before
continuing, run `npm run generate-secrets` from the repo to generate your instance secrets and
add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia)
and a Fastmail account with an app password. This takes about 5 minutes."
- Informational note block (Surface 3 — inside the card, `background: var(--color-surface-dim)`,
`border-radius: 4px`, `padding: var(--space-3) var(--space-4)`, `marginBottom: var(--space-4)`):
- Label (13px/600, `var(--color-text-primary)`): "Before you start"
- Body: "Run `npm run generate-secrets` and add the output to your Docker environment block.
These secrets cannot be recovered if lost."
- No input fields.
- Continue button: always enabled.
### Step 2: Instance Configuration
Purpose: collect non-secret runtime config that the wizard writes to `app_config`. No secrets
are collected here. Validates DB connectivity and OIDC discovery.
- Heading: "Instance Configuration"
- Description: "Enter your instance's connection details. These are written to the database —
not your environment file."
**Fields (collected and written to `app_config`):**
1. **App URL**
- Label: "App URL"
- Type: `text`, placeholder: `https://familysync.example.com`
- Helper: "The public URL where FamilySync is reachable."
- `app_config` key: `app_url`
2. **OIDC Issuer**
- Label: "OIDC issuer URL"
- Type: `text`, placeholder: `https://auth.example.com`
- Helper: "Your Authelia instance URL. FamilySync will fetch `/.well-known/openid-configuration`
from this URL."
- `app_config` key: `oidc_issuer`
3. **OIDC Client ID**
- Label: "OIDC client ID"
- Type: `text`, placeholder: `familysync`
- Helper: "The client ID registered in Authelia for this application."
- `app_config` key: `oidc_client_id`
4. **VAPID Public Key**
- Label: "VAPID public key"
- Type: `text`, placeholder: `BH…` (URL-safe base64, 87 chars)
- Helper: "Paste the `VAPID_PUBLIC_KEY` value from `npm run generate-secrets`."
- `app_config` key: `vapid_public_key`
**Validations (triggered by "Save & Validate" button):**
Two sequential checks run after the operator taps the action button:
1. **Database**`POST /api/setup/validate/db`
- Validation state row (Surface 5):
- Pending: "Testing database connection…"
- Success: "Database connection verified."
- Failure: "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your
Docker environment and try again."
2. **OIDC discovery**`POST /api/setup/validate/oidc` (runs after DB success)
- Validation state row (Surface 5):
- Pending: "Checking OIDC discovery…"
- Success: "OIDC discovery resolved."
- Failure: "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from
the server."
- Action button label on this step: "Save & Validate" (primary filled, full-width on this
step — replace the normal right-aligned action row with a full-width button above the validation
state rows, then normal Continue/Back row appears below once both pass)
- Continue appears (enabled) only when both validation rows show success and config has been saved
(`POST /api/setup/config` call completes before validation begins).
- Back is available.
### Step 3: Calendar Credential
Purpose: set the first member's Fastmail app password; validate against CalDAV PROPFIND.
Reuses the CredentialSheet interaction pattern (same fields, same validation feedback,
same copy).
- Heading: "Fastmail Credential"
- Description: "Add the Fastmail app password for the first household member. This credential
is validated against Fastmail CalDAV before saving. The password is never stored in
plain text."
- Fields (same as CredentialSheet):
- Fastmail email (type="email", autoComplete="email", placeholder="user@fastmail.com")
- App password (type="password", autoComplete="new-password")
- Helper text below fields: "Enter the Fastmail app password scoped to Calendars/CalDAV.
[Get an app password](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` — provisions the pre-OIDC local user + credential,
flips `app_config.setup_complete`, redirects to Surface 7 (Terminal Screen)
- Back is available.
### Step 4 — no longer a step card; becomes the Terminal Screen
After `POST /api/setup/complete` succeeds, the wizard card is replaced by Surface 7 (Terminal
"Setup Complete" screen). There is no separate "Step 4" card — the terminal screen IS the
completion state.
---
## Routing & App-Level Gate
The wizard lives at route `/setup`. App.tsx must add a gate at startup:
1. On app load, `GET /api/setup/status` is fetched (before any other /api route, mounts outside
OIDC guard).
2. If response is `{ setupComplete: false }` → redirect entire app to `/setup` (full-page
takeover, no AppNav/BottomTabBar rendered).
3. If response is `{ setupComplete: true }` → normal app boot continues.
4. If `/setup` is navigated directly after setup is complete (API returns 423) → render
Surface 8 ("Already Locked").
The `/setup` route does NOT render inside the normal App shell. It replaces it entirely (or
is handled before `<BrowserRouter>` routes — implementation choice for planner, but the design
contract requires: no AppNav, no BottomTabBar, no SetupBanner, no PermissionDeniedBanner).
---
## Copywriting Contract
| Element | Copy |
|---------|------|
| Page title | "FamilySync Setup" |
| Page subtitle | "Let's get your instance ready." |
| Primary CTA (steps 12) | "Continue" |
| Step 2 action button | "Save & Validate" |
| Primary CTA (step 3) | "Complete Setup" |
| Secondary action | "Back" |
| Terminal heading | "Setup complete" |
| Terminal body | "Your FamilySync instance is ready. Sign in to continue." |
| Terminal CTA | "Sign in" |
| Locked heading | "Setup already complete" |
| Locked body | "This instance has already been configured. Sign in to continue." |
| Locked link | "Sign in" |
| Step 1 heading | "Welcome to FamilySync Setup" |
| Step 1 description | "This wizard will guide you through configuring your self-hosted instance. Before continuing, run `npm run generate-secrets` from the repo to generate your instance secrets and add them to your Docker environment. You'll also need: your OIDC client credentials (Authelia) and a Fastmail account with an app password. This takes about 5 minutes." |
| Step 1 pre-start label | "Before you start" |
| Step 1 pre-start body | "Run `npm run generate-secrets` and add the output to your Docker environment block. These secrets cannot be recovered if lost." |
| Step 2 heading | "Instance Configuration" |
| Step 2 description | "Enter your instance's connection details. These are written to the database — not your environment file." |
| Step 2 field: App URL label | "App URL" |
| Step 2 field: App URL placeholder | "https://familysync.example.com" |
| Step 2 field: App URL helper | "The public URL where FamilySync is reachable." |
| Step 2 field: OIDC issuer label | "OIDC issuer URL" |
| Step 2 field: OIDC issuer placeholder | "https://auth.example.com" |
| Step 2 field: OIDC issuer helper | "Your Authelia instance URL. FamilySync will fetch `/.well-known/openid-configuration` from this URL." |
| Step 2 field: OIDC client ID label | "OIDC client ID" |
| Step 2 field: OIDC client ID placeholder | "familysync" |
| Step 2 field: OIDC client ID helper | "The client ID registered in Authelia for this application." |
| Step 2 field: VAPID public key label | "VAPID public key" |
| Step 2 field: VAPID public key placeholder | "BH…" |
| Step 2 field: VAPID public key helper | "Paste the `VAPID_PUBLIC_KEY` value from `npm run generate-secrets`." |
| Step 2 DB pending | "Testing database connection…" |
| Step 2 DB success | "Database connection verified." |
| Step 2 DB failure | "Cannot reach the database. Check DB_HOST, DB_PORT, DB_USER, DB_PASSWORD in your Docker environment and try again." |
| Step 2 OIDC pending | "Checking OIDC discovery…" |
| Step 2 OIDC success | "OIDC discovery resolved." |
| Step 2 OIDC failure | "OIDC discovery failed. Check the issuer URL and that Authelia is reachable from the server." |
| Step 3 heading | "Fastmail Credential" |
| Step 3 description | "Add the Fastmail app password for the first household member. This credential is validated against Fastmail CalDAV before saving. The password is never stored in plain text." |
| Step 3 helper text | "Enter the Fastmail app password scoped to Calendars/CalDAV." |
| Step 3 helper link text | "Get an app password" |
| Step 3 helper link suffix | " — choose the 'Calendars & Contacts (CalDAV)' scope." |
| Step 3 pending | "Validating against CalDAV…" |
| Step 3 success | "Credential verified." |
| Step 3 failure | "Invalid password — CalDAV validation failed. Check the scope is 'Calendars & Contacts (CalDAV)' and try again." |
| Empty state | N/A — every step has explicit content |
| Error state (network/unexpected) | "Something went wrong. Please try again." (generic fallback, shown in Surface 5 failure style) |
| Destructive actions | None — wizard has no destructive actions. "Complete Setup" is irreversible in effect but not destructive; no confirmation dialog required. |
---
## Accessibility Contract
- `role="main"` on the wizard content column
- Step indicator: `role="list"` with each step as `role="listitem"`;
active step has `aria-current="step"`
- Step card: `role="group"` with `aria-labelledby` pointing to the step heading `id`
- Heading hierarchy: `<h1>` for page title, `<h2>` for step heading
- All inputs: explicit `<label htmlFor>` association (same CredentialSheet pattern)
- Disabled buttons: `disabled` attribute (not just pointer-events: none)
- Validation state row: `role="status"` with `aria-live="polite"` so screen readers announce
results without focus movement
- Escape key: no sheet to close on this page; Escape has no effect in wizard
- Focus management: on step advance, focus moves to the new step heading (tabIndex={-1} pattern
from CredentialSheet heading)
- Minimum touch targets: 44px on all interactive elements (`minHeight: 44px`, `minWidth: 44px`)
- Focus ring: `var(--color-focus-ring)` (#4a90d9), 2px outline, 2px offset on all focusable
elements (same project convention)
---
## Responsive Behavior
The wizard is desktop-first (operator is typically on a desktop browser when standing up
infrastructure), but must be usable on a phone if needed.
- Desktop (≥768px): card centered at maxWidth 540px; step indicator spans full card width
- Phone (<768px): card fills viewport minus 24px horizontal padding;
step indicator uses short labels (1, 2, 3, 4) or icon-only to avoid overflow;
no bottom tab bar (not rendered at all on wizard page)
- No BottomTabBar, no AppNav on this page at any breakpoint
---
## Security Display Rules
These are hard UI rules, not implementation notes:
- The wizard never displays generated secret values. Secrets (SESSION_SECRET,
APP_PASSWORD_ENCRYPTION_KEY, VAPID_PRIVATE_KEY, VAPID_PUBLIC_KEY) are generated pre-boot by
the `npm run generate-secrets` helper and pasted into Docker env by the operator. The wizard
only collects the VAPID public key (non-secret) as a form field.
- App password in Step 3: `type="password"` — never visible
- No `dangerouslySetInnerHTML` anywhere on this page (T-05-24 project convention)
---
## Registry Safety
| Registry | Blocks Used | Safety Gate |
|----------|-------------|-------------|
| shadcn official | none | not applicable |
| third-party | none | not applicable |
No third-party registries. All components are hand-rolled following existing project convention.
lucide-react icons are already installed (`KeyRound`, `ShieldCheck`, `Loader2`, `CheckCircle`,
`AlertCircle`, `Copy`, `Check` are all available or trivially added from the existing
lucide-react dependency).
---
## Pre-Population Sources
| Decision | Source | Value |
|----------|--------|-------|
| Spacing scale | apps/pwa/src/styles/tokens.css | --space-1 through --space-12 |
| Typography scale | apps/pwa/src/styles/tokens.css | 4 sizes (13/15/18/24px), 2 weights (400/600) |
| Color palette | apps/pwa/src/styles/tokens.css | All hex values |
| Component library | apps/pwa (convention) | Hand-rolled inline styles, no shadcn |
| Icon library | apps/pwa imports | lucide-react |
| Button style | CredentialSheet.tsx / AdminPage.tsx | Filled accent for primary, ghost for secondary |
| Input style | CredentialSheet.tsx | 12px/16px padding, 4px border-radius, destructive border on error |
| Section label style | AdminPage.tsx | 13px/600/uppercase/0.06em letter-spacing |
| Card padding | AdminPage.tsx | var(--space-12) top/bottom, var(--space-6) horizontal |
| Credential copy | CredentialSheet.tsx | Same field layout, same validation feedback pattern |
| Step 3 fields | CredentialSheet.tsx | Exact field structure, labels, helper text, link |
| Step count (4 not 5) | CONTEXT.md D-04/D-05 | Step 2 "Generate Secrets" dropped — generation pre-boot |
| Step 2 input fields | CONTEXT.md D-02 | Non-secret config collected in wizard, written to app_config |
| No secrets in wizard | CONTEXT.md D-01/D-05 | Kernel secrets (DB, SESSION, ENCRYPTION_KEY, VAPID_PRIVATE) stay in env |
| Step labels | Claude's Discretion (CONTEXT.md) | Welcome / Instance / Calendar / Complete |
---
## Checker Sign-Off
- [ ] Dimension 1 Copywriting: PASS
- [ ] Dimension 2 Visuals: PASS
- [ ] Dimension 3 Color: PASS
- [ ] Dimension 4 Typography: PASS
- [ ] Dimension 5 Spacing: PASS
- [ ] Dimension 6 Registry Safety: PASS
**Approval:** pending