05/19 Enhance codes 3
This commit is contained in:
@@ -2568,3 +2568,128 @@ html.sidebar-open {
|
|||||||
border-color: var(--primary);
|
border-color: var(--primary);
|
||||||
box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);
|
box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ── Bulk operations ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Bulk action toolbar */
|
||||||
|
.bulk-toolbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background: var(--surface-2, #f8fafc);
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bulk-count {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text);
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Item checkbox — hidden by default, shown in select mode */
|
||||||
|
.vault-item .item-checkbox {
|
||||||
|
display: none;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
margin-right: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
accent-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vault-list.select-mode .vault-item .item-checkbox {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected item highlight */
|
||||||
|
.vault-item.item-selected {
|
||||||
|
background: #fef2f2;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select mode — hide per-item action buttons to reduce clutter */
|
||||||
|
.vault-list.select-mode .item-actions {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Select-mode indicator on the Select button */
|
||||||
|
#btn-select-mode.active {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Folder move dropdown in bulk toolbar */
|
||||||
|
.bulk-move-select {
|
||||||
|
padding: 5px 8px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 13px;
|
||||||
|
background: var(--surface);
|
||||||
|
color: var(--text);
|
||||||
|
max-width: 160px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Password history panel (edit modal) ─────────────────────────────────── */
|
||||||
|
|
||||||
|
.password-history-panel {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 10px 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
background: var(--surface-2, #f8fafc);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 5px 0;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-masked {
|
||||||
|
font-family: monospace;
|
||||||
|
flex: 1;
|
||||||
|
color: var(--text-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-date {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-reveal,
|
||||||
|
.pw-history-restore {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pw-history-reveal:hover,
|
||||||
|
.pw-history-restore:hover {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|||||||
+259
-6
@@ -93,6 +93,9 @@ const Vault = (() => {
|
|||||||
}
|
}
|
||||||
let _activeFilter = null;
|
let _activeFilter = null;
|
||||||
let _sortOrder = "name-asc";
|
let _sortOrder = "name-asc";
|
||||||
|
// ── Bulk selection state ──────────────────────────────────────────────────
|
||||||
|
let _selectMode = false;
|
||||||
|
let _selectedIds = new Set();
|
||||||
|
|
||||||
const FIELD_LABELS = {
|
const FIELD_LABELS = {
|
||||||
url: "Website URL",
|
url: "Website URL",
|
||||||
@@ -301,6 +304,8 @@ const Vault = (() => {
|
|||||||
// ── View switching ────────────────────────────────────────────────────────
|
// ── View switching ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function switchView(view, { pushState = true } = {}) {
|
function switchView(view, { pushState = true } = {}) {
|
||||||
|
// Exit bulk-select mode when navigating away from the vault view.
|
||||||
|
if (view !== "vault" && _selectMode) _exitSelectMode();
|
||||||
_currentView = view;
|
_currentView = view;
|
||||||
["vault", "security", "sharing", "emergency", "import-export"].forEach(
|
["vault", "security", "sharing", "emergency", "import-export"].forEach(
|
||||||
(v) => {
|
(v) => {
|
||||||
@@ -453,6 +458,10 @@ const Vault = (() => {
|
|||||||
const list = document.getElementById("vault-list");
|
const list = document.getElementById("vault-list");
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
|
// Apply or remove select-mode CSS class on the parent wrapper.
|
||||||
|
const wrapper = list.closest(".vault-list");
|
||||||
|
if (wrapper) wrapper.classList.toggle("select-mode", _selectMode);
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
list.innerHTML =
|
list.innerHTML =
|
||||||
'<li class="vault-empty">No items found. Click + to add one.</li>';
|
'<li class="vault-empty">No items found. Click + to add one.</li>';
|
||||||
@@ -581,6 +590,7 @@ const Vault = (() => {
|
|||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
|
<input type="checkbox" class="item-checkbox" data-id="${item.id}" ${_selectedIds.has(item.id) ? "checked" : ""} aria-label="Select ${escHtml(item.name)}"/>
|
||||||
<div class="item-icon">${icon}</div>
|
<div class="item-icon">${icon}</div>
|
||||||
<div class="item-info">
|
<div class="item-info">
|
||||||
<span class="item-name">${escHtml(item.name)}</span>
|
<span class="item-name">${escHtml(item.name)}</span>
|
||||||
@@ -621,6 +631,23 @@ const Vault = (() => {
|
|||||||
li.dataset.totpInterval = intervalId;
|
li.dataset.totpInterval = intervalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wire the bulk-select checkbox.
|
||||||
|
const checkbox = li.querySelector(".item-checkbox");
|
||||||
|
if (checkbox) {
|
||||||
|
checkbox.addEventListener("change", () => {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
_selectedIds.add(item.id);
|
||||||
|
li.classList.add("item-selected");
|
||||||
|
} else {
|
||||||
|
_selectedIds.delete(item.id);
|
||||||
|
li.classList.remove("item-selected");
|
||||||
|
}
|
||||||
|
_updateBulkToolbar();
|
||||||
|
});
|
||||||
|
// Apply selected state from module state (survives re-renders).
|
||||||
|
if (_selectedIds.has(item.id)) li.classList.add("item-selected");
|
||||||
|
}
|
||||||
|
|
||||||
if (showLaunch) {
|
if (showLaunch) {
|
||||||
li.querySelector('[data-action="launch"]').addEventListener(
|
li.querySelector('[data-action="launch"]').addEventListener(
|
||||||
"click",
|
"click",
|
||||||
@@ -724,6 +751,171 @@ const Vault = (() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Bulk operations ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function _enterSelectMode() {
|
||||||
|
_selectMode = true;
|
||||||
|
_selectedIds.clear();
|
||||||
|
document.getElementById("btn-select-mode")?.classList.add("active");
|
||||||
|
document.getElementById("bulk-toolbar")?.classList.remove("hidden");
|
||||||
|
_updateBulkToolbar();
|
||||||
|
applyCurrentFilter(); // re-render to show checkboxes
|
||||||
|
}
|
||||||
|
|
||||||
|
function _exitSelectMode() {
|
||||||
|
_selectMode = false;
|
||||||
|
_selectedIds.clear();
|
||||||
|
document.getElementById("btn-select-mode")?.classList.remove("active");
|
||||||
|
document.getElementById("bulk-toolbar")?.classList.add("hidden");
|
||||||
|
applyCurrentFilter(); // re-render to hide checkboxes
|
||||||
|
}
|
||||||
|
|
||||||
|
function _updateBulkToolbar() {
|
||||||
|
const countEl = document.getElementById("bulk-count");
|
||||||
|
const n = _selectedIds.size;
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = n === 0
|
||||||
|
? "0 selected"
|
||||||
|
: `${n} item${n !== 1 ? "s" : ""} selected`;
|
||||||
|
}
|
||||||
|
// Disable action buttons when nothing is selected.
|
||||||
|
["btn-bulk-move", "btn-bulk-delete", "btn-bulk-export"].forEach((id) => {
|
||||||
|
const btn = document.getElementById(id);
|
||||||
|
if (btn) btn.disabled = n === 0;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _handleBulkDelete() {
|
||||||
|
const ids = [..._selectedIds];
|
||||||
|
if (!ids.length) return;
|
||||||
|
if (!confirm(`Delete ${ids.length} item${ids.length !== 1 ? "s" : ""}? This cannot be undone.`)) return;
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
ids.map((id) => apiFetch(`/api/vault/${id}`, { method: "DELETE" })),
|
||||||
|
);
|
||||||
|
showToast(`${ids.length} item${ids.length !== 1 ? "s" : ""} deleted`);
|
||||||
|
_exitSelectMode();
|
||||||
|
await loadVault();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Delete failed: " + err.message, "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _handleBulkMove() {
|
||||||
|
if (!_selectedIds.size) return;
|
||||||
|
// Build a folder picker inline in the bulk toolbar.
|
||||||
|
const toolbar = document.getElementById("bulk-toolbar");
|
||||||
|
if (!toolbar) return;
|
||||||
|
// Remove any existing picker first.
|
||||||
|
toolbar.querySelector(".bulk-move-select")?.remove();
|
||||||
|
|
||||||
|
const sel = document.createElement("select");
|
||||||
|
sel.className = "bulk-move-select";
|
||||||
|
sel.innerHTML =
|
||||||
|
'<option value="">— No folder —</option>' +
|
||||||
|
_folders.map((f) => `<option value="${f.id}">${escHtml(f.name)}</option>`).join("");
|
||||||
|
|
||||||
|
sel.addEventListener("change", async () => {
|
||||||
|
const folderId = sel.value ? parseInt(sel.value) : null;
|
||||||
|
sel.remove();
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
[..._selectedIds].map((id) =>
|
||||||
|
apiFetch(`/api/vault/${id}`, {
|
||||||
|
method: "PUT",
|
||||||
|
body: JSON.stringify({ folder_id: folderId }),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const dest = folderId
|
||||||
|
? (_folders.find((f) => f.id === folderId)?.name || "folder")
|
||||||
|
: "root";
|
||||||
|
showToast(`${_selectedIds.size} item${_selectedIds.size !== 1 ? "s" : ""} moved to ${dest}`);
|
||||||
|
_exitSelectMode();
|
||||||
|
await loadVault();
|
||||||
|
} catch (err) {
|
||||||
|
showToast("Move failed: " + err.message, "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Insert after the Move button.
|
||||||
|
const moveBtn = document.getElementById("btn-bulk-move");
|
||||||
|
moveBtn?.after(sel);
|
||||||
|
sel.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _handleBulkExport() {
|
||||||
|
const ids = new Set(_selectedIds);
|
||||||
|
if (!ids.size) return;
|
||||||
|
const vaultKey = VaultSession.getKey();
|
||||||
|
if (!vaultKey) { showUnlockOverlay(); return; }
|
||||||
|
|
||||||
|
const selected = _items.filter((i) => ids.has(i.id));
|
||||||
|
const envelope = {
|
||||||
|
version: 1,
|
||||||
|
exported_at: new Date().toISOString(),
|
||||||
|
items: selected.map((i) => ({
|
||||||
|
id: i.id,
|
||||||
|
name: i.name,
|
||||||
|
item_type: i.item_type,
|
||||||
|
folder_id: i.folder_id,
|
||||||
|
enc_data: i.enc_data,
|
||||||
|
iv: i.iv,
|
||||||
|
enc_name: i.enc_name || null,
|
||||||
|
iv_name: i.iv_name || null,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
const blob = new Blob([JSON.stringify(envelope, null, 2)], {
|
||||||
|
type: "application/json",
|
||||||
|
});
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = `passkeeper-selection-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
showToast(`${selected.length} item${selected.length !== 1 ? "s" : ""} exported`);
|
||||||
|
_exitSelectMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _initBulkToolbar() {
|
||||||
|
document.getElementById("btn-select-mode")?.addEventListener("click", () => {
|
||||||
|
if (_selectMode) _exitSelectMode();
|
||||||
|
else _enterSelectMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById("btn-bulk-cancel")?.addEventListener("click", _exitSelectMode);
|
||||||
|
|
||||||
|
document.getElementById("btn-bulk-delete")?.addEventListener("click", _handleBulkDelete);
|
||||||
|
|
||||||
|
document.getElementById("btn-bulk-move")?.addEventListener("click", _handleBulkMove);
|
||||||
|
|
||||||
|
document.getElementById("btn-bulk-export")?.addEventListener("click", _handleBulkExport);
|
||||||
|
|
||||||
|
document.getElementById("btn-bulk-select-all")?.addEventListener("click", () => {
|
||||||
|
// Select all currently visible items (from the rendered list).
|
||||||
|
const list = document.getElementById("vault-list");
|
||||||
|
if (!list) return;
|
||||||
|
const checkboxes = list.querySelectorAll(".item-checkbox");
|
||||||
|
const allChecked = [...checkboxes].every((cb) => cb.checked);
|
||||||
|
checkboxes.forEach((cb) => {
|
||||||
|
cb.checked = !allChecked;
|
||||||
|
const id = parseInt(cb.dataset.id);
|
||||||
|
if (!allChecked) {
|
||||||
|
_selectedIds.add(id);
|
||||||
|
cb.closest(".vault-item")?.classList.add("item-selected");
|
||||||
|
} else {
|
||||||
|
_selectedIds.delete(id);
|
||||||
|
cb.closest(".vault-item")?.classList.remove("item-selected");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_updateBulkToolbar();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Background vault health checks ──────────────────────────────────────────
|
// ── Background vault health checks ──────────────────────────────────────────
|
||||||
//
|
//
|
||||||
// Runs after every vault load. Computes weak/reused counts synchronously,
|
// Runs after every vault load. Computes weak/reused counts synchronously,
|
||||||
@@ -3472,6 +3664,58 @@ const Vault = (() => {
|
|||||||
}
|
}
|
||||||
document.getElementById("item-modal").classList.add("open");
|
document.getElementById("item-modal").classList.add("open");
|
||||||
document.getElementById("field-name").focus();
|
document.getElementById("field-name").focus();
|
||||||
|
|
||||||
|
// Render password history panel for password items in edit mode.
|
||||||
|
const historyPanel = document.getElementById("password-history-panel");
|
||||||
|
if (historyPanel) {
|
||||||
|
if (mode === "edit" && item?.item_type === "password") {
|
||||||
|
const history = item.plain?.password_history || [];
|
||||||
|
if (history.length) {
|
||||||
|
historyPanel.classList.remove("hidden");
|
||||||
|
historyPanel.innerHTML =
|
||||||
|
`<div class="pw-history-title">🕐 Previous passwords (${history.length})</div>` +
|
||||||
|
history.map((h) => {
|
||||||
|
const date = h.changed_at
|
||||||
|
? new Date(h.changed_at).toLocaleDateString()
|
||||||
|
: "Unknown date";
|
||||||
|
return `<div class="pw-history-row">
|
||||||
|
<span class="pw-history-masked">••••••••</span>
|
||||||
|
<span class="pw-history-date">${escHtml(date)}</span>
|
||||||
|
<button class="btn-text btn-sm pw-history-reveal" data-pw="${escHtml(h.password || "")}" title="Reveal">👁</button>
|
||||||
|
<button class="btn-text btn-sm pw-history-restore" data-pw="${escHtml(h.password || "")}" title="Restore this password">↩ Restore</button>
|
||||||
|
</div>`;
|
||||||
|
}).join("");
|
||||||
|
historyPanel.querySelectorAll(".pw-history-reveal").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
const masked = btn.closest(".pw-history-row").querySelector(".pw-history-masked");
|
||||||
|
if (masked.textContent === "••••••••") {
|
||||||
|
masked.textContent = btn.dataset.pw;
|
||||||
|
btn.textContent = "🙈";
|
||||||
|
} else {
|
||||||
|
masked.textContent = "••••••••";
|
||||||
|
btn.textContent = "👁";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
historyPanel.querySelectorAll(".pw-history-restore").forEach((btn) => {
|
||||||
|
btn.addEventListener("click", () => {
|
||||||
|
const pwField = document.getElementById("field-password");
|
||||||
|
if (pwField) {
|
||||||
|
pwField.value = btn.dataset.pw;
|
||||||
|
pwField.dispatchEvent(new Event("input"));
|
||||||
|
showToast("Password restored — click Save to apply");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
historyPanel.classList.add("hidden");
|
||||||
|
historyPanel.innerHTML = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
historyPanel.classList.add("hidden");
|
||||||
|
historyPanel.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
(mode === "add" &&
|
(mode === "add" &&
|
||||||
(document.getElementById("field-type").value || "password") ===
|
(document.getElementById("field-type").value || "password") ===
|
||||||
@@ -3594,20 +3838,28 @@ const Vault = (() => {
|
|||||||
if (mode === "add") {
|
if (mode === "add") {
|
||||||
// New item — set password_changed_at to now.
|
// New item — set password_changed_at to now.
|
||||||
plainData.password_changed_at = new Date().toISOString();
|
plainData.password_changed_at = new Date().toISOString();
|
||||||
|
plainData.password_history = [];
|
||||||
} else {
|
} else {
|
||||||
// Edit — only update if the password field actually changed.
|
// Edit — only update if the password field actually changed.
|
||||||
const existingItem = _items.find((i) => i.id === parseInt(itemId));
|
const existingItem = _items.find((i) => i.id === parseInt(itemId));
|
||||||
const existingPassword = existingItem?.plain?.password ?? null;
|
const existingPassword = existingItem?.plain?.password ?? null;
|
||||||
const existingChangedAt = existingItem?.plain?.password_changed_at ?? null;
|
const existingChangedAt = existingItem?.plain?.password_changed_at ?? null;
|
||||||
|
const existingHistory = existingItem?.plain?.password_history ?? [];
|
||||||
|
|
||||||
if (plainData.password !== existingPassword) {
|
if (plainData.password !== existingPassword) {
|
||||||
// Password changed — record now.
|
// Password changed — push the old password onto history (max 5 entries).
|
||||||
|
const historyEntry = {
|
||||||
|
password: existingPassword,
|
||||||
|
changed_at: existingChangedAt || existingItem?.created_at || new Date().toISOString(),
|
||||||
|
};
|
||||||
|
const newHistory = [historyEntry, ...existingHistory].slice(0, 5);
|
||||||
plainData.password_changed_at = new Date().toISOString();
|
plainData.password_changed_at = new Date().toISOString();
|
||||||
} else if (existingChangedAt) {
|
plainData.password_history = newHistory;
|
||||||
// Password unchanged — preserve the existing timestamp.
|
} else {
|
||||||
plainData.password_changed_at = existingChangedAt;
|
// Password unchanged — preserve existing tracking data.
|
||||||
|
if (existingChangedAt) plainData.password_changed_at = existingChangedAt;
|
||||||
|
plainData.password_history = existingHistory;
|
||||||
}
|
}
|
||||||
// If no existing timestamp and password unchanged, leave it absent
|
|
||||||
// — the security dashboard will fall back to created_at.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4349,6 +4601,7 @@ const Vault = (() => {
|
|||||||
|
|
||||||
// Start web-app inactivity tracking.
|
// Start web-app inactivity tracking.
|
||||||
_startWebIdleTracking();
|
_startWebIdleTracking();
|
||||||
|
_initBulkToolbar();
|
||||||
|
|
||||||
if (!VaultSession.getKey()) {
|
if (!VaultSession.getKey()) {
|
||||||
showUnlockOverlay();
|
showUnlockOverlay();
|
||||||
|
|||||||
@@ -236,11 +236,23 @@
|
|||||||
<option value="folder">By folder</option>
|
<option value="folder">By folder</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<button class="btn-secondary btn-sm" id="btn-select-mode" title="Select items">
|
||||||
|
☑ Select
|
||||||
|
</button>
|
||||||
<button class="btn-fab" id="btn-add-item" title="Add item">
|
<button class="btn-fab" id="btn-add-item" title="Add item">
|
||||||
+
|
+
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Bulk action toolbar — hidden until select mode is active -->
|
||||||
|
<div id="bulk-toolbar" class="bulk-toolbar hidden">
|
||||||
|
<span id="bulk-count" class="bulk-count">0 selected</span>
|
||||||
|
<button class="btn-secondary btn-sm" id="btn-bulk-move">📁 Move</button>
|
||||||
|
<button class="btn-danger btn-sm" id="btn-bulk-delete">🗑️ Delete</button>
|
||||||
|
<button class="btn-secondary btn-sm" id="btn-bulk-export">⬇️ Export</button>
|
||||||
|
<button class="btn-secondary btn-sm" id="btn-bulk-select-all">Select all</button>
|
||||||
|
<button class="btn-secondary btn-sm" id="btn-bulk-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div id="vault-spinner" class="spinner-overlay hidden">
|
<div id="vault-spinner" class="spinner-overlay hidden">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
@@ -521,6 +533,9 @@
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Password history — populated by JS when editing a password item -->
|
||||||
|
<div id="password-history-panel" class="password-history-panel hidden"></div>
|
||||||
|
|
||||||
<!-- ── Advanced Settings (collapsible) ── -->
|
<!-- ── Advanced Settings (collapsible) ── -->
|
||||||
<div class="adv-section">
|
<div class="adv-section">
|
||||||
<button
|
<button
|
||||||
|
|||||||
Reference in New Issue
Block a user