From dc7c29b7ce67f1a88764d73cf96101c1ff8d04af Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Mon, 18 May 2026 02:11:05 +0000 Subject: [PATCH] Don't forget about ungrouped entries when selecting export groups --- AGENTS.md | 7 +- README.md | 2 +- dist/index.html | 353 ++++++++++++++++++++--------- src/components/EntryForm.svelte | 5 + src/components/ImportExport.svelte | 133 ++++++++++- src/lib/storage/db.js | 28 ++- tests/lib/storage/db.test.js | 54 ++++- 7 files changed, 466 insertions(+), 116 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 123d61a..375f47a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -21,7 +21,7 @@ src/ │ └── SettingsDialog.svelte # Auto-lock and tab-switch settings ├── lib/ │ ├── crypto/crypto.js # Web Crypto API: PBKDF2 key derivation, AES-GCM encrypt/decrypt, password generator -│ ├── storage/db.js # IndexedDB layer (idb wrapper): entries, groups, meta stores +│ ├── storage/db.js # IndexedDB layer (idb wrapper): entries, groups, meta stores, exportSelected(groupIds) │ ├── models/schema.js # Data models: CredentialEntry, Group; validation; ID generation │ ├── autofocus.js # Svelte action for autofocus on mount │ └── stores/ @@ -81,6 +81,11 @@ Password verification uses a test payload (random string encrypted at vault crea - Clipboard auto-clears after 15 seconds. - No browser fingerprinting or anti-keylogger protections. +## Export + +- `exportSelected(groupIds)` replaces the old `exportAll()` — accepts an array of group IDs to export. Pass `null` or `[]` for a full export. Vault meta (salt, test payload) is always included for import decryption. +- `ImportExport.svelte` fetches groups/entries on modal open and shows a checkbox list for group selection with live entry count. + ## Known Bug Fixes - `base64ToUint8Array` was used in `db.js` `importAll()` but never imported — now imported from `crypto.js`. diff --git a/README.md b/README.md index 04e4a30..1a4fefe 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ An offline-first password manager that runs entirely in your browser. No server, - **Full-text search** — Instant search across title, username, URL, and notes. - **Password generator** — One-click random password generation in the entry form (🎲 button). Uses Web Crypto API for cryptographically secure randomness. - **Copy to clipboard** — One-click copy with 15-second auto-clear. -- **JSON import/export** — Export your entire vault as encrypted JSON. Import with merge or replace mode. +- **JSON import/export** — Export your vault as encrypted JSON with selective group filtering. Import with merge or replace mode. - **Auto-lock** — Vault locks automatically on tab switch, visibility change, or 5-minute inactivity timer. - **Dark theme** — Responsive layout that works on desktop and mobile. diff --git a/dist/index.html b/dist/index.html index a87c9b2..16e68cd 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4513,6 +4513,15 @@ function set_value(element, value) { } /** * @param {Element} element +* @param {boolean} checked +*/ +function set_checked(element, checked) { + var attributes = get_attributes(element); + if (attributes.checked === (attributes.checked = checked ?? void 0)) return; + element.checked = checked; +} +/** +* @param {Element} element * @param {string} attribute * @param {string | null} value * @param {boolean} [skip_warning] @@ -5578,20 +5587,35 @@ async function moveEntryToGroup(entryId, groupId) { await db.put("entries", entry); } /** -* Export all data (entries + groups + meta) as a JSON object. +* Export data (entries + groups + meta) as a JSON object. * Entries remain encrypted with the source vault's key. The import function * requires the source vault's master password to decrypt and re-encrypt * entries under the target vault's key. * +* @param {string[]} [groupIds] - Array of group IDs to export. If null/empty, exports everything. +* Include '' to export ungrouped entries. * @returns {Promise} */ -async function exportAll() { +async function exportSelected(groupIds) { const db = await getDb(); - const entries = await db.getAll("entries"); - const groups = await db.getAll("groups"); + const allEntries = await db.getAll("entries"); + const allGroups = await db.getAll("groups"); const saltRow = await db.get("meta", "salt"); const testEncryptedRow = await db.get("meta", "testEncrypted"); const testPlaintextRow = await db.get("meta", "testPlaintext"); + if (!groupIds || groupIds.length === 0) return { + version: DB_VERSION, + exportedAt: (/* @__PURE__ */ new Date()).toISOString(), + meta: { + salt: saltRow?.value || null, + testEncrypted: testEncryptedRow?.value || null, + testPlaintext: testPlaintextRow?.value || null + }, + groups: allGroups, + entries: allEntries + }; + const entries = allEntries.filter((e) => groupIds.includes(e.groupId)); + const groups = allGroups.filter((g) => groupIds.includes(g.id)); return { version: DB_VERSION, exportedAt: (/* @__PURE__ */ new Date()).toISOString(), @@ -5973,9 +5997,9 @@ var search = new SearchStore(); //#region src/components/Sidebar.svelte var root_2$5 = /* @__PURE__ */ from_html(`
`); var root_4$5 = /* @__PURE__ */ from_html(`
`); -var root_5$4 = /* @__PURE__ */ from_html(``); +var root_5$5 = /* @__PURE__ */ from_html(``); var root_3$5 = /* @__PURE__ */ from_html(``); -var root_6$4 = /* @__PURE__ */ from_html(``); +var root_6$3 = /* @__PURE__ */ from_html(``); var root$6 = /* @__PURE__ */ from_html(``); function Sidebar($$anchor, $$props) { push($$props, true); @@ -6151,7 +6175,7 @@ function Sidebar($$anchor, $$props) { var div_10 = sibling(div_9, 2); var div_11 = sibling(child(div_10), 2); each(div_11, 21, () => GROUP_COLORS, index, ($$anchor, color) => { - var button_6 = root_5$4(); + var button_6 = root_5$5(); template_effect(() => { set_class(button_6, 1, `color-swatch ${get(groupColor) === get(color) ? "selected" : ""}`, "svelte-181dlmc"); set_style(button_6, `background-color: ${get(color) ?? ""}`); @@ -6185,7 +6209,7 @@ function Sidebar($$anchor, $$props) { }); var node_4 = sibling(node_2, 2); var consequent_3 = ($$anchor) => { - var div_13 = root_6$4(); + var div_13 = root_6$3(); var div_14 = child(div_13); var p = sibling(child(div_14), 2); var strong = sibling(child(p)); @@ -6231,12 +6255,12 @@ var root_1$6 = /* @__PURE__ */ from_html(`
Lo var root_2$4 = /* @__PURE__ */ from_html(`
`); var root_4$4 = /* @__PURE__ */ from_html(``); var root_3$4 = /* @__PURE__ */ from_html(`

`); -var root_6$3 = /* @__PURE__ */ from_html(`matching " "`, 1); +var root_6$2 = /* @__PURE__ */ from_html(`matching " "`, 1); var root_7$4 = /* @__PURE__ */ from_html(``); var root_9$1 = /* @__PURE__ */ from_html(``); var root_10$1 = /* @__PURE__ */ from_html(``); -var root_8$2 = /* @__PURE__ */ from_html(` `); -var root_5$3 = /* @__PURE__ */ from_html(`
TitleUsernameURLNotes
`, 1); +var root_8$3 = /* @__PURE__ */ from_html(` `); +var root_5$4 = /* @__PURE__ */ from_html(`
TitleUsernameURLNotes
`, 1); var root$5 = /* @__PURE__ */ from_html(`
`); function EntryList($$anchor, $$props) { push($$props, true); @@ -6319,13 +6343,13 @@ function EntryList($$anchor, $$props) { append($$anchor, div_3); }; var alternate = ($$anchor) => { - var fragment = root_5$3(); + var fragment = root_5$4(); var div_4 = first_child(fragment); var span = child(div_4); var text_4 = child(span); var node_2 = sibling(text_4); var consequent_4 = ($$anchor) => { - var fragment_1 = root_6$3(); + var fragment_1 = root_6$2(); var strong = sibling(first_child(fragment_1)); var text_5 = child(strong, true); reset(strong); @@ -6352,7 +6376,7 @@ function EntryList($$anchor, $$props) { reset(thead); var tbody = sibling(thead); each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => { - var tr_1 = root_8$2(); + var tr_1 = root_8$3(); var td = child(tr_1); var node_4 = child(td); var consequent_6 = ($$anchor) => { @@ -6438,14 +6462,14 @@ var root_1$5 = /* @__PURE__ */ from_html(`
Loading...
`); var root_3$3 = /* @__PURE__ */ from_html(`
`); var root_4$3 = /* @__PURE__ */ from_html(`
Entry not found
`); -var root_6$2 = /* @__PURE__ */ from_html(` `, 1); +var root_6$1 = /* @__PURE__ */ from_html(` `, 1); var root_7$3 = /* @__PURE__ */ from_html(` `, 1); -var root_8$1 = /* @__PURE__ */ from_html(`
Username
`); +var root_8$2 = /* @__PURE__ */ from_html(`
Username
`); var root_9 = /* @__PURE__ */ from_html(`
URL
`); var root_10 = /* @__PURE__ */ from_html(`
Notes
`); var root_11 = /* @__PURE__ */ from_html(``); var root_12 = /* @__PURE__ */ from_html(``); -var root_5$2 = /* @__PURE__ */ from_html(`

Password
`, 1); +var root_5$3 = /* @__PURE__ */ from_html(`

Password
`, 1); var root$4 = /* @__PURE__ */ from_html(`
`); function EntryDetail($$anchor, $$props) { push($$props, true); @@ -6549,7 +6573,7 @@ function EntryDetail($$anchor, $$props) { append($$anchor, root_4$3()); }; var alternate_1 = ($$anchor) => { - var fragment = root_5$2(); + var fragment = root_5$3(); var div_5 = first_child(fragment); var div_6 = child(div_5); var h2 = child(div_6); @@ -6558,7 +6582,7 @@ function EntryDetail($$anchor, $$props) { var div_7 = sibling(h2, 2); var node_2 = child(div_7); var consequent_4 = ($$anchor) => { - var fragment_1 = root_6$2(); + var fragment_1 = root_6$1(); var button = first_child(fragment_1); var button_1 = sibling(button, 2); delegated("click", button, () => $$props.onEdit(get(entry).id)); @@ -6582,7 +6606,7 @@ function EntryDetail($$anchor, $$props) { var div_8 = sibling(div_6, 2); var node_3 = child(div_8); var consequent_5 = ($$anchor) => { - var div_9 = root_8$1(); + var div_9 = root_8$2(); var div_10 = sibling(child(div_9), 2); var span = child(div_10); var text_4 = child(span, true); @@ -6741,7 +6765,7 @@ delegate(["click"]); //#region src/components/EntryForm.svelte var root_1$4 = /* @__PURE__ */ from_html(`
Loading...
`); var root_3$2 = /* @__PURE__ */ from_html(`
`); -var root_5$1 = /* @__PURE__ */ from_html(`
`); +var root_5$2 = /* @__PURE__ */ from_html(`
`); var root_4$2 = /* @__PURE__ */ from_html(`
`); var root_7$2 = /* @__PURE__ */ from_html(``); var root_2$2 = /* @__PURE__ */ from_html(`
`, 1); @@ -6776,6 +6800,9 @@ function EntryForm($$anchor, $$props) { set(notes, entry.notes || "", true); set(groupId, entry.groupId || "", true); } else set(error, "Entry not found"); + } else { + const active = search.activeGroupId; + set(groupId, active !== "all" && active !== "trash" ? active : "", true); } } catch (e) { set(error, "Failed to load form: " + e.message); @@ -6844,7 +6871,7 @@ function EntryForm($$anchor, $$props) { var consequent_2 = ($$anchor) => { var div_3 = root_4$2(); each(div_3, 21, () => get(formErrors), index, ($$anchor, err) => { - var div_4 = root_5$1(); + var div_4 = root_5$2(); var text_1 = child(div_4); reset(div_4); template_effect(() => set_text(text_1, `⚠ ${get(err) ?? ""}`)); @@ -6950,17 +6977,28 @@ function EntryForm($$anchor, $$props) { delegate(["click"]); //#endregion //#region src/components/ImportExport.svelte -var root_1$3 = /* @__PURE__ */ from_html(``); -var root_3$1 = /* @__PURE__ */ from_html(`
`); -var root_4$1 = /* @__PURE__ */ from_html(`
`); -var root_6$1 = /* @__PURE__ */ from_html(`

File loaded. Enter the source vault's master password to decrypt and re-encrypt entries under your current vault.

`, 1); -var root_7$1 = /* @__PURE__ */ from_html(`

Select how to handle existing data:

`, 1); -var root_2$1 = /* @__PURE__ */ from_html(``); +var root_2$1 = /* @__PURE__ */ from_html(``); +var root_1$3 = /* @__PURE__ */ from_html(``); +var root_4$1 = /* @__PURE__ */ from_html(`
`); +var root_5$1 = /* @__PURE__ */ from_html(`
`); +var root_7$1 = /* @__PURE__ */ from_html(`

File loaded. Enter the source vault's master password to decrypt and re-encrypt entries under your current vault.

`, 1); +var root_8$1 = /* @__PURE__ */ from_html(`

Select how to handle existing data:

`, 1); +var root_3$1 = /* @__PURE__ */ from_html(``); var root$2 = /* @__PURE__ */ from_html(`
`); function ImportExport($$anchor, $$props) { push($$props, true); const binding_group = []; let showExport = /* @__PURE__ */ state(false); + async function openExportModal() { + set(allGroups, [{ + id: "", + name: "Ungrouped", + color: "#6b7280" + }, ...(await getGroups()).filter((g) => !isTrashGroup(g.id))], true); + set(allEntries, await getEntries(), true); + set(selectedGroupIds, get(allGroups).map((g) => g.id), true); + set(showExport, true); + } let showImport = /* @__PURE__ */ state(false); let importMode = /* @__PURE__ */ state("merge"); let importResult = /* @__PURE__ */ state(null); @@ -6970,10 +7008,15 @@ function ImportExport($$anchor, $$props) { let exporting = /* @__PURE__ */ state(false); let sourcePassword = /* @__PURE__ */ state(""); let parsedFileData = /* @__PURE__ */ state(null); + let allGroups = /* @__PURE__ */ state(proxy([])); + let allEntries = /* @__PURE__ */ state(proxy([])); + let selectedGroupIds = /* @__PURE__ */ state(proxy([])); + let selectAll = /* @__PURE__ */ user_derived(() => get(allGroups).length > 0 && get(allGroups).every((g) => get(selectedGroupIds).includes(g.id))); + let exportEntryCount = /* @__PURE__ */ user_derived(() => get(allEntries).filter((e) => get(selectedGroupIds).includes(e.groupId)).length); async function handleExport() { set(exporting, true); try { - set(exportData, await exportAll(), true); + set(exportData, await exportSelected(get(selectedGroupIds).length === get(allGroups).length ? null : get(selectedGroupIds)), true); const json = JSON.stringify(get(exportData), null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); @@ -6988,6 +7031,14 @@ function ImportExport($$anchor, $$props) { } set(exporting, false); } + function toggleSelectAll() { + if (get(selectAll)) set(selectedGroupIds, [], true); + else set(selectedGroupIds, get(allGroups).map((g) => g.id), true); + } + function toggleGroup(groupId) { + if (get(selectedGroupIds).includes(groupId)) set(selectedGroupIds, get(selectedGroupIds).filter((id) => id !== groupId), true); + else set(selectedGroupIds, [...get(selectedGroupIds), groupId], true); + } async function handleFileSelect(event) { const file = event.target.files[0]; if (!file) return; @@ -7034,19 +7085,51 @@ function ImportExport($$anchor, $$props) { var div_1 = root_1$3(); var div_2 = child(div_1); var div_3 = sibling(child(div_2), 4); - var button_2 = child(div_3); - var text_1 = child(button_2, true); + var label = child(div_3); + var input = child(label); + remove_input_defaults(input); + next(2); + reset(label); + var span = sibling(label, 2); + var text_1 = child(span); + reset(span); + reset(div_3); + var div_4 = sibling(div_3, 2); + each(div_4, 21, () => get(allGroups), index, ($$anchor, group) => { + var label_1 = root_2$1(); + var input_1 = child(label_1); + remove_input_defaults(input_1); + var span_1 = sibling(input_1, 2); + var span_2 = sibling(span_1, 2); + var text_2 = child(span_2, true); + reset(span_2); + reset(label_1); + template_effect(($0) => { + set_checked(input_1, $0); + set_style(span_1, `background-color: ${(get(group).color || "#6c63ff") ?? ""}`); + set_text(text_2, get(group).name); + }, [() => get(selectedGroupIds).includes(get(group).id)]); + delegated("change", input_1, () => toggleGroup(get(group).id)); + append($$anchor, label_1); + }); + reset(div_4); + var div_5 = sibling(div_4, 2); + var button_2 = child(div_5); + var text_3 = child(button_2, true); reset(button_2); var button_3 = sibling(button_2, 2); - reset(div_3); + reset(div_5); reset(div_2); reset(div_1); template_effect(() => { - button_2.disabled = get(exporting); - set_text(text_1, get(exporting) ? "Exporting..." : "📤 Export JSON"); + set_checked(input, get(selectAll)); + set_text(text_1, `${get(exportEntryCount) ?? ""} entries`); + button_2.disabled = get(exporting) || get(selectedGroupIds).length === 0; + set_text(text_3, get(exporting) ? "Exporting..." : "📤 Export JSON"); }); delegated("click", div_1, () => set(showExport, false)); delegated("click", div_2, (e) => e.stopPropagation()); + delegated("change", input, toggleSelectAll); delegated("click", button_2, handleExport); delegated("click", button_3, () => set(showExport, false)); append($$anchor, div_1); @@ -7056,79 +7139,43 @@ function ImportExport($$anchor, $$props) { }); var node_1 = sibling(node, 2); var consequent_5 = ($$anchor) => { - var div_4 = root_2$1(); - var div_5 = child(div_4); - var node_2 = sibling(child(div_5), 2); + var div_6 = root_3$1(); + var div_7 = child(div_6); + var node_2 = sibling(child(div_7), 2); var consequent_1 = ($$anchor) => { - var div_6 = root_3$1(); - var text_2 = child(div_6, true); - reset(div_6); - template_effect(() => set_text(text_2, get(importError))); - append($$anchor, div_6); + var div_8 = root_4$1(); + var text_4 = child(div_8, true); + reset(div_8); + template_effect(() => set_text(text_4, get(importError))); + append($$anchor, div_8); }; if_block(node_2, ($$render) => { if (get(importError)) $$render(consequent_1); }); var node_3 = sibling(node_2, 2); var consequent_3 = ($$anchor) => { - var div_7 = root_4$1(); - var text_3 = child(div_7); - var node_4 = sibling(text_3); + var div_9 = root_5$1(); + var text_5 = child(div_9); + var node_4 = sibling(text_5); var consequent_2 = ($$anchor) => { - var text_4 = text(); - template_effect(() => set_text(text_4, `(${get(importResult).skipped ?? ""} skipped)`)); - append($$anchor, text_4); + var text_6 = text(); + template_effect(() => set_text(text_6, `(${get(importResult).skipped ?? ""} skipped)`)); + append($$anchor, text_6); }; if_block(node_4, ($$render) => { if (get(importResult).skipped > 0) $$render(consequent_2); }); - reset(div_7); - template_effect(() => set_text(text_3, `✓ Imported ${get(importResult).imported.entries ?? ""} entries and ${get(importResult).imported.groups ?? ""} groups `)); - append($$anchor, div_7); + reset(div_9); + template_effect(() => set_text(text_5, `✓ Imported ${get(importResult).imported.entries ?? ""} entries and ${get(importResult).imported.groups ?? ""} groups `)); + append($$anchor, div_9); }; var consequent_4 = ($$anchor) => { - var fragment_1 = root_6$1(); - var div_8 = sibling(first_child(fragment_1), 2); - var input = sibling(child(div_8), 2); - remove_input_defaults(input); - reset(div_8); - var div_9 = sibling(div_8, 2); - var label = child(div_9); - var input_1 = child(label); - remove_input_defaults(input_1); - input_1.value = input_1.__value = "merge"; - next(2); - reset(label); - var label_1 = sibling(label, 2); - var input_2 = child(label_1); + var fragment_1 = root_7$1(); + var div_10 = sibling(first_child(fragment_1), 2); + var input_2 = sibling(child(div_10), 2); remove_input_defaults(input_2); - input_2.value = input_2.__value = "replace"; - next(2); - reset(label_1); - reset(div_9); - var div_10 = sibling(div_9, 2); - var button_4 = child(div_10); - var text_5 = child(button_4, true); - reset(button_4); - var button_5 = sibling(button_4, 2); reset(div_10); - template_effect(() => { - button_4.disabled = get(importing); - set_text(text_5, get(importing) ? "Importing..." : "📥 Import"); - }); - bind_value(input, () => get(sourcePassword), ($$value) => set(sourcePassword, $$value)); - bind_group(binding_group, [], input_1, () => get(importMode), ($$value) => set(importMode, $$value)); - bind_group(binding_group, [], input_2, () => get(importMode), ($$value) => set(importMode, $$value)); - delegated("click", button_4, handleImportSubmit); - delegated("click", button_5, () => { - set(parsedFileData, null); - set(sourcePassword, ""); - }); - append($$anchor, fragment_1); - }; - var alternate = ($$anchor) => { - var fragment_2 = root_7$1(); - var div_11 = sibling(first_child(fragment_2), 2); + var div_11 = sibling(div_10, 2); var label_2 = child(div_11); var input_3 = child(label_2); remove_input_defaults(input_3); @@ -7143,12 +7190,48 @@ function ImportExport($$anchor, $$props) { reset(label_3); reset(div_11); var div_12 = sibling(div_11, 2); - var input_5 = sibling(child(div_12), 2); + var button_4 = child(div_12); + var text_7 = child(button_4, true); + reset(button_4); + var button_5 = sibling(button_4, 2); reset(div_12); - template_effect(() => input_5.disabled = get(importing)); + template_effect(() => { + button_4.disabled = get(importing); + set_text(text_7, get(importing) ? "Importing..." : "📥 Import"); + }); + bind_value(input_2, () => get(sourcePassword), ($$value) => set(sourcePassword, $$value)); bind_group(binding_group, [], input_3, () => get(importMode), ($$value) => set(importMode, $$value)); bind_group(binding_group, [], input_4, () => get(importMode), ($$value) => set(importMode, $$value)); - delegated("change", input_5, handleFileSelect); + delegated("click", button_4, handleImportSubmit); + delegated("click", button_5, () => { + set(parsedFileData, null); + set(sourcePassword, ""); + }); + append($$anchor, fragment_1); + }; + var alternate = ($$anchor) => { + var fragment_2 = root_8$1(); + var div_13 = sibling(first_child(fragment_2), 2); + var label_4 = child(div_13); + var input_5 = child(label_4); + remove_input_defaults(input_5); + input_5.value = input_5.__value = "merge"; + next(2); + reset(label_4); + var label_5 = sibling(label_4, 2); + var input_6 = child(label_5); + remove_input_defaults(input_6); + input_6.value = input_6.__value = "replace"; + next(2); + reset(label_5); + reset(div_13); + var div_14 = sibling(div_13, 2); + var input_7 = sibling(child(div_14), 2); + reset(div_14); + template_effect(() => input_7.disabled = get(importing)); + bind_group(binding_group, [], input_5, () => get(importMode), ($$value) => set(importMode, $$value)); + bind_group(binding_group, [], input_6, () => get(importMode), ($$value) => set(importMode, $$value)); + delegated("change", input_7, handleFileSelect); append($$anchor, fragment_2); }; if_block(node_3, ($$render) => { @@ -7156,25 +7239,25 @@ function ImportExport($$anchor, $$props) { else if (get(parsedFileData)) $$render(consequent_4, 1); else $$render(alternate, -1); }); - var div_13 = sibling(node_3, 2); - var button_6 = child(div_13); - reset(div_13); - reset(div_5); - reset(div_4); - delegated("click", div_4, () => set(showImport, false)); - delegated("click", div_5, (e) => e.stopPropagation()); + var div_15 = sibling(node_3, 2); + var button_6 = child(div_15); + reset(div_15); + reset(div_7); + reset(div_6); + delegated("click", div_6, () => set(showImport, false)); + delegated("click", div_7, (e) => e.stopPropagation()); delegated("click", button_6, () => { set(showImport, false); set(importResult, null); set(importError, ""); }); - append($$anchor, div_4); + append($$anchor, div_6); }; if_block(node_1, ($$render) => { if (get(showImport)) $$render(consequent_5); }); reset(div); - delegated("click", button, () => set(showExport, true)); + delegated("click", button, openExportModal); delegated("click", button_1, () => set(showImport, true)); append($$anchor, div); pop(); @@ -8434,6 +8517,70 @@ label { border-color: var(--color-primary); } + /* Group selection for export */ + .group-select-header.svelte-17di1i9 { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-bottom: 1px solid var(--color-border); + margin-bottom: 4px; + } + + .group-select-header.svelte-17di1i9 .entry-count:where(.svelte-17di1i9) { + font-size: 0.8rem; + color: var(--color-text-muted); + } + + .group-select-list.svelte-17di1i9 { + max-height: 240px; + overflow-y: auto; + margin-bottom: 8px; + } + + .group-select-list.svelte-17di1i9::-webkit-scrollbar { + width: 6px; + } + + .group-select-list.svelte-17di1i9::-webkit-scrollbar-track { + background: transparent; + } + + .group-select-list.svelte-17di1i9::-webkit-scrollbar-thumb { + background: var(--color-border); + border-radius: 3px; + } + + .checkbox-label.svelte-17di1i9 { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + font-size: 0.85rem; + } + + .checkbox-label.svelte-17di1i9 input[type="checkbox"]:where(.svelte-17di1i9) { + accent-color: var(--color-primary); + cursor: pointer; + } + + .group-checkbox.svelte-17di1i9 { + padding: 6px 12px; + border-radius: var(--radius-md); + transition: background-color 150ms; + } + + .group-checkbox.svelte-17di1i9:hover { + background: var(--color-surface-hover); + } + + .group-color-dot.svelte-17di1i9 { + width: 10px; + height: 10px; + border-radius: 50%; + flex-shrink: 0; + } + .settings-panel.svelte-1koizbb { max-width: 500px; } diff --git a/src/components/EntryForm.svelte b/src/components/EntryForm.svelte index 917e8c0..a226270 100644 --- a/src/components/EntryForm.svelte +++ b/src/components/EntryForm.svelte @@ -4,6 +4,7 @@ import { createEntry, updateEntry as updateEntryModel, validateEntry, isTrashGroup } from '../lib/models/schema.js' import { generatePassword } from '../lib/crypto/crypto.js' import { app } from '../lib/stores/app.svelte.js' + import { search as searchStore } from '../lib/stores/search.svelte.js' import { autofocus } from '../lib/autofocus.js' let { entryId, onSave, onCancel } = $props() @@ -39,6 +40,10 @@ } else { error = 'Entry not found' } + } else { + // Default to the currently active group if it's a real group + const active = searchStore.activeGroupId + groupId = (active !== 'all' && active !== 'trash') ? active : '' } } catch (e) { error = 'Failed to load form: ' + e.message diff --git a/src/components/ImportExport.svelte b/src/components/ImportExport.svelte index 09f1222..6f9a206 100644 --- a/src/components/ImportExport.svelte +++ b/src/components/ImportExport.svelte @@ -1,9 +1,18 @@