feat: entry templates + tag autocomplete + slideover push + robustness bundle

- Entry templates: new vault_entries.template column drives a typed
  sub-kind ('credit-card', 'ssh-key', 'server', 'recovery-codes'). Card
  + table label off the template, badge reads "credit card" instead of
  "note". Templates seed kind=note (no site/password required), use
  custom_fields with optional dropdown options (brand, month/year,
  protocol). Round-tripped across export/import/duplicate/master-pw
  rotation, preserved by partial PUTs via a HasTemplate flag.
- Custom fields: support per-field `options[]` rendering as <select>
  (card brand, expiry MM/YYYY, SSH/server protocol).
- Tags: existing-tag autocomplete dropdown under the chip input,
  filtered against what's already selected.
- Search history: per-query X for individual delete + 1s debounced
  commit (no Enter required).
- Slideover: clicking outside closes again (drag-selection respected
  via mousedown origin tracker), Esc closes, X closes. App shell is
  pushed left by 420px when the panel is open so the table / pagination
  / sort / search stay visible and interactive.
- Export/import: JSON now round-trips custom_fields, attachments
  (decrypted to base64, re-encrypted under current key on restore),
  icon_b64, and template. CSV warning lists what's not included.
- Auto-backup: same payload shape as user-driven export.
- Notes: import (JSON + CSV) accepts kind=note with empty site,
  preserves title/template/custom_fields. CSV parser detects kind/
  template columns.
- Bulk-import response returns `ids[]` parallel to input so the
  client can map back to new entry IDs (drives attachment restore).
- Move-to-folder bugs fixed: moveEntryToFolder, batchMoveToFolder,
  addTag, batchAddTag were all silently wiping TOTP / custom_fields
  / kind / template via partial PUT. Now re-ship full payload.
- Master-pw rotation: server mints a fresh session token + csrf so
  the very next request after rotation no longer ESessionRejects.
  Client adopts the new pair. Attachments are re-encrypted client-side
  during rotation (GET old → decrypt with old key → encrypt with new
  → PUT). New endpoints: GET /attachments/all, PUT /attachments/:id.
