docs(spec): v0.5.x UX polish, settings redesign, and recovery QR design

Three-stream spec for the next release train:
- Stream A: fullscreen 3-col layout, popup type-picker polish, glyphs, toasts, empty states
- Stream B: settings UX redesign with left-nav sections (Device/Vault split)
- Stream C: recovery QR crypto (Rust/WASM), setup wizard redesign (Style C), security settings tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
adlee-was-taken
2026-05-03 19:32:43 -04:00
parent 3021ef9d9f
commit 4dc034d846

View File

@@ -0,0 +1,357 @@
# v0.5.x — UX Polish, Settings Redesign & Recovery QR — Design
**Date:** 2026-05-03
**Status:** Draft
**Target:** v0.5.1 / next release train
## Overview
Three parallel streams building on the v0.5.0 base:
- **Stream A — Fullscreen + popup layout polish** — fullscreen vault tab gets a new 3-column layout (sidebar with type-category nav, full-width list, slide-in detail drawer); popup gets a polished type-picker; glyph additions; toast system; empty states.
- **Stream B — Settings UX redesign** — replace the current flat settings dump with a left-nav sectioned settings page; Security section with trusted-devices and Recovery QR integration.
- **Stream C — Recovery QR + setup wizard** — implement the recovery QR cryptographic feature (Rust core + WASM); integrate into the setup wizard's final step; wire into the vault-tab Security settings section.
Streams A and B share no files with Stream C (Rust/WASM). A and B share only `glyphs.ts` and `styles.css`; all other files are disjoint. All three can run in parallel.
---
## Stream A — Fullscreen + Popup Layout Polish
### A1. Fullscreen vault tab — 3-column layout
**Current state.** `extension/src/vault/vault.ts` renders a fixed sidebar (~220px) with brand, search, item list, and bottom nav buttons. Clicking `+ new item` navigates to the type-picker. The main pane shows the selected item in a single-column layout.
**New layout.**
```
┌─────────────┬──────────────────────────┬──────────────────────────┐
│ sidebar │ full-width list │ detail drawer (440px) │
│ (200px) │ (flex: 1) │ slides in on row click │
└─────────────┴──────────────────────────┴──────────────────────────┘
```
**Sidebar changes:**
- Replaces the current flat item list with **type-category nav**: all items are listed by section (Logins N, Secure Notes N, Cards N, Identities N, TOTP N, Keys N, Documents N) plus an "All items" entry at the top.
- Search bar stays above the category list.
- Bottom nav buttons remain ( new item, ▦ trash, ⌬ devices, ⚙ settings, ⏻ lock) — the `+ new item` button triggers the bottom sheet (see A3).
- `⧉` replaces the current `&#x2934;` pop-out button in the **popup toolbar only** — it stays in the popup toolbar and is not added to the fullscreen sidebar (you're already there).
**Full-width list:**
- Each row: 32px type icon (rounded, gold-tinted on selection) + title (13px) + subtitle (URL or type description, 11px muted) + `last-modified` age (10px dim, right-aligned).
- Clicking a row: highlights the row and slides in the detail drawer from the right. The list narrows to accommodate the 440px drawer — flex layout handles this naturally.
- Active row stays highlighted while drawer is open.
**Detail drawer (440px):**
- Header: type pill (e.g. `LOGIN`) left, action buttons right (`edit`, `history`, `copy pwd` where applicable), `✕` close.
- Body: title (18-20px bold) + subtitle (URL/description, muted), then a **2-column field grid** for sibling fields (username/password, first/last name, number/expiry, etc.). Full-width spans for URL, notes, address, and any field without a natural pair.
- Close (`✕` or Esc): drawer slides out, list returns to full width.
- At ≤ 720px viewport: drawer pushes full-page (list hidden), back breadcrumb `← <Section>` navigates back.
**Files affected:**
- `extension/src/vault/vault.ts` — full layout rewrite (sidebar list → category nav, main pane wiring, drawer state)
- `extension/src/vault/vault.css` — layout rules for 3-column, drawer, list rows, responsive breakpoint
### A2. Fullscreen vault tab — "new item" bottom sheet
**Current state.** Clicking `+ new item` in the sidebar sets `state.newType = null` and calls `renderPane()` which renders the type-picker inline in the main pane.
**New behaviour.** A bottom sheet slides up from the bottom edge of the **main pane** (pane-only scrim — sidebar stays interactive).
- Sheet structure: drag handle, "New item — choose type" label, 7-item type grid (Login, Secure Note, TOTP, Card, Identity, SSH/API Key, Document) as cards with large glyph (28px), name (11px muted). Selected type border turns gold on hover.
- Clicking a type: sheet closes, main pane renders the add form for that type.
- Dismissing (Esc, click scrim, `✕`): sheet closes, main pane returns to previous state.
- Scrim covers the main pane only (not the sidebar). Sidebar nav remains clickable.
**Files affected:**
- `extension/src/vault/vault.ts` — sheet trigger, render, dismiss logic
- `extension/src/vault/vault.css` — sheet, scrim, type-card styles
### A3. Popup — polished type-picker page
**Current state.** `+ new` button in the popup toolbar navigates directly to the `add` route. `renderItemForm` is called with `state.newType = null`, which presumably renders a type picker inline.
**New behaviour.** Keep the current navigation model (navigate to `add` route) but upgrade the type-picker page:
- Back arrow + "New item" title in the search-bar row (replacing search input).
- 2-column grid of type cards: icon (glyph, 20px), name (12px bold), description (10px muted). E.g. "Login / Username + password", "TOTP / 2FA token".
- Glyphs not emoji for type icons (use the per-type glyph table from A5).
- `Esc` navigates back to the list.
- Keyhint bar updates to show `Esc back`.
**Files affected:**
- `extension/src/popup/components/item-list.ts``+ new` button label/glyph, keyhint
- `extension/src/popup/components/item-form.ts` (or wherever the type picker lives) — card layout, glyphs
### A4. Glyphs
Add to `extension/src/shared/glyphs.ts`:
```ts
export const GLYPH_VAULT_TAB = '⧉'; // pop-out to fullscreen vault tab (replaces &#x2934;)
```
Remove the inline `&#x2934;` from `extension/src/popup/components/item-list.ts:69` and replace with `GLYPH_VAULT_TAB`.
### A5. Item row type icons
The popup item list (`buildRowsHtml` in `item-list.ts`) currently renders title-only rows with no visual type anchor. Add a per-type glyph to each row using the item's `ManifestEntry.type` field:
| Type | Glyph |
|------|-------|
| login | `◉` |
| secure_note | `◫` |
| totp | `⊡` |
| card | `▭` |
| identity | `⌬` |
| key | `⊹` |
| document | `≡` |
Icon: 26×26px, rounded, `--bg-elevated` fill, gold-tinted border on active row.
**Files affected:** `extension/src/popup/components/item-list.ts`, `extension/src/popup/styles.css`
### A6. Empty states
Two surfaces:
1. **Popup item list, vault empty** — centered message: glyph `◈` (28px dim), "No items yet", "Press `+` to add your first item."
2. **Popup item list, search returns nothing** — centered message: glyph `⊘` (28px dim), "No results for "{query}"", "Try a shorter search term."
3. **Fullscreen list pane, section empty** — same treatment scaled for the wider pane.
**Files affected:** `extension/src/popup/components/item-list.ts`, `extension/src/vault/vault.ts`
### A7. Toast notification system
Replace the current ad-hoc `sync-status` div with a shared toast system:
- `showToast(message: string, type: 'success' | 'error' | 'info', durationMs = 2500)` in `extension/src/shared/toast.ts`.
- Toasts appear bottom-center of the popup / bottom-right of the vault tab, auto-dismiss.
- Used for: sync success/failure, copy-to-clipboard confirmation, device registration success.
**Files affected:** new `extension/src/shared/toast.ts`, `extension/src/popup/styles.css`, `extension/src/vault/vault.css`, call sites in `item-list.ts` and `vault.ts`
---
## Stream B — Settings UX Redesign
### B1. Settings page structure
Replace the current flat settings dump (`settings.ts` + `settings-vault.ts`) with a unified settings page that renders within the fullscreen vault tab's main pane (and a compact equivalent in the popup).
**Left-nav sections:**
```
Device
⊙ Autofill
◈ Display
Vault
◉ Security ← Recovery QR + trusted devices (replaces devices.ts nav)
↻ Generator
▦ Retention
⤓ Backup
≡ Import
```
Each section renders its content in the right panel. The left nav is 148px; content area fills the remainder.
**Device vs Vault distinction:**
- "Device" sections read/write `chrome.storage.local` (per-browser settings).
- "Vault" sections read/write encrypted `VaultSettings` (shared across devices via git).
**Files affected:**
- `extension/src/popup/components/settings.ts` — rewrite as sectioned layout
- `extension/src/popup/components/settings-vault.ts` — content moves into new section components
**Note on vault.ts:** DEV-B delivers the settings component with a stable export signature. The `⚙ settings` nav wiring in `vault.ts` is updated as part of Stream A's vault.ts rewrite. DEV-A and DEV-B must agree on the component's export signature before either lands.
### B2. Autofill section (Device)
Content replaces the current flat settings dump:
- **Capture** group: "Auto-detect logins" toggle (was checkbox); "Prompt style" select (bar / toast).
- **Blocked sites** group: list of blacklisted hostnames, each with a remove button. Add-hostname input at bottom.
All options use the standardised `setting-row` pattern: left (title + description), right (control).
### B3. Display section (Device)
Moves the existing password-coloring UI (digit color picker, symbol color picker, live swatch, reset) from its current location into a proper Display section card.
### B4. Security section (Vault)
**Recovery QR card** (three states, see Stream C for implementation):
- **State 1 — no QR:** amber warning ("▲ No recovery QR generated — losing your reference image would make this vault unrecoverable"), single "Generate recovery QR…" button.
- **State 2 — QR exists, at rest:** green status ("◉ Recovery QR is set up"), last-generated date. Buttons: "Show / print QR…" and "Regenerate…". **No QR is visible in this state.**
- **State 3 — explicit view:** modal overlay (scrim over main pane only). QR rendered at ~140×140px. Warning: "▲ Close this window before stepping away. This QR is only displayed, never saved." Actions: "⎙ Print" (triggers `window.print()` scoped to modal) and "Done" (dismisses).
**Trusted devices** group: subsumes the current `⌬ devices` sidebar nav entry. Each registered device shows name, registration date, fingerprint, and a revoke button. "Register this device" entry for unregistered browsers. Once Stream B lands, the `⌬ devices` button is removed from the vault sidebar nav (settings → Security replaces it).
### B5. Generator section (Vault)
Pulls the existing generator-defaults content from `settings-vault.ts` into the new section layout. No functional changes — just consistent styling.
### B6. Retention section (Vault)
Pulls the existing retention content (trash retention, field history retention). No functional changes.
### B7. Backup section (Vault)
Pulls the existing backup & restore section. No functional changes.
### B8. Import section (Vault)
Pulls the existing import section. No functional changes.
---
## Stream C — Recovery QR
### C1. Rust core — `relicario-core/src/recovery_qr.rs`
Per the existing spec at `docs/superpowers/specs/2026-05-01-recovery-qr-design.md`. Key implementation points:
**KDF input:**
```
b"relicario-recovery-v1\0" || u64_be(len(nfc(passphrase))) || nfc(passphrase)
```
Fed to Argon2id with production params (`m=64MiB, t=3, p=4`), fresh 32-byte salt per generation.
**Wrap:** `XChaCha20-Poly1305(wrap_key, nonce=OsRng(24), image_secret)` — 32+16=48 bytes ciphertext.
**Binary payload (109 bytes):**
```
[magic "RREC" 4B][version 0x01 1B][salt 32B][nonce 24B][ciphertext 48B]
```
**QR encoding:** byte mode, error-correction M, version 6 (41×41 modules). Library: `qrcode` crate (already in workspace or add it).
**API surface:**
```rust
pub struct RecoveryQrPayload { /* opaque */ }
pub fn generate_recovery_qr(
passphrase: &str,
image_secret: &[u8; 32],
) -> Result<RecoveryQrPayload, RelicarioError>;
pub fn recovery_qr_to_svg(payload: &RecoveryQrPayload) -> String;
pub fn unwrap_recovery_qr(
payload_bytes: &[u8],
passphrase: &str,
) -> Result<Zeroizing<[u8; 32]>, RelicarioError>;
```
The payload bytes are never written to disk by this module — callers are responsible for rendering only.
**Passphrase entropy floor:** enforce `zxcvbn score ≥ 3` at vault init in the CLI and the setup wizard (already gated in the extension by 1C-α; confirm CLI `create` command applies the same gate).
**Files affected:**
- `crates/relicario-core/src/recovery_qr.rs` — new module
- `crates/relicario-core/src/lib.rs` — pub mod recovery_qr
- `crates/relicario-core/src/error.rs` — add `RecoveryQr` error variants if needed
- `crates/relicario-core/Cargo.toml` — add `qrcode` crate
- `crates/relicario-core/tests/` — new `recovery_qr.rs` test file
### C2. CLI — `relicario recovery-qr` subcommand group
```
relicario recovery-qr generate # prompts passphrase, renders QR to terminal (kitty/iTerm2 inline protocol or ASCII fallback)
relicario recovery-qr unwrap # prompts passphrase, prints image_secret as hex
```
`generate` never writes a file. It renders the QR inline in the terminal using the Kitty graphics protocol if `$TERM` indicates support, falling back to ASCII art via the `qrcode` crate's built-in ASCII renderer.
**Files affected:** `crates/relicario-cli/src/main.rs`
### C3. WASM bindings
```ts
// relicario-wasm/src/lib.rs
generate_recovery_qr(passphrase: &str, image_secret: &[u8]) -> Result<String, JsValue> // returns SVG string
unwrap_recovery_qr(payload_b64: &str, passphrase: &str) -> Result<Vec<u8>, JsValue> // returns image_secret bytes
```
**Files affected:** `crates/relicario-wasm/src/lib.rs`, `crates/relicario-wasm/Cargo.toml`
### C4. Extension — Recovery QR in Security settings
Implement the three-state Security section card described in B4:
- State determined by `chrome.storage.local.recovery_qr_generated_at` (timestamp or null).
- "Generate recovery QR…" button: calls WASM `generate_recovery_qr(passphrase, image_secret)` → stores `recovery_qr_generated_at = Date.now()` in local storage → transitions to State 3 (show modal with SVG).
- "Show / print QR…" button: re-derives QR (requires vault to be unlocked, master key in session) → shows State 3 modal.
- "Regenerate…" button: same as generate, with a confirmation step first.
- Print: injects SVG into a `<iframe>` styled for print, calls `iframe.contentWindow.print()`.
**Files affected:**
- New `extension/src/popup/components/settings-security.ts`
- `extension/src/popup/components/settings.ts` — wire Security section
### C5. Extension — Recovery QR in setup wizard (Step 5 "Done")
The wizard's final step adds a **skippable banner** above the "Download reference image" button:
```
◫ Generate a recovery QR before you go
If you lose your reference image, this QR lets you recover your vault.
[Generate now] [Skip — I'll do this in Settings]
```
- "Generate now": calls WASM → shows QR modal inline on the wizard page. After dismissing, banner becomes green "◉ Recovery QR generated".
- "Skip": dismisses banner permanently for this session; user can generate later from Settings → Security.
- The banner is informational, not a blocker. Vault is fully usable without a recovery QR.
**Files affected:** `extension/src/setup/setup.ts`
### C6. Setup wizard redesign (Style C)
Redesign the setup wizard from the current single-column glass-card layout to **Style C (centered hero card)**:
- Full-page dark background (`--bg-page`).
- Relicario logo glyph + wordmark centered at top.
- **Colored progress track**: 5 segments, `--success` fill for completed, `--gold` for current, `--border` for pending.
- Centered card (max-width 560px): step eyebrow label ("Step N of 5 · <step name>"), h2 heading, hint text, form content, action row.
- **Glyphs not emoji** throughout. Mode cards use `◈` (create new) and `⌥` (attach). Mode-card glyphs at 28px. All other icons from the existing glyph set.
- Probe-banner success state uses `◉` (filled circle, matches ⊙/⊘ family).
- Action row: "◂ back" text button (left), "Continue ▸" primary button (right).
This is a pure CSS/markup change — no logic changes.
**Files affected:** `extension/src/setup/setup.ts`, setup CSS (inline or extracted)
---
## Responsive behaviour
| Viewport | Fullscreen behaviour |
|---|---|
| ≥ 960px | 3-column: sidebar + list + drawer |
| 720960px | 2-column: sidebar + list; drawer pushes full-pane on click |
| ≤ 720px | Sidebar collapses (hamburger/icon strip); list full-width; detail is full-page push |
The popup is always narrow (~340px) — popup-specific components are unaffected by the fullscreen responsive rules.
---
## Acceptance criteria (shared)
- `cargo test` green. `bun run test` green. `bun run build` + `bun run build:firefox` clean.
- No raw `snake_case` error codes in any UI surface.
- No emoji in any UI surface — all icons are Unicode monochrome glyphs.
- `glyphs.ts` is the single source of truth for all icon constants; no inline Unicode literals at call sites.
- QR code is never written to any file, `chrome.storage`, or git. `recovery_qr_generated_at` (timestamp only) is the only persisted artifact.
- Settings left-nav sections all render without console errors. Device sections read/write `chrome.storage.local`. Vault sections read/write `VaultSettings`.
---
## Stream split summary (for multi-agent kickoff)
| Stream | Owner | Core files | Dependency |
|---|---|---|---|
| A — Fullscreen + popup layout | DEV-A | `vault.ts`, `vault.css`, `item-list.ts`, `glyphs.ts` | none |
| B — Settings UX | DEV-B | `settings.ts`, `settings-vault.ts`, new `settings-security.ts` | waits for C4 interface (can stub) |
| C — Recovery QR | DEV-C | `recovery_qr.rs`, `relicario-wasm/src/lib.rs`, `setup.ts`, `settings-security.ts` | none |
B and C share `settings-security.ts` — DEV-C owns the file, DEV-B wires it into the nav. Coordinate on interface (component export signature) before DEV-B proceeds with B4.