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

18 KiB
Raw Blame History

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:

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:

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

// 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 · "), 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.