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>
358 lines
18 KiB
Markdown
358 lines
18 KiB
Markdown
# 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 `⤴` 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 ⤴)
|
||
```
|
||
|
||
Remove the inline `⤴` 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 |
|
||
| 720–960px | 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.
|