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:
+135
-1
@@ -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 -------------------------------- */
|
||||
|
||||
Reference in New Issue
Block a user