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>
24 KiB
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 readschrome.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 inchrome.storage.local.
Existing handlers (rate_passphrase, get_totp, add_item, etc.) are reused as-is.
Dependencies
jsqr—~50KBminified. QR-image → otpauth-URI decoder for TOTP-from-QR. Loaded lazily (only when the user clicks the◫button).- No other new runtime deps.
zxcvbnalready integrated viarate_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-numson 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
stickyat 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:
- Popout button: removed from fullscreen forms. Stays in popup forms.
- Sidebar glyphs: emoji → unicode constants from
shared/glyphs.ts. - Required pill:
<span class="req-pill">required</span>replaces trailing*. - Focus ring:
--focus-ringtoken on:focus-visible. - Form header subtitle: "unsaved · esc to cancel" / "no changes" status line.
- 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. Useschrome.tabs.query({active:true, lastFocusedWindow:true}), filters outchrome:/// 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
URLconstructor 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[] }. Readsstate.manifest.items, collects unique non-emptygroupvalues, 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.typebetweenpassword↔textand swaps glyph. - Resets to
passwordwhen 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_passphraseSW message. Debounced 150ms (already done insetup-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 reuseget_totpwith a transient secret. Preferred: newpreview_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:- Paste: listen for
pasteevent on the panel; extract image from clipboard. - Upload:
<input type=file accept=image/*>. - Drop: drag image into the panel area.
- Paste: listen for
- Lazy-load
jsqr(import('jsqr')only when panel opens). Decode → if URI starts withotpauth://, parse thesecretquery 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. Togglesfont-familybetween body andui-monospacefor the textarea. - Persisted per-item in
chrome.storage.localkeyed 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.tsexportssetDirty(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 checksisDirty(), shows a toast confirmation ("Discard changes?" — keep editing / discard).
E5. Multi-select bulk operations
- New
extension/src/vault/selection.tsholds aSet<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.
xkeymap 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_itemsandbulk_move_to_groupto 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.tsupload 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) andlist_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 (C1–C8) | ✓ | ✓ |
| 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
jsqrfor QR decode.
Phase 3: Three-pane shell + keyboard nav
vault/shell/three-pane.ts,keymap.ts,unsaved-guard.ts.- Restructure
vault.htmlandvault.tsfor 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_viewedSW 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.tsconstants. 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 --showflag - C6 TOTP code preview ↔ existing
get(getalways 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.