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

24 KiB
Raw Blame History

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)

: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:

.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 passwordtext 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.