- Duplicate: carries icon_b64 + template + attachments to the copy.
- HandleCreateEntry: accepts icon_b64.
- FireDAC param fix: all blob/icon/custom_fields params use ftMemo +
  .Value assignment so SQLite TEXT no longer truncates to 4000 chars
  (deepseek's 200+ KB favicon was being wiped on lock/unlock).
- HandleSetEntryIcon cap: 262144 → 524288 chars (base64 of a 256 KB
  raw fetch overflows the old cap, fails silently in saveEntryIcon).
- Native save dialog: surfaces server errors instead of swallowing.
- Modals: reauth (export) + backup-password prompt support inline
  error display, retry up to 5 attempts, then hard-stop.
- Keyboard cursor (j/k): bootstraps to current page, auto-paginates
  when the cursor crosses a page boundary, Enter opens slideover.
- Slideover focuses Title on edit-open so j/k → Enter → type Just
  Works.
- TOTP tool: Esc closes the modal.
- App version + launch mode (auto/manual): exposed via bridge,
  surfaced in Settings → Account. Autostart launches suppress the
  first-time tray balloon.
- Passkey button hidden (Delphi backend stubs WebAuthn at 501).
- TEST_PLAN.md captured for regression coverage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-06-26 21:20:07 +01:00
parent fa7ea191be
commit e23a78dda7
14 changed files with 1752 additions and 203 deletions
+135 -1
View File
@@ -304,6 +304,35 @@ input[type="range"]::-webkit-slider-thumb {
box-shadow: 0 0 0 3px var(--accent-soft);
background: var(--bg);
}
/* Existing-tag autocomplete dropdown anchored under the chip-input. */
.tag-suggest {
position: absolute;
top: 100%; left: 0; right: 0;
margin-top: 4px;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
z-index: 60;
overflow: hidden;
max-height: 240px;
overflow-y: auto;
}
.tag-suggest.is-hidden { display: none; }
.tag-suggest-item {
display: flex; align-items: center; gap: 8px;
width: 100%;
padding: 7px 10px;
background: transparent;
border: none;
color: var(--text);
font-size: 13px;
text-align: left;
cursor: pointer;
}
.tag-suggest-item:hover { background: var(--bg-elev-2); }
.tag-suggest-item svg { width: 12px; height: 12px; color: var(--text-faint); }
.chip-input input {
flex: 1;
min-width: 80px;
@@ -605,6 +634,81 @@ input[type="range"]::-webkit-slider-thumb {
opacity: 1;
}
.search kbd { position: absolute; right: 8px; }
/* Search history dropdown */
.search-history {
position: absolute;
top: calc(100% + 4px);
left: 0;
right: 0;
background: var(--bg-elev);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
box-shadow: var(--shadow-lg);
z-index: 50;
overflow: hidden;
}
.search-history.is-hidden { display: none; }
.search-history-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 10px;
border-bottom: 1px solid var(--border-soft);
font-size: 11px;
color: var(--text-faint);
text-transform: uppercase;
letter-spacing: 0.4px;
}
.search-history-clear {
background: transparent;
border: none;
color: var(--text-dim);
font-size: 11px;
cursor: pointer;
padding: 2px 6px;
border-radius: 3px;
}
.search-history-clear:hover { color: var(--danger); background: var(--danger-soft); }
.search-history-row {
display: flex;
align-items: stretch;
}
.search-history-row:hover { background: var(--bg-elev-2); }
.search-history-item {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
min-width: 0;
padding: 8px 12px;
background: transparent;
border: none;
color: var(--text);
font-size: 13px;
cursor: pointer;
text-align: left;
}
.search-history-item svg { width: 13px; height: 13px; color: var(--text-faint); }
.search-history-del {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
flex-shrink: 0;
background: transparent;
border: none;
color: var(--text-dim);
cursor: pointer;
transition: color var(--t-fast), background var(--t-fast);
}
.search-history-del {
font-size: 18px;
line-height: 1;
font-weight: 400;
}
.search-history-row:hover .search-history-del { color: var(--text); }
.search-history-del:hover { color: var(--danger); background: var(--danger-soft); }
.topbar-actions { display: flex; align-items: center; gap: 8px; margin-left: auto; }
/* View toggle (Cards / List) */
@@ -709,6 +813,13 @@ input[type="range"]::-webkit-slider-thumb {
background: var(--accent-soft);
box-shadow: inset 0 0 0 2px var(--accent);
}
/* Keyboard cursor (j/k nav) — distinct from selected so a focused card
stays visible after the slideover closes. */
.entry-card.is-cursor,
.entry-row.is-cursor {
outline: 2px solid var(--accent);
outline-offset: -2px;
}
/* === List view layout === */
/* Cards are flattened into a single horizontal row using flexbox + `order`.
@@ -904,7 +1015,7 @@ input[type="range"]::-webkit-slider-thumb {
font-size: 12px;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
.entry-fav, .entry-del, .entry-dup {
.entry-fav, .entry-del, .entry-dup, .entry-pin {
color: var(--text-faint);
background: transparent; border: none;
padding: 4px;
@@ -913,6 +1024,8 @@ input[type="range"]::-webkit-slider-thumb {
}
.entry-fav.is-on { color: var(--warning); }
.entry-fav:hover { color: var(--warning); transform: scale(1.1); }
.entry-pin.is-on { color: var(--accent); }
.entry-pin:hover { color: var(--accent); transform: scale(1.1); }
.entry-dup {
color: var(--text-dim);
opacity: 0;
@@ -1733,6 +1846,18 @@ input[type="range"]::-webkit-slider-thumb {
}
.new-entry-menu .dropdown-item:hover { background: var(--accent-soft); }
.new-entry-menu .dropdown-item svg { width: 14px; height: 14px; }
.new-entry-menu .dropdown-sep {
height: 1px;
background: var(--border-soft);
margin: 4px 0;
}
.new-entry-menu .dropdown-section {
padding: 4px 10px 2px;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text-faint);
}
/* ---- Quick search modal (tray menu) ------------------ */
.quick-search-panel {
@@ -1861,6 +1986,12 @@ input[type="range"]::-webkit-slider-thumb {
z-index: 50;
}
.slideover.is-open { transform: translateX(0); }
/* Push the app shell to the left when the slideover is open, so the
table, sort buttons, pagination and search bar are no longer hidden
behind it. Width matches .slideover (420px) — bumped to 100% via the
responsive override below 720px. */
body:has(.slideover.is-open) #appShell { margin-right: 420px; }
#appShell { transition: margin-right var(--t-base); }
.slideover-header {
display: flex; align-items: center; justify-content: space-between;
padding: 16px 20px;
@@ -2156,6 +2287,9 @@ input[type="range"]::-webkit-slider-thumb {
.topbar { padding: 12px 16px; }
.content { padding: 16px; }
.slideover { width: 100%; }
/* On a narrow viewport the slideover fills the screen — no point
pushing the shell, the table isn't visible anyway. */
body:has(.slideover.is-open) #appShell { margin-right: 0; }
}
/* ---- 17. AUTOFILL PICKER -------------------------------- */