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>
This commit is contained in:
@@ -0,0 +1,455 @@
|
|||||||
|
# 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 (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 `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.
|
||||||
Reference in New Issue
Block a user