Files
relicario/docs/superpowers/specs/2026-04-30-relicario-fullscreen-ux-redesign-design.md
adlee-was-taken ad2c0f9e24 docs(specs): fullscreen UX redesign — layout, polish, smart inputs, power-user features
Captures the brainstorm output for the fullscreen vault tab: two-column login
form with sticky save bar, monospace-coherent glyph buttons, eight smart-input
affordances (fill-from-tab, hostname chip, group autocomplete, password reveal
& strength, TOTP live preview, TOTP-from-QR, notes monospace), and seven
power-user features (three-pane shell, keyboard nav, ⌘K palette, unsaved guard,
multi-select bulk ops, drag-drop attach, recent items).

Includes a CLI-parity section pairing each extension capability with its CLI
counterpart so the surfaces ship together.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-30 20:10:33 -04:00

456 lines
24 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.
# Relicario fullscreen UX redesign
**Date:** 2026-04-30
**Status:** Spec, awaiting review
**Surface:** Browser extension fullscreen vault UI (`extension/src/vault/`)
## Goals
- Make the fullscreen vault tab (`vault.html`) feel like a first-class app, not a popup form stretched across a wide monitor.
- Add structural affordances (keyboard nav, command palette, multi-select) that the popup cannot fit and that match the project's monospace/terminal aesthetic.
- Improve form-level affordances (smart inputs) in a way the popup can also adopt where space allows.
- Establish a consistent visual language — typography, glyphs, focus states, button conventions — shared between popup and fullscreen.
## Non-goals
- Sidebar/empty-state rework (deliberately out of scope; current sidebar layout stays as-is).
- Mobile responsive design (fullscreen is desktop-only; popup handles narrow widths).
- New item types, schema changes, or sync-protocol changes.
- Theme system / light mode (single dark theme stays).
## Scope summary
| Theme | Where it applies |
|---|---|
| **A.** Two-column form layout, sticky save bar | Fullscreen only |
| **B.** Visual polish: glyphs, focus rings, required pill, "esc to cancel" subtitle | Both (popup adopts what fits) |
| **C.** Smart inputs (8 affordances) | Both — same code path in `popup/components/types/login.ts` |
| **E.** Keyboard nav, ⌘K palette, three-pane shell, multi-select, drag-drop attach, unsaved-changes guard, recent items | Fullscreen only |
| **Glyph button convention** | Both (icons-only, native tooltips) |
---
## Architecture
### Component map (after redesign)
```
extension/src/vault/
├── vault.ts # entry point — restructured for 3-pane shell
├── vault.html # split panes: nav | list | detail
├── vault.css # restyled — see "visual language" below
├── shell/
│ ├── three-pane.ts # NEW — pane sizing, divider drag
│ ├── keymap.ts # NEW — global keyboard handler
│ ├── command-palette.ts # NEW — ⌘K overlay
│ └── unsaved-guard.ts # NEW — beforeunload + in-app intercept
├── selection.ts # NEW — multi-select state
└── components/ # existing — backup-panel, import-panel
extension/src/popup/components/types/
├── login.ts # restructured form, 8 smart-input affordances
├── secure-note.ts # adopts shared visual language
├── identity.ts # ditto (later phase)
├── card.ts # ditto (later phase)
├── key.ts # ditto (later phase)
├── totp.ts # ditto (later phase)
└── document.ts # ditto (later phase)
extension/src/shared/
├── glyphs.ts # NEW — icon glyph constants & button helper
├── shortcuts.ts # NEW — keymap registry consumed by vault
└── form-affordances/ # NEW — reusable smart-input mixins
├── url-affordances.ts # fill-from-tab, hostname chip
├── group-autocomplete.ts # datalist
├── password-tools.ts # reveal toggle, strength bar
└── totp-tools.ts # live preview, QR decode
```
### Data flow
No changes to the message-bus contract. New SW handlers needed:
- `get_active_tab_url` — popup-only message; SW reads `chrome.tabs.query({active:true, lastFocusedWindow:true})`, returns `{ url, title }`. Used by URL fill-from-tab affordance.
- `list_groups` — popup-only; reads manifest, returns deduplicated set of all group strings (for datalist autocomplete).
- `list_recently_viewed` — popup-only; returns last N item IDs from a per-device LRU stored in `chrome.storage.local`.
Existing handlers (`rate_passphrase`, `get_totp`, `add_item`, etc.) are reused as-is.
### Dependencies
- **`jsqr`** — `~50KB` minified. QR-image → otpauth-URI decoder for TOTP-from-QR. Loaded lazily (only when the user clicks the `◫` button).
- No other new runtime deps. `zxcvbn` already integrated via `rate_passphrase`.
---
## Visual language
The single source of truth for shared style is `extension/src/shared/glyphs.ts` (constants) and `vault.css` / `popup.css` (CSS tokens).
### Typography
- Body: `ui-monospace, "JetBrains Mono", "SF Mono", monospace` (already present).
- Numerals: `font-variant-numeric: tabular-nums` on TOTP code, countdowns, item counts.
- Labels: lowercase, weight 400, color `var(--text-muted)`.
- Section headers (form sub-sections): uppercase, letter-spacing 1px, weight 500, with a 1px bottom border.
### Color tokens (additive — no existing colors removed)
```css
:root {
--accent: #d49b3a; /* amber, brand */
--accent-soft: rgba(212, 155, 58, 0.18);
--focus-ring: 0 0 0 2px rgba(212, 155, 58, 0.35);
--bg-input: #0e1620;
--bg-pane: #1a2230;
--border-subtle: #2a3848;
--text: #cdd6e0;
--text-muted: #8b97a8;
--text-dim: #6b7888;
--danger: #c75a4f;
--success: #6cb37a;
}
```
### Glyph convention
All action glyphs are unicode (no emoji), monochrome, with `title=` tooltips. Defined as constants in `shared/glyphs.ts`:
| Glyph | Constant | Use |
|---|---|---|
| `⊙` / `⊘` | `GLYPH_REVEAL` / `GLYPH_HIDE` | Password reveal toggle |
| `↻` | `GLYPH_GENERATE` | Password / passphrase generate |
| `⤓` | `GLYPH_FILL_FROM_TAB` | Fill URL from active tab |
| `◫` | `GLYPH_QR` | Paste/upload QR image |
| `≡` | `GLYPH_MONO` | Toggle notes monospace |
| `▦` | `GLYPH_TRASH` | Trash nav (replaces 🗑) |
| `⌬` | `GLYPH_DEVICES` | Devices nav (replaces 📺) |
| `⚙` | `GLYPH_SETTINGS` | Settings nav (kept) |
| `⏻` | `GLYPH_LOCK` | Lock nav (replaces 🔒) |
| `⌘ K` | (literal) | Command palette label |
Buttons use a shared `.glyph-btn` class: 28px min-width, monospace, neutral background, hover lift.
### Focus state
Single token `--focus-ring` applied to all focusable form elements via `:focus-visible`. Browser default outline is suppressed. Combined with a 1px amber border on the focused input.
### Required-field pill
Replaces the trailing `*` marker. A `<span class="req-pill">required</span>` after the label text:
```css
.req-pill {
display: inline-block; font-size: 9px; padding: 1px 5px;
background: var(--accent-soft); color: var(--accent);
border-radius: 2px; margin-left: 6px; vertical-align: middle;
text-transform: uppercase; letter-spacing: 0.5px;
}
```
---
## A. Form layout (fullscreen only)
The fullscreen `vault.html` form pane gets a two-column layout for login items. Other types stay single-column for now.
### Layout rules
```
┌────────────────────────────────────────────────────────────┐
│ ◀ new login ⌘+S to save │
│ unsaved · esc to cancel │
├──────────────────────────┬─────────────────────────────────┤
│ IDENTITY │ CREDENTIALS │
│ ┌──────────────────────┐ │ ┌─────────────────────────────┐ │
│ │ title [required] │ │ │ username │ │
│ │ url + ⤓ │ │ │ password ⊙ ↻ │ │
│ │ group (autocomplete) │ │ │ strength: ████░ │ │
│ └──────────────────────┘ │ │ totp secret ◫ │ │
│ │ │ live: 492 837 · 23s │ │
│ │ └─────────────────────────────┘ │
├──────────────────────────┴─────────────────────────────────┤
│ NOTES │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ ... │ │
│ └────────────────────────────────────────────────────────┘ │
│ │
│ ▾ custom sections & fields ▸ attachments │
├────────────────────────────────────────────────────────────┤
│ STICKY SAVE BAR [cancel] [save] │
└────────────────────────────────────────────────────────────┘
```
- Pane content max-width: `960px`, centered horizontally in the pane.
- Two columns: equal width, 24px gap. Stack to single column under 720px viewport (degrades gracefully for narrow windows).
- Notes / custom sections / attachments are full-width below the columns.
- **Sticky save bar:** position `sticky` at the bottom of the form pane, with a fade gradient above so content scrolls under it. Always reachable, even on long forms.
### Header treatment
- Heading "new login" / "edit login" left-aligned.
- Subtitle below: "unsaved · esc to cancel" (when dirty) or "no changes" (when pristine).
- Right side: keyboard hint "⌘+S to save" (visual only — not a button).
- The popout-to-tab `⤴` button is **removed** from the fullscreen form (it's a no-op in this context). It stays in the popup form.
### Fields per item type (column assignment)
Only `login` is two-column. Other types (`secure_note`, `identity`, `card`, `key`, `totp`, `document`) remain single-column with the polish/visual-language updates applied.
---
## B. Visual polish (both surfaces, popup adopts what fits)
Six tweaks, applied via `vault.css` / `popup.css`:
1. **Popout button:** removed from fullscreen forms. Stays in popup forms.
2. **Sidebar glyphs:** emoji → unicode constants from `shared/glyphs.ts`.
3. **Required pill:** `<span class="req-pill">required</span>` replaces trailing `*`.
4. **Focus ring:** `--focus-ring` token on `:focus-visible`.
5. **Form header subtitle:** "unsaved · esc to cancel" / "no changes" status line.
6. **Rhythm:** input padding raised from 5px → 6px, line-height 1.4 → 1.5, label margin tweaks for breathing room.
The popup adopts (3), (4), (5 — minus "esc to cancel" since popup escape closes the popup). Popup keeps (2) sidebar glyphs. Layout (sticky bar / two-column) does not apply to popup.
---
## C. Smart inputs (both surfaces)
Each affordance lives in `shared/form-affordances/` so the popup and fullscreen forms call the same module.
### C1. Fill URL from current tab
- New SW message: `get_active_tab_url``{ url: string, title: string } | null`. Uses `chrome.tabs.query({active:true, lastFocusedWindow:true})`, filters out `chrome://` / extension URLs.
- Glyph button `⤓` next to the URL input. Click → fetch → set URL field; if title field is empty, set title too.
- No-op (button disabled) if no usable active tab (e.g., user opened vault.html and no other tab).
### C2. Hostname chip next to URL
- Live: parse the URL with `URL` constructor on each input event (debounced 200ms).
- If it parses, show a chip with the first letter of the hostname on a colored background + the bare hostname underneath the input.
- No network fetch. No favicon download. Pure visual confirmation.
### C3. Group autocomplete (datalist)
- New SW message: `list_groups``{ groups: string[] }`. Reads `state.manifest.items`, collects unique non-empty `group` values, sorts.
- Form's group input gets `<datalist>` attribute. Browser handles dropdown UI.
- One round-trip on form open; cached for the form's lifetime.
### C4. Password reveal toggle
- Glyph button `⊙` (hidden) / `⊘` (revealed) next to password input.
- Click toggles `input.type` between `password``text` and swaps glyph.
- Resets to `password` when the form is unmounted (paranoia: don't leak revealed-state across navigation).
### C5. Inline strength bar (zxcvbn)
- Below password input: 5-segment bar + label "strength: weak / fair / good / strong · ~10ⁿ guesses".
- Drives off existing `rate_passphrase` SW message. Debounced 150ms (already done in `setup-helpers.ts`; reuse the helper).
- Color: red (score 1) → amber (score 2-3) → green (score 4) per existing palette.
### C6. TOTP live code preview
- Below the totp-secret input: when the field contains a valid base32 string (length ≥ 16, charset `A-Z2-7`), show "492 837 · 23s" in a dashed-bordered preview box.
- Drives off a new SW message: `preview_totp``{ code, expires_at }`. Or reuse `get_totp` with a transient secret. **Preferred:** new `preview_totp_from_secret { secret_b32 }` so we don't pollute the get_totp path with unsaved data.
- Updates every second (interval ticker, torn down on unmount).
### C7. TOTP from QR image (paste / upload)
- Glyph button `◫` opens a small inline panel with three sources:
1. **Paste:** listen for `paste` event on the panel; extract image from clipboard.
2. **Upload:** `<input type=file accept=image/*>`.
3. **Drop:** drag image into the panel area.
- Lazy-load `jsqr` (`import('jsqr')` only when panel opens). Decode → if URI starts with `otpauth://`, parse the `secret` query param → fill the totp-secret field.
- On failure: inline error "no QR found" / "not a TOTP URI".
### C8. Notes monospace toggle
- Small glyph button `≡` near the notes label. Toggles `font-family` between body and `ui-monospace` for the textarea.
- Persisted per-item in `chrome.storage.local` keyed by item ID (purely a display preference, not encrypted state).
---
## E. Power-user features (fullscreen only)
### E1. Three-pane shell
```
┌─────┬──────────────────┬──────────────────────────────────┐
│ NAV │ LIST + SEARCH │ DETAIL / FORM │
│ │ │ │
│ + │ /search │ ... │
│ ▦ │ ─────────────── │ │
│ ⌬ │ GitHub │ │
│ ⚙ │ GitLab │ │
│ ⏻ │ Reddit │ │
│ │ ... │ │
└─────┴──────────────────┴──────────────────────────────────┘
60px 320px (resizable) flex: 1
```
- Leftmost pane (60px): icon-only nav (`+ new`, `▦ trash`, `⌬ devices`, `⚙ settings`, `⏻ lock`). Hover tooltips show labels.
- Middle pane (320px default, resizable via drag divider, persisted in `chrome.storage.local`): search input + item list.
- Right pane (fills remaining width): current view (detail, form, settings, devices, etc.).
- Resizable divider between middle and right panes; min 240px / max 60% of viewport.
Migration from current 2-pane: extract the bottom nav buttons from the sidebar into the new leftmost pane. Existing list rendering moves to the middle pane unchanged.
### E2. Keyboard navigation
A new `extension/src/vault/shell/keymap.ts` registers a single global keydown handler. Shortcuts only fire when no input/textarea is focused (or `/` always focuses search):
| Key | Action |
|---|---|
| `j` / `↓` | Next item in list |
| `k` / `↑` | Previous item in list |
| `Enter` | Open detail of selected item |
| `e` | Edit currently-open item |
| `/` | Focus search |
| `Esc` | Close detail / cancel form / clear search |
| `⌘N` / `Ctrl+N` | New item (open type-selection) |
| `⌘L` / `Ctrl+L` | Lock vault |
| `⌘S` / `Ctrl+S` | Save current form (when editing/adding) |
| `⌘K` / `Ctrl+K` | Open command palette |
| `gg` | Jump to top of list |
| `G` | Jump to bottom of list |
| `x` | Toggle multi-select on focused list row |
Implementation: small dispatch table; consumers register handlers tagged by view (`list`, `detail`, `form`); the keymap module routes based on current view + focus state.
### E3. Command palette (⌘K)
- Modal overlay, centered, ~520px wide.
- Input at top; fuzzy-matches against all decrypted item titles + URLs + groups + a handful of static actions ("new login", "lock", "open settings", etc.).
- Up/down arrow + enter to select; ⌘K or Esc to close.
- Implementation: simple substring + token matching (no third-party fuzzy lib). Renders top 8 results.
- Actions executed via the existing `navigate()` host method.
### E4. Unsaved-changes guard
- New `extension/src/vault/shell/unsaved-guard.ts` exports `setDirty(dirty: boolean)` / `isDirty()`.
- Form components call `setDirty(true)` on any input change, `setDirty(false)` on save/cancel/initial render.
- Browser tab close: `window.addEventListener('beforeunload', e => isDirty() && e.preventDefault())`.
- In-app navigation: `navigate()` host method checks `isDirty()`, shows a toast confirmation ("Discard changes?" — keep editing / discard).
### E5. Multi-select bulk operations
- New `extension/src/vault/selection.ts` holds a `Set<ItemId>` of selected items.
- List rows render a checkbox (only visible on hover, or always when ≥1 item selected).
- Shift-click a row toggles selection. `x` keymap toggles focused row.
- Footer action bar appears when selection is non-empty: "N selected" + buttons (move to group, trash).
- Bulk operations call existing per-item handlers in a loop, with a single manifest write at the end. SW handler: `bulk_trash_items` and `bulk_move_to_group` to keep the round-trips down.
### E6. Drag-drop attachments anywhere on form
- The whole form pane becomes a drop target when a drag enters with `dataTransfer.types.includes('Files')`.
- Overlay shows "⤓ drop to attach" with the per-attachment size cap.
- Drop → forwards files to existing `attachments-disclosure.ts` upload pipeline, which already handles encryption and SW round-trip.
### E7. Recent items in sidebar
- New SW message: `record_view_item { id }` (called when detail pane renders an item) and `list_recently_viewed { limit }` (called by sidebar on render).
- Backed by an LRU in `chrome.storage.local` (per-device, NOT in the encrypted vault — leaks no data because only IDs are stored, and IDs are random opaque strings).
- Sidebar shows a "recent" mini-section above the main list (last 3 items, collapsible).
---
## Parity matrix (popup vs fullscreen)
| Feature | Popup | Fullscreen |
|---|---|---|
| Two-column form layout | — | ✓ (login only) |
| Sticky save bar | — | ✓ |
| Header subtitle | "esc to close" | "esc to cancel · ⌘+S to save" |
| Popout-to-tab button | ✓ | — |
| Sidebar glyphs | ✓ | ✓ |
| Required pill | ✓ | ✓ |
| Focus ring | ✓ | ✓ |
| Smart inputs (C1C8) | ✓ | ✓ |
| Three-pane shell | — | ✓ |
| Keyboard nav | — | ✓ |
| Command palette | — | ✓ |
| Unsaved-changes guard | — | ✓ (popup auto-closes on Esc; loss is implicit) |
| Multi-select bulk ops | — | ✓ |
| Drag-drop attachments | partial (existing) | ✓ (whole form pane) |
| Recent items section | — | ✓ |
---
## Implementation phases (suggested split)
The work is large enough to want phased landings. Each phase is independently shippable.
### Phase 1: Visual foundation
- `shared/glyphs.ts`, color tokens, focus ring, required pill, sidebar glyph migration, popout button removal in fullscreen.
- Touches both popup and fullscreen CSS.
- Smallest, lowest-risk; sets the visual baseline for everything else.
### Phase 2: Form layout + smart inputs
- `shared/form-affordances/` modules.
- Two-column login form in fullscreen, sticky save bar, header subtitle.
- All 8 smart inputs wired in `login.ts` (touches popup too).
- New SW messages: `get_active_tab_url`, `list_groups`, `preview_totp_from_secret`.
- Lazy-load `jsqr` for QR decode.
### Phase 3: Three-pane shell + keyboard nav
- `vault/shell/three-pane.ts`, `keymap.ts`, `unsaved-guard.ts`.
- Restructure `vault.html` and `vault.ts` for the new shell.
- All shortcuts wired.
### Phase 4: Command palette + multi-select + drag-drop + recent items
- `vault/shell/command-palette.ts`, `vault/selection.ts`.
- Drag-drop attach overlay.
- `record_view_item` / `list_recently_viewed` SW handlers.
- Bulk SW handlers: `bulk_trash_items`, `bulk_move_to_group`.
---
## Testing approach
Existing `vitest` setup with `happy-dom` is sufficient for the new components. Per-phase test additions:
- **Phase 1:** Snapshot test for `shared/glyphs.ts` constants. Visual regression: manual.
- **Phase 2:** Per-affordance unit tests in `shared/form-affordances/__tests__/`. Each tests the parse/format logic and DOM mutation in isolation. Form integration test that mounts the login form and exercises all 8 affordances.
- **Phase 3:** Keymap dispatch table tests (verify each key resolves to the right handler given current view+focus). Three-pane shell test: mount, simulate divider drag, verify width persistence.
- **Phase 4:** Command palette fuzzy-match tests (input → expected result ordering). Multi-select selection-state tests. Bulk-op handler tests (router.test.ts pattern).
No new e2e infrastructure; manual QA pass per phase with the rebuilt extension loaded in Chrome.
---
## CLI parity
The user's design philosophy: every user-facing capability lands on **both** CLI and extension together. Most of this spec is UI-shaped (form layout, three-pane shell, command palette, drag-drop) and has no CLI counterpart by nature. The remaining items where this design introduces a genuine parity gap:
| Feature | CLI counterpart | Status |
|---|---|---|
| **C3** group autocomplete | `relicario` clap completion script with dynamic group enumeration for `--group <TAB>` | **In scope** — bundle with C3 |
| **C5** password strength bar | New `relicario rate <passphrase>` subcommand printing zxcvbn score + guess count | **In scope** — bundle with C5 |
| **C7** TOTP from QR | New flag `relicario add login --totp-qr <path-to-image>` (and `edit`) | **In scope** — bundle with C7 |
| **E5** multi-select bulk ops | `relicario rm <q1> <q2> ...` (vararg) and bulk move/group counterparts | **In scope** — bundle with E5 |
| **E7** recent items | `relicario list --recent <N>` flag (LRU stored in vault dir) | **In scope** — bundle with E7 |
Items with parity already satisfied:
- **C4 password reveal** ↔ existing `get --show` flag
- **C6 TOTP code preview** ↔ existing `get` (`get` always shows the code; live preview is UI-only nicety)
- **C8 notes monospace** ↔ CLI prints monospace by default
- **E2 keyboard nav** ↔ CLI is keyboard-native
- **E3 command palette** ↔ CLI subcommand discovery via `--help`
- **E4 unsaved guard** ↔ CLI is single-action per invocation; nothing to lose
- **E6 drag-drop attach** ↔ existing `attach <id> <file>`
The CLI counterparts above land in the same phase as their extension counterpart (e.g., `rate` subcommand ships in Phase 2 with C5, not as a follow-up). The implementation plan must pair them.
---
## Out of scope / deferred
- Sidebar empty-state ("no items" CTA, etc.) — explicitly skipped per brainstorm.
- Light theme / theme picker.
- Mobile / narrow fullscreen layouts (under 720px).
- Vim-style chord shortcuts beyond `gg` / `G`.
- Pinned/favorite items as a sidebar section (favorite field already exists; not surfacing it differently right now).
- Auto-save drafts (unsaved guard catches the common case; full draft persistence is a separate effort).
- Form-level diff view ("you changed 3 fields") — would be nice but not asked for.