Files
relicario/docs/superpowers/specs/2026-05-03-v0.5.x-ux-polish-and-recovery-qr-design.md
adlee-was-taken 4dc034d846 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>
2026-05-03 19:32:43 -04:00

358 lines
18 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.
# 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.