diff --git a/dist/index.html b/dist/index.html index e45046d..f53912e 100644 --- a/dist/index.html +++ b/dist/index.html @@ -4843,6 +4843,13 @@ var app$1 = new AppStore(); * to keep things sortable and collision-resistant without external deps. */ /** +* Well-known ID for the permanent Trash group. +* This is a fixed constant so it survives re-creates. +*/ +var TRASH_GROUP_ID = "__trash__"; +var TRASH_GROUP_NAME = "Trash"; +var TRASH_GROUP_COLOR = "#e5484d"; +/** * Generate a unique ID (timestamp-based, sortable). * @returns {string} */ @@ -4977,6 +4984,26 @@ function validateGroup(name) { errors }; } +/** +* Create the permanent Trash group. +* @returns {Group} +*/ +function createTrashGroup() { + return { + id: TRASH_GROUP_ID, + name: TRASH_GROUP_NAME, + color: TRASH_GROUP_COLOR, + createdAt: (/* @__PURE__ */ new Date()).toISOString() + }; +} +/** +* Check if a group ID refers to the Trash group. +* @param {string} groupId +* @returns {boolean} +*/ +function isTrashGroup(groupId) { + return groupId === TRASH_GROUP_ID; +} //#endregion //#region src/lib/crypto/crypto.js /** @@ -5465,9 +5492,18 @@ async function updateGroup(group) { * @returns {Promise} */ async function deleteGroup(groupId) { + if (isTrashGroup(groupId)) throw new Error("Cannot delete the Trash group"); await (await getDb()).delete("groups", groupId); } /** +* Ensure the Trash group exists in the database. +* @returns {Promise} +*/ +async function ensureTrashGroup() { + const db = await getDb(); + if (!await db.get("groups", "__trash__")) await db.put("groups", createTrashGroup()); +} +/** * Get all groups, sorted by creation date. * @returns {Promise} */ @@ -5475,6 +5511,46 @@ async function getGroups() { return (await (await getDb()).transaction("groups", "readonly").store.getAll()).sort((a, b) => a.createdAt.localeCompare(b.createdAt)); } /** +* Move an entry to the Trash group. +* @param {string} entryId +* @returns {Promise} +*/ +async function moveToTrash(entryId) { + await ensureTrashGroup(); + const db = await getDb(); + const entry = await db.get("entries", entryId); + if (!entry) throw new Error("Entry not found"); + entry.groupId = TRASH_GROUP_ID; + entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); + await db.put("entries", entry); +} +/** +* Permanently delete all entries in the Trash group. +* @returns {Promise} Number of entries deleted +*/ +async function emptyTrash() { + const db = await getDb(); + const trashed = await db.transaction("entries").store.index("groupId").getAll(TRASH_GROUP_ID); + const tx = db.transaction("entries", "readwrite"); + for (const entry of trashed) await tx.store.delete(entry.id); + await tx.done; + return trashed.length; +} +/** +* Restore a trashed entry to its original group (or ungrouped if unknown). +* @param {string} entryId +* @param {string} [restoreGroupId] - Group to restore to (default: empty/ungrouped) +* @returns {Promise} +*/ +async function restoreEntry(entryId, restoreGroupId = "") { + const db = await getDb(); + const entry = await db.get("entries", entryId); + if (!entry) throw new Error("Entry not found"); + entry.groupId = restoreGroupId; + entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); + await db.put("entries", entry); +} +/** * @typedef {import('../models/schema.js').CredentialEntry} CredentialEntry */ /** @@ -5494,14 +5570,6 @@ async function updateEntry(entry) { await (await getDb()).put("entries", entry); } /** -* Delete an entry. -* @param {string} entryId -* @returns {Promise} -*/ -async function deleteEntry(entryId) { - await (await getDb()).delete("entries", entryId); -} -/** * Get a single entry by ID. * @param {string} entryId * @returns {Promise} @@ -5552,19 +5620,6 @@ async function moveEntryToGroup(entryId, groupId) { await db.put("entries", entry); } /** -* Count entries per group. -* @returns {Promise>} -*/ -async function getEntryCountsByGroup() { - const all = await (await getDb()).getAll("entries"); - const counts = /* @__PURE__ */ new Map(); - for (const entry of all) { - const gid = entry.groupId || ""; - counts.set(gid, (counts.get(gid) || 0) + 1); - } - return counts; -} -/** * Export all 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 @@ -5655,7 +5710,7 @@ function autofocus(node, condition = true) { } //#endregion //#region src/components/LockScreen.svelte -var root_1$8 = /* @__PURE__ */ from_html(``); +var root_1$7 = /* @__PURE__ */ from_html(``); var root_2$6 = /* @__PURE__ */ from_html(`
`); var root$7 = /* @__PURE__ */ from_html(`
🔐

Password Vault

`); function LockScreen($$anchor, $$props) { @@ -5688,6 +5743,7 @@ function LockScreen($$anchor, $$props) { app$1.salt = salt; app$1.encryptionKey = await deriveKey(get(masterPassword), salt); await saveVaultMeta(salt, testEncrypted, testPlaintext); + await ensureTrashGroup(); app$1.isUnlocked = true; startAutoLock(); } else { @@ -5723,7 +5779,7 @@ function LockScreen($$anchor, $$props) { reset(p); var node = sibling(p, 2); var consequent = ($$anchor) => { - var div_2 = root_1$8(); + var div_2 = root_1$7(); var text_1 = child(div_2, true); reset(div_2); template_effect(() => set_text(text_1, get(error))); @@ -5832,16 +5888,15 @@ var SearchStore = class { var search = new SearchStore(); //#endregion //#region src/components/Sidebar.svelte -var root_1$7 = /* @__PURE__ */ from_html(`
`); -var root_3$5 = /* @__PURE__ */ from_html(`
`); -var root_4$5 = /* @__PURE__ */ from_html(``); -var root_2$5 = /* @__PURE__ */ from_html(``); -var root_5$4 = /* @__PURE__ */ from_html(``); -var root$6 = /* @__PURE__ */ from_html(``); +var root_2$5 = /* @__PURE__ */ from_html(`
`); +var root_4$5 = /* @__PURE__ */ from_html(`
`); +var root_5$4 = /* @__PURE__ */ from_html(``); +var root_3$5 = /* @__PURE__ */ from_html(``); +var root_6$4 = /* @__PURE__ */ from_html(``); +var root$6 = /* @__PURE__ */ from_html(``); function Sidebar($$anchor, $$props) { push($$props, true); let groups = /* @__PURE__ */ state(proxy([])); - let entryCounts = /* @__PURE__ */ state(proxy(/* @__PURE__ */ new Map())); let showGroupForm = /* @__PURE__ */ state(false); let editingGroupId = /* @__PURE__ */ state(null); let groupName = /* @__PURE__ */ state(""); @@ -5863,7 +5918,7 @@ function Sidebar($$anchor, $$props) { } catch (e) {} } function canDrop(groupId) { - return groupId !== search.activeGroupId; + return groupId !== search.activeGroupId && !isTrashGroup(groupId); } const GROUP_COLORS = [ "#6c63ff", @@ -5883,9 +5938,8 @@ function Sidebar($$anchor, $$props) { "#6366f1" ]; async function loadData() { - const [g, counts] = await Promise.all([getGroups(), getEntryCountsByGroup()]); - set(groups, g, true); - set(entryCounts, counts, true); + await ensureTrashGroup(); + set(groups, await getGroups(), true); } user_effect(() => { search.refreshTrigger; @@ -5942,144 +5996,165 @@ function Sidebar($$anchor, $$props) { var nav = sibling(div_1, 2); var button = child(nav); each(sibling(button, 2), 17, () => get(groups), index, ($$anchor, group) => { - var div_2 = root_1$7(); - var button_1 = child(div_2); - var span = child(button_1); - var span_1 = sibling(span, 2); - var text = child(span_1, true); - reset(span_1); - next(2); - reset(button_1); - var div_3 = sibling(button_1, 2); - var button_2 = child(div_3); - var button_3 = sibling(button_2, 2); - reset(div_3); - reset(div_2); - template_effect(() => { - set_class(button_1, 1, `group-item ${search.activeGroupId === get(group).id ? "active" : ""} ${get(dragOverGroupId) === get(group).id ? "drag-over" : ""} ${get(droppedGroupId) === get(group).id ? "dropped" : ""}`, "svelte-181dlmc"); - set_style(span, `background-color: ${(get(group).color || "#6c63ff") ?? ""}`); - set_text(text, get(group).name); - }); - delegated("click", button_1, () => search.activeGroupId = get(group).id); - event("dragover", button_1, (e) => { - if (canDrop(get(group).id)) { + var fragment = comment(); + var node_1 = first_child(fragment); + var consequent = ($$anchor) => { + var div_2 = root_2$5(); + var button_1 = child(div_2); + var span = child(button_1); + var span_1 = sibling(span, 2); + var text = child(span_1, true); + reset(span_1); + next(2); + reset(button_1); + var div_3 = sibling(button_1, 2); + var button_2 = child(div_3); + var button_3 = sibling(button_2, 2); + reset(div_3); + reset(div_2); + template_effect(() => { + set_class(button_1, 1, `group-item ${search.activeGroupId === get(group).id ? "active" : ""} ${get(dragOverGroupId) === get(group).id ? "drag-over" : ""} ${get(droppedGroupId) === get(group).id ? "dropped" : ""}`, "svelte-181dlmc"); + set_style(span, `background-color: ${(get(group).color || "#6c63ff") ?? ""}`); + set_text(text, get(group).name); + }); + delegated("click", button_1, () => search.activeGroupId = get(group).id); + event("dragover", button_1, (e) => { + if (canDrop(get(group).id)) { + e.preventDefault(); + e.dataTransfer.dropEffect = "move"; + set(dragOverGroupId, get(group).id, true); + } + }); + event("dragleave", button_1, () => { + if (get(dragOverGroupId) === get(group).id) set(dragOverGroupId, null); + }); + event("drop", button_1, (e) => { e.preventDefault(); - e.dataTransfer.dropEffect = "move"; - set(dragOverGroupId, get(group).id, true); - } + set(dragOverGroupId, null); + if (canDrop(get(group).id)) { + const entryId = e.dataTransfer.getData("text/plain"); + if (entryId) handleDrop(get(group).id, entryId); + } + }); + delegated("click", button_2, () => openGroupForm(get(group))); + delegated("click", button_3, () => set(showDeleteGroupConfirm, get(group).id, true)); + append($$anchor, div_2); + }; + var d = /* @__PURE__ */ user_derived(() => !isTrashGroup(get(group).id)); + if_block(node_1, ($$render) => { + if (get(d)) $$render(consequent); }); - event("dragleave", button_1, () => { - if (get(dragOverGroupId) === get(group).id) set(dragOverGroupId, null); - }); - event("drop", button_1, (e) => { - e.preventDefault(); - set(dragOverGroupId, null); - if (canDrop(get(group).id)) { - const entryId = e.dataTransfer.getData("text/plain"); - if (entryId) handleDrop(get(group).id, entryId); - } - }); - delegated("click", button_2, () => openGroupForm(get(group))); - delegated("click", button_3, () => set(showDeleteGroupConfirm, get(group).id, true)); - append($$anchor, div_2); + append($$anchor, fragment); }); reset(nav); var div_4 = sibling(nav, 2); var button_4 = child(div_4); + var span_2 = child(button_4); + var span_3 = sibling(span_2, 2); + var text_1 = child(span_3, true); + reset(span_3); + reset(button_4); reset(div_4); - var node_1 = sibling(div_4, 2); - var consequent_1 = ($$anchor) => { - var div_5 = root_2$5(); - var div_6 = child(div_5); - var h3 = child(div_6); - var text_1 = child(h3, true); + var div_5 = sibling(div_4, 2); + var button_5 = child(div_5); + reset(div_5); + var node_2 = sibling(div_5, 2); + var consequent_2 = ($$anchor) => { + var div_6 = root_3$5(); + var div_7 = child(div_6); + var h3 = child(div_7); + var text_2 = child(h3, true); reset(h3); - var node_2 = sibling(h3, 2); - var consequent = ($$anchor) => { - var div_7 = root_3$5(); - var text_2 = child(div_7, true); - reset(div_7); - template_effect(() => set_text(text_2, get(groupError))); - append($$anchor, div_7); + var node_3 = sibling(h3, 2); + var consequent_1 = ($$anchor) => { + var div_8 = root_4$5(); + var text_3 = child(div_8, true); + reset(div_8); + template_effect(() => set_text(text_3, get(groupError))); + append($$anchor, div_8); }; - if_block(node_2, ($$render) => { - if (get(groupError)) $$render(consequent); + if_block(node_3, ($$render) => { + if (get(groupError)) $$render(consequent_1); }); - var div_8 = sibling(node_2, 2); - var input_1 = sibling(child(div_8), 2); + var div_9 = sibling(node_3, 2); + var input_1 = sibling(child(div_9), 2); remove_input_defaults(input_1); effect(() => bind_value(input_1, () => get(groupName), ($$value) => set(groupName, $$value))); action(input_1, ($$node, $$action_arg) => autofocus?.($$node, $$action_arg), () => !get(editingGroupId)); - reset(div_8); - var div_9 = sibling(div_8, 2); - var div_10 = sibling(child(div_9), 2); - each(div_10, 21, () => GROUP_COLORS, index, ($$anchor, color) => { - var button_5 = root_4$5(); - template_effect(() => { - set_class(button_5, 1, `color-swatch ${get(groupColor) === get(color) ? "selected" : ""}`, "svelte-181dlmc"); - set_style(button_5, `background-color: ${get(color) ?? ""}`); - set_attribute(button_5, "title", get(color)); - }); - delegated("click", button_5, () => set(groupColor, get(color), true)); - append($$anchor, button_5); - }); - reset(div_10); reset(div_9); - var div_11 = sibling(div_9, 2); - var button_6 = child(div_11); - var text_3 = child(button_6, true); - reset(button_6); - var button_7 = sibling(button_6, 2); - reset(div_11); - reset(div_6); - reset(div_5); - template_effect(() => { - set_text(text_1, get(editingGroupId) ? "Edit Group" : "New Group"); - set_text(text_3, get(editingGroupId) ? "Update" : "Create"); + 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(); + template_effect(() => { + set_class(button_6, 1, `color-swatch ${get(groupColor) === get(color) ? "selected" : ""}`, "svelte-181dlmc"); + set_style(button_6, `background-color: ${get(color) ?? ""}`); + set_attribute(button_6, "title", get(color)); + }); + delegated("click", button_6, () => set(groupColor, get(color), true)); + append($$anchor, button_6); }); - delegated("click", div_5, () => set(showGroupForm, false)); - delegated("click", div_6, (e) => e.stopPropagation()); - delegated("click", button_6, saveGroup); - delegated("click", button_7, () => set(showGroupForm, false)); - append($$anchor, div_5); + reset(div_11); + reset(div_10); + var div_12 = sibling(div_10, 2); + var button_7 = child(div_12); + var text_4 = child(button_7, true); + reset(button_7); + var button_8 = sibling(button_7, 2); + reset(div_12); + reset(div_7); + reset(div_6); + template_effect(() => { + set_text(text_2, get(editingGroupId) ? "Edit Group" : "New Group"); + set_text(text_4, get(editingGroupId) ? "Update" : "Create"); + }); + delegated("click", div_6, () => set(showGroupForm, false)); + delegated("click", div_7, (e) => e.stopPropagation()); + delegated("click", button_7, saveGroup); + delegated("click", button_8, () => set(showGroupForm, false)); + append($$anchor, div_6); }; - if_block(node_1, ($$render) => { - if (get(showGroupForm)) $$render(consequent_1); + if_block(node_2, ($$render) => { + if (get(showGroupForm)) $$render(consequent_2); }); - var node_3 = sibling(node_1, 2); - var consequent_2 = ($$anchor) => { - var div_12 = root_5$4(); - var div_13 = child(div_12); - var p = sibling(child(div_13), 2); + var node_4 = sibling(node_2, 2); + var consequent_3 = ($$anchor) => { + var div_13 = root_6$4(); + var div_14 = child(div_13); + var p = sibling(child(div_14), 2); var strong = sibling(child(p)); - var text_4 = child(strong, true); + var text_5 = child(strong, true); reset(strong); next(); reset(p); - var div_14 = sibling(p, 2); - var button_8 = child(div_14); - var button_9 = sibling(button_8, 2); + var div_15 = sibling(p, 2); + var button_9 = child(div_15); + var button_10 = sibling(button_9, 2); + reset(div_15); reset(div_14); reset(div_13); - reset(div_12); - template_effect(() => set_text(text_4, get(deletingGroup).name)); - delegated("click", div_12, () => set(showDeleteGroupConfirm, null)); - delegated("click", div_13, (e) => e.stopPropagation()); - delegated("click", button_8, () => confirmDeleteGroup(get(deletingGroup).id)); - delegated("click", button_9, () => set(showDeleteGroupConfirm, null)); - append($$anchor, div_12); + template_effect(() => set_text(text_5, get(deletingGroup).name)); + delegated("click", div_13, () => set(showDeleteGroupConfirm, null)); + delegated("click", div_14, (e) => e.stopPropagation()); + delegated("click", button_9, () => confirmDeleteGroup(get(deletingGroup).id)); + delegated("click", button_10, () => set(showDeleteGroupConfirm, null)); + append($$anchor, div_13); }; - if_block(node_3, ($$render) => { - if (get(deletingGroup)) $$render(consequent_2); + if_block(node_4, ($$render) => { + if (get(deletingGroup)) $$render(consequent_3); }); reset(div); template_effect(() => { set_value(input, search.query); set_class(button, 1, `group-item ${search.activeGroupId === "all" ? "active" : ""}`, "svelte-181dlmc"); + set_class(button_4, 1, `group-item ${search.activeGroupId === "trash" ? "active" : ""}`, "svelte-181dlmc"); + set_style(span_2, `background-color: #e5484d`); + set_text(text_1, TRASH_GROUP_NAME); }); delegated("input", input, (e) => search.setSearchQuery(e.target.value)); delegated("click", button, () => search.activeGroupId = "all"); - delegated("click", button_4, () => openGroupForm(null)); + delegated("click", button_4, () => search.activeGroupId = "trash"); + delegated("click", button_5, () => openGroupForm(null)); append($$anchor, div); pop(); } @@ -6090,9 +6165,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$4 = /* @__PURE__ */ from_html(`matching " "`, 1); -var root_7$2 = /* @__PURE__ */ from_html(` `); -var root_5$3 = /* @__PURE__ */ from_html(`
TitleUsernameURLNotes
`, 1); +var root_6$3 = /* @__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$1 = /* @__PURE__ */ from_html(` `); +var root_5$3 = /* @__PURE__ */ from_html(`
TitleUsernameURLNotes
`, 1); var root$5 = /* @__PURE__ */ from_html(`
`); function EntryList($$anchor, $$props) { push($$props, true); @@ -6101,21 +6179,31 @@ function EntryList($$anchor, $$props) { let error = /* @__PURE__ */ state(""); let resultCount = /* @__PURE__ */ state(0); let dragging = /* @__PURE__ */ state(false); + const isTrashView = /* @__PURE__ */ user_derived(() => search.activeGroupId === "trash"); async function loadEntries() { set(loading, true); set(error, ""); try { const query = search.debouncedQuery.trim(); const groupId = search.activeGroupId; - if (query) set(entries, await searchEntries(query, groupId !== "all" ? { groupId } : {}), true); - else if (groupId !== "all") set(entries, await getEntries({ groupId }), true); - else set(entries, await getEntries(), true); + const resolvedGroupId = groupId === "trash" ? TRASH_GROUP_ID : groupId; + if (query) set(entries, await searchEntries(query, resolvedGroupId !== "all" ? { groupId: resolvedGroupId } : {}), true); + else if (resolvedGroupId !== "all") set(entries, await getEntries({ groupId: resolvedGroupId }), true); + else set(entries, (await getEntries()).filter((e) => e.groupId !== TRASH_GROUP_ID), true); set(resultCount, get(entries).length, true); } catch (e) { set(error, "Failed to load entries: " + e.message); } set(loading, false); } + async function handleRestore(entryId) { + try { + await restoreEntry(entryId); + search.refresh(); + } catch (e) { + set(error, "Failed to restore: " + e.message); + } + } user_effect(() => { search.debouncedQuery; search.activeGroupId; @@ -6154,13 +6242,13 @@ function EntryList($$anchor, $$props) { append($$anchor, button); }; if_block(node_1, ($$render) => { - if (!search.query) $$render(consequent_2); + if (!search.query && !get(isTrashView)) $$render(consequent_2); }); reset(div_3); template_effect(() => { - set_text(text_1, search.query ? "🔍" : "🔑"); - set_text(text_2, search.query ? "No results found" : "No entries yet"); - set_text(text_3, search.query ? "Try a different search term" : "Add your first login credential to get started"); + set_text(text_1, search.query ? "🔍" : get(isTrashView) ? "🗑" : "🔑"); + set_text(text_2, search.query ? "No results found" : get(isTrashView) ? "Trash is empty" : "No entries yet"); + set_text(text_3, search.query ? "Try a different search term" : get(isTrashView) ? "Deleted entries will appear here" : "Add your first login credential to get started"); }); append($$anchor, div_3); }; @@ -6171,7 +6259,7 @@ function EntryList($$anchor, $$props) { var text_4 = child(span); var node_2 = sibling(text_4); var consequent_4 = ($$anchor) => { - var fragment_1 = root_6$4(); + var fragment_1 = root_6$3(); var strong = sibling(first_child(fragment_1)); var text_5 = child(strong, true); reset(strong); @@ -6185,48 +6273,82 @@ function EntryList($$anchor, $$props) { reset(span); reset(div_4); var table = sibling(div_4, 2); - var tbody = sibling(child(table)); + var thead = child(table); + var tr = child(thead); + var node_3 = sibling(child(tr), 4); + var consequent_5 = ($$anchor) => { + append($$anchor, root_7$4()); + }; + if_block(node_3, ($$render) => { + if (get(isTrashView)) $$render(consequent_5); + }); + reset(tr); + reset(thead); + var tbody = sibling(thead); each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => { - var tr = root_7$2(); - set_attribute(tr, "draggable", true); - var td = child(tr); - var span_1 = sibling(child(td), 2); - var text_6 = child(span_1, true); - reset(span_1); + var tr_1 = root_8$1(); + var td = child(tr_1); + var node_4 = child(td); + var consequent_6 = ($$anchor) => { + append($$anchor, root_9$1()); + }; + if_block(node_4, ($$render) => { + if (!get(isTrashView)) $$render(consequent_6); + }); + var span_2 = sibling(node_4, 2); + var text_6 = child(span_2, true); + reset(span_2); reset(td); var td_1 = sibling(td); - var span_2 = child(td_1); - var text_7 = child(span_2, true); - reset(span_2); + var span_3 = child(td_1); + var text_7 = child(span_3, true); + reset(span_3); reset(td_1); var td_2 = sibling(td_1); - var span_3 = child(td_2); - var text_8 = child(span_3, true); - reset(span_3); + var span_4 = child(td_2); + var text_8 = child(span_4, true); + reset(span_4); reset(td_2); var td_3 = sibling(td_2); - var span_4 = child(td_3); - var text_9 = child(span_4, true); - reset(span_4); + var span_5 = child(td_3); + var text_9 = child(span_5, true); + reset(span_5); reset(td_3); - reset(tr); + var node_5 = sibling(td_3); + var consequent_7 = ($$anchor) => { + var td_4 = root_10$1(); + var button_1 = child(td_4); + reset(td_4); + delegated("click", button_1, (e) => { + e.stopPropagation(); + handleRestore(get(entry).id); + }); + append($$anchor, td_4); + }; + if_block(node_5, ($$render) => { + if (get(isTrashView)) $$render(consequent_7); + }); + reset(tr_1); template_effect(() => { - set_class(tr, 1, `entry-row ${get(dragging) ? "dragging" : ""}`, "svelte-13s7gu4"); + set_attribute(tr_1, "draggable", !get(isTrashView)); + set_class(tr_1, 1, `entry-row ${get(dragging) ? "dragging" : ""}`, "svelte-13s7gu4"); set_text(text_6, get(entry).title); set_text(text_7, get(entry).username || "—"); set_text(text_8, get(entry).url || "—"); set_text(text_9, get(entry).notes || "—"); }); - delegated("click", tr, () => $$props.onSelect(get(entry).id)); - event("dragstart", tr, (e) => { - set(dragging, true); - e.dataTransfer.setData("text/plain", get(entry).id); - e.dataTransfer.effectAllowed = "move"; + delegated("click", tr_1, () => $$props.onSelect(get(entry).id)); + event("dragstart", tr_1, (e) => { + if (!get(isTrashView)) { + set(dragging, true); + e.dataTransfer.setData("text/plain", get(entry).id); + e.dataTransfer.effectAllowed = "move"; + } }); - event("dragend", tr, () => { + event("dragend", tr_1, () => { set(dragging, false); }); - append($$anchor, tr); + append($$anchor, tr_1); }); reset(tbody); reset(table); @@ -6250,11 +6372,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$3 = /* @__PURE__ */ from_html(`
Username
`); -var root_7$1 = /* @__PURE__ */ from_html(`
URL
`); -var root_8 = /* @__PURE__ */ from_html(`
Notes
`); -var root_9 = /* @__PURE__ */ from_html(``); -var root_5$2 = /* @__PURE__ */ from_html(`

Password
`, 1); +var root_6$2 = /* @__PURE__ */ from_html(` `, 1); +var root_7$3 = /* @__PURE__ */ from_html(` `, 1); +var root_8 = /* @__PURE__ */ from_html(`
Username
`); +var root_9 = /* @__PURE__ */ from_html(`
URL
`); +var root_10 = /* @__PURE__ */ from_html(`
Notes
`); +var root_11$1 = /* @__PURE__ */ from_html(``); +var root_12 = /* @__PURE__ */ from_html(``); +var root_5$2 = /* @__PURE__ */ from_html(`

Password
`, 1); var root$4 = /* @__PURE__ */ from_html(`
`); function EntryDetail($$anchor, $$props) { push($$props, true); @@ -6264,8 +6389,10 @@ function EntryDetail($$anchor, $$props) { let loading = /* @__PURE__ */ state(true); let error = /* @__PURE__ */ state(""); let showDeleteConfirm = /* @__PURE__ */ state(false); + let showPermanentDeleteConfirm = /* @__PURE__ */ state(false); let deleting = /* @__PURE__ */ state(false); let toast = /* @__PURE__ */ state(""); + const isInTrash = /* @__PURE__ */ user_derived(() => get(entry) && isTrashGroup(get(entry).groupId)); let toastTimer = null; async function loadEntry() { set(loading, true); @@ -6307,17 +6434,29 @@ function EntryDetail($$anchor, $$props) { showToast(`✓ ${label} copied`); } } - async function handleDelete() { + async function handleMoveToTrash() { set(deleting, true); try { - await deleteEntry($$props.entryId); + await moveToTrash($$props.entryId); $$props.onBack(); } catch (e) { - set(error, "Failed to delete: " + e.message); + set(error, "Failed to move to trash: " + e.message); } set(deleting, false); set(showDeleteConfirm, false); } + async function handlePermanentDelete() { + set(deleting, true); + try { + await moveToTrash($$props.entryId); + await emptyTrash(); + $$props.onBack(); + } catch (e) { + set(error, "Failed to permanently delete: " + e.message); + } + set(deleting, false); + set(showPermanentDeleteConfirm, false); + } var div = root$4(); var node = child(div); var consequent = ($$anchor) => { @@ -6344,7 +6483,7 @@ function EntryDetail($$anchor, $$props) { var consequent_3 = ($$anchor) => { append($$anchor, root_4$3()); }; - var alternate = ($$anchor) => { + var alternate_1 = ($$anchor) => { var fragment = root_5$2(); var div_5 = first_child(fragment); var div_6 = child(div_5); @@ -6352,62 +6491,81 @@ function EntryDetail($$anchor, $$props) { var text_3 = child(h2, true); reset(h2); var div_7 = sibling(h2, 2); - var button = child(div_7); - var button_1 = sibling(button, 2); + var node_2 = child(div_7); + var consequent_4 = ($$anchor) => { + var fragment_1 = root_6$2(); + var button = first_child(fragment_1); + var button_1 = sibling(button, 2); + delegated("click", button, () => $$props.onEdit(get(entry).id)); + delegated("click", button_1, () => set(showPermanentDeleteConfirm, true)); + append($$anchor, fragment_1); + }; + var alternate = ($$anchor) => { + var fragment_2 = root_7$3(); + var button_2 = first_child(fragment_2); + var button_3 = sibling(button_2, 2); + delegated("click", button_2, () => $$props.onEdit(get(entry).id)); + delegated("click", button_3, () => set(showDeleteConfirm, true)); + append($$anchor, fragment_2); + }; + if_block(node_2, ($$render) => { + if (get(isInTrash)) $$render(consequent_4); + else $$render(alternate, -1); + }); reset(div_7); reset(div_6); var div_8 = sibling(div_6, 2); - var node_2 = child(div_8); - var consequent_4 = ($$anchor) => { - var div_9 = root_6$3(); + var node_3 = child(div_8); + var consequent_5 = ($$anchor) => { + var div_9 = root_8(); var div_10 = sibling(child(div_9), 2); var span = child(div_10); var text_4 = child(span, true); reset(span); - var button_2 = sibling(span, 2); + var button_4 = sibling(span, 2); reset(div_10); reset(div_9); template_effect(() => set_text(text_4, get(entry).username)); - delegated("click", button_2, () => copyToClipboard(get(entry).username, "Username")); + delegated("click", button_4, () => copyToClipboard(get(entry).username, "Username")); append($$anchor, div_9); }; - if_block(node_2, ($$render) => { - if (get(entry).username) $$render(consequent_4); + if_block(node_3, ($$render) => { + if (get(entry).username) $$render(consequent_5); }); - var div_11 = sibling(node_2, 2); + var div_11 = sibling(node_3, 2); var div_12 = sibling(child(div_11), 2); var span_1 = child(div_12); var text_5 = child(span_1, true); reset(span_1); - var button_3 = sibling(span_1, 2); - var text_6 = child(button_3, true); - reset(button_3); - var button_4 = sibling(button_3, 2); + var button_5 = sibling(span_1, 2); + var text_6 = child(button_5, true); + reset(button_5); + var button_6 = sibling(button_5, 2); reset(div_12); reset(div_11); - var node_3 = sibling(div_11, 2); - var consequent_5 = ($$anchor) => { - var div_13 = root_7$1(); + var node_4 = sibling(div_11, 2); + var consequent_6 = ($$anchor) => { + var div_13 = root_9(); var div_14 = sibling(child(div_13), 2); var a = child(div_14); var text_7 = child(a, true); reset(a); - var button_5 = sibling(a, 2); + var button_7 = sibling(a, 2); reset(div_14); reset(div_13); template_effect(() => { set_attribute(a, "href", get(entry).url); set_text(text_7, get(entry).url); }); - delegated("click", button_5, () => copyToClipboard(get(entry).url, "URL")); + delegated("click", button_7, () => copyToClipboard(get(entry).url, "URL")); append($$anchor, div_13); }; - if_block(node_3, ($$render) => { - if (get(entry).url) $$render(consequent_5); + if_block(node_4, ($$render) => { + if (get(entry).url) $$render(consequent_6); }); - var node_4 = sibling(node_3, 2); - var consequent_6 = ($$anchor) => { - var div_15 = root_8(); + var node_5 = sibling(node_4, 2); + var consequent_7 = ($$anchor) => { + var div_15 = root_10(); var div_16 = sibling(child(div_15), 2); var text_8 = child(div_16, true); reset(div_16); @@ -6415,8 +6573,8 @@ function EntryDetail($$anchor, $$props) { template_effect(() => set_text(text_8, get(entry).notes)); append($$anchor, div_15); }; - if_block(node_4, ($$render) => { - if (get(entry).notes) $$render(consequent_6); + if_block(node_5, ($$render) => { + if (get(entry).notes) $$render(consequent_7); }); reset(div_8); var div_17 = sibling(div_8, 2); @@ -6428,9 +6586,9 @@ function EntryDetail($$anchor, $$props) { reset(span_3); reset(div_17); reset(div_5); - var node_5 = sibling(div_5, 2); - var consequent_7 = ($$anchor) => { - var div_18 = root_9(); + var node_6 = sibling(div_5, 2); + var consequent_8 = ($$anchor) => { + var div_18 = root_11$1(); var div_19 = child(div_18); var p = sibling(child(div_19), 2); var strong = sibling(child(p)); @@ -6439,26 +6597,58 @@ function EntryDetail($$anchor, $$props) { next(); reset(p); var div_20 = sibling(p, 2); - var button_6 = child(div_20); - var text_12 = child(button_6, true); - reset(button_6); - var button_7 = sibling(button_6, 2); + var button_8 = child(div_20); + var text_12 = child(button_8, true); + reset(button_8); + var button_9 = sibling(button_8, 2); reset(div_20); reset(div_19); reset(div_18); template_effect(() => { set_text(text_11, get(entry).title); - button_6.disabled = get(deleting); - set_text(text_12, get(deleting) ? "Deleting..." : "Yes, delete"); + button_8.disabled = get(deleting); + set_text(text_12, get(deleting) ? "Moving..." : "Move to Trash"); }); delegated("click", div_18, () => set(showDeleteConfirm, false)); delegated("click", div_19, (e) => e.stopPropagation()); - delegated("click", button_6, handleDelete); - delegated("click", button_7, () => set(showDeleteConfirm, false)); + delegated("click", button_8, handleMoveToTrash); + delegated("click", button_9, () => set(showDeleteConfirm, false)); append($$anchor, div_18); }; - if_block(node_5, ($$render) => { - if (get(showDeleteConfirm)) $$render(consequent_7); + if_block(node_6, ($$render) => { + if (get(showDeleteConfirm)) $$render(consequent_8); + }); + var node_7 = sibling(node_6, 2); + var consequent_9 = ($$anchor) => { + var div_21 = root_12(); + var div_22 = child(div_21); + var p_1 = sibling(child(div_22), 2); + var strong_1 = sibling(child(p_1)); + var text_13 = child(strong_1, true); + reset(strong_1); + next(); + reset(p_1); + var div_23 = sibling(p_1, 2); + var button_10 = child(div_23); + var text_14 = child(button_10, true); + reset(button_10); + var button_11 = sibling(button_10, 2); + reset(div_23); + reset(div_22); + reset(div_21); + template_effect(() => { + set_text(text_13, get(entry).title); + button_10.disabled = get(deleting); + set_text(text_14, get(deleting) ? "Deleting..." : "Delete Forever"); + }); + delegated("click", div_21, () => set(showPermanentDeleteConfirm, false)); + delegated("click", div_22, (e) => e.stopPropagation()); + delegated("click", button_10, handlePermanentDelete); + delegated("click", button_11, () => set(showPermanentDeleteConfirm, false)); + append($$anchor, div_21); + }; + if_block(node_7, ($$render) => { + if (get(showPermanentDeleteConfirm)) $$render(consequent_9); }); template_effect(($0, $1) => { set_text(text_3, get(entry).title); @@ -6467,17 +6657,15 @@ function EntryDetail($$anchor, $$props) { set_text(text_9, `Created: ${$0 ?? ""}`); set_text(text_10, `Updated: ${$1 ?? ""}`); }, [() => new Date(get(entry).createdAt).toLocaleString(), () => new Date(get(entry).updatedAt).toLocaleString()]); - delegated("click", button, () => $$props.onEdit(get(entry).id)); - delegated("click", button_1, () => set(showDeleteConfirm, true)); - delegated("click", button_3, () => set(passwordVisible, !get(passwordVisible))); - delegated("click", button_4, () => copyToClipboard(get(decryptedPassword), "Password")); + delegated("click", button_5, () => set(passwordVisible, !get(passwordVisible))); + delegated("click", button_6, () => copyToClipboard(get(decryptedPassword), "Password")); append($$anchor, fragment); }; if_block(node_1, ($$render) => { if (get(loading)) $$render(consequent_1); else if (get(error)) $$render(consequent_2, 1); else if (!get(entry)) $$render(consequent_3, 2); - else $$render(alternate, -1); + else $$render(alternate_1, -1); }); reset(div); append($$anchor, div); @@ -6491,7 +6679,7 @@ var root_1$3 = /* @__PURE__ */ from_html(`
Loa var root_3$2 = /* @__PURE__ */ from_html(`
`); var root_5$1 = /* @__PURE__ */ from_html(`
`); var root_4$2 = /* @__PURE__ */ from_html(`
`); -var root_6$2 = /* @__PURE__ */ from_html(``); +var root_7$2 = /* @__PURE__ */ from_html(``); var root_2$2 = /* @__PURE__ */ from_html(`
`, 1); var root$2 = /* @__PURE__ */ from_html(`
`); function EntryForm($$anchor, $$props) { @@ -6633,15 +6821,24 @@ function EntryForm($$anchor, $$props) { var option = child(select); option.value = option.__value = ""; each(sibling(option), 17, () => get(groups), index, ($$anchor, group) => { - var option_1 = root_6$2(); - var text_3 = child(option_1, true); - reset(option_1); - var option_1_value = {}; - template_effect(() => { - set_text(text_3, get(group).name); - if (option_1_value !== (option_1_value = get(group).id)) option_1.value = (option_1.__value = get(group).id) ?? ""; + var fragment_1 = comment(); + var node_4 = first_child(fragment_1); + var consequent_3 = ($$anchor) => { + var option_1 = root_7$2(); + var text_3 = child(option_1, true); + reset(option_1); + var option_1_value = {}; + template_effect(() => { + set_text(text_3, get(group).name); + if (option_1_value !== (option_1_value = get(group).id)) option_1.value = (option_1.__value = get(group).id) ?? ""; + }); + append($$anchor, option_1); + }; + var d = /* @__PURE__ */ user_derived(() => !isTrashGroup(get(group).id)); + if_block(node_4, ($$render) => { + if (get(d)) $$render(consequent_3); }); - append($$anchor, option_1); + append($$anchor, fragment_1); }); reset(select); reset(div_10); @@ -6693,7 +6890,7 @@ var root_1$2 = /* @__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 = /* @__PURE__ */ from_html(`

Select how to handle existing data:

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

Select how to handle existing data:

`, 1); var root_2$1 = /* @__PURE__ */ from_html(``); var root$1 = /* @__PURE__ */ from_html(`
`); function ImportExport($$anchor, $$props) { @@ -6866,7 +7063,7 @@ function ImportExport($$anchor, $$props) { append($$anchor, fragment_1); }; var alternate = ($$anchor) => { - var fragment_2 = root_7(); + var fragment_2 = root_7$1(); var div_11 = sibling(first_child(fragment_2), 2); var label_2 = child(div_11); var input_3 = child(label_2); @@ -6923,16 +7120,21 @@ delegate(["click", "change"]); //#region src/components/MainLayout.svelte var root_1$1 = /* @__PURE__ */ from_html(``); var root_2 = /* @__PURE__ */ from_html(``); -var root_3 = /* @__PURE__ */ from_html(`

All Entries

`); +var root_3 = /* @__PURE__ */ from_html(`

`); var root_4 = /* @__PURE__ */ from_html(`

Entry Details

`); var root_5 = /* @__PURE__ */ from_html(`

`); -var root_6 = /* @__PURE__ */ from_html(``); -var root = /* @__PURE__ */ from_html(`
Password Vault
`); +var root_6 = /* @__PURE__ */ from_html(``); +var root_7 = /* @__PURE__ */ from_html(``); +var root_11 = /* @__PURE__ */ from_html(``); +var root = /* @__PURE__ */ from_html(`
Password Vault
`); function MainLayout($$anchor, $$props) { push($$props, true); let sidebarOpen = /* @__PURE__ */ state(false); let viewMode = /* @__PURE__ */ state("list"); let selectedEntryId = /* @__PURE__ */ state(null); + let showEmptyTrashConfirm = /* @__PURE__ */ state(false); + let emptyingTrash = /* @__PURE__ */ state(false); + const isTrashView = /* @__PURE__ */ user_derived(() => search.activeGroupId === "trash"); function goList() { set(viewMode, "list"); set(selectedEntryId, null); @@ -6952,6 +7154,17 @@ function MainLayout($$anchor, $$props) { if (get(viewMode) === "form") goDetail(get(selectedEntryId)); else goList(); } + async function handleEmptyTrash() { + set(emptyingTrash, true); + try { + await emptyTrash(); + search.activeGroupId = "all"; + set(showEmptyTrashConfirm, false); + } catch (e) { + console.error("Failed to empty trash:", e); + } + set(emptyingTrash, false); + } function handleLock() { app$1.lockVault(); } @@ -6986,16 +7199,20 @@ function MainLayout($$anchor, $$props) { var div_3 = sibling(node_2, 2); var node_3 = child(div_3); var consequent_2 = ($$anchor) => { - append($$anchor, root_3()); + var h1 = root_3(); + var text = child(h1, true); + reset(h1); + template_effect(() => set_text(text, search.activeGroupId === "trash" ? TRASH_GROUP_NAME : "All Entries")); + append($$anchor, h1); }; var consequent_3 = ($$anchor) => { append($$anchor, root_4()); }; var consequent_4 = ($$anchor) => { var h1_2 = root_5(); - var text = child(h1_2, true); + var text_1 = child(h1_2, true); reset(h1_2); - template_effect(() => set_text(text, get(selectedEntryId) ? "Edit Entry" : "New Entry")); + template_effect(() => set_text(text_1, get(selectedEntryId) ? "Edit Entry" : "New Entry")); append($$anchor, h1_2); }; if_block(node_3, ($$render) => { @@ -7008,26 +7225,41 @@ function MainLayout($$anchor, $$props) { var node_4 = child(div_4); var consequent_5 = ($$anchor) => { var button_4 = root_6(); - delegated("click", button_4, () => goForm(null)); + var text_2 = child(button_4, true); + reset(button_4); + template_effect(() => { + button_4.disabled = get(emptyingTrash); + set_text(text_2, get(emptyingTrash) ? "Emptying..." : "🗑 Empty Trash"); + }); + delegated("click", button_4, () => set(showEmptyTrashConfirm, true)); append($$anchor, button_4); }; if_block(node_4, ($$render) => { - if (get(viewMode) === "list") $$render(consequent_5); + if (get(viewMode) === "list" && get(isTrashView)) $$render(consequent_5); }); var node_5 = sibling(node_4, 2); - ImportExport(node_5, {}); - var button_5 = sibling(node_5, 2); + var consequent_6 = ($$anchor) => { + var button_5 = root_7(); + delegated("click", button_5, () => goForm(null)); + append($$anchor, button_5); + }; + if_block(node_5, ($$render) => { + if (get(viewMode) === "list" && !get(isTrashView)) $$render(consequent_6); + }); + var node_6 = sibling(node_5, 2); + ImportExport(node_6, {}); + var button_6 = sibling(node_6, 2); reset(div_4); reset(div_2); var div_5 = sibling(div_2, 2); - var node_6 = child(div_5); - var consequent_6 = ($$anchor) => { + var node_7 = child(div_5); + var consequent_7 = ($$anchor) => { EntryList($$anchor, { onSelect: goDetail, onAdd: () => goForm(null) }); }; - var consequent_7 = ($$anchor) => { + var consequent_8 = ($$anchor) => { EntryDetail($$anchor, { get entryId() { return get(selectedEntryId); @@ -7036,7 +7268,7 @@ function MainLayout($$anchor, $$props) { onBack: goList }); }; - var consequent_8 = ($$anchor) => { + var consequent_9 = ($$anchor) => { EntryForm($$anchor, { get entryId() { return get(selectedEntryId); @@ -7045,18 +7277,43 @@ function MainLayout($$anchor, $$props) { onCancel: handleBack }); }; - if_block(node_6, ($$render) => { - if (get(viewMode) === "list") $$render(consequent_6); - else if (get(viewMode) === "detail" && get(selectedEntryId)) $$render(consequent_7, 1); - else if (get(viewMode) === "form") $$render(consequent_8, 2); + if_block(node_7, ($$render) => { + if (get(viewMode) === "list") $$render(consequent_7); + else if (get(viewMode) === "detail" && get(selectedEntryId)) $$render(consequent_8, 1); + else if (get(viewMode) === "form") $$render(consequent_9, 2); }); reset(div_5); reset(main); + var node_8 = sibling(main, 2); + var consequent_10 = ($$anchor) => { + var div_6 = root_11(); + var div_7 = child(div_6); + var div_8 = sibling(child(div_7), 4); + var button_7 = child(div_8); + var text_3 = child(button_7, true); + reset(button_7); + var button_8 = sibling(button_7, 2); + reset(div_8); + reset(div_7); + reset(div_6); + template_effect(() => { + button_7.disabled = get(emptyingTrash); + set_text(text_3, get(emptyingTrash) ? "Emptying..." : "Yes, empty trash"); + }); + delegated("click", div_6, () => set(showEmptyTrashConfirm, false)); + delegated("click", div_7, (e) => e.stopPropagation()); + delegated("click", button_7, handleEmptyTrash); + delegated("click", button_8, () => set(showEmptyTrashConfirm, false)); + append($$anchor, div_6); + }; + if_block(node_8, ($$render) => { + if (get(showEmptyTrashConfirm)) $$render(consequent_10); + }); reset(div); template_effect(() => set_class(aside, 1, `sidebar ${get(sidebarOpen) ? "open" : ""}`, "svelte-1ybayt")); delegated("click", button, () => set(sidebarOpen, !get(sidebarOpen))); delegated("click", button_1, handleLock); - delegated("click", button_5, handleLock); + delegated("click", button_6, handleLock); append($$anchor, div); pop(); } @@ -7455,6 +7712,11 @@ label { opacity: 1; } + .trash-section.svelte-181dlmc { + padding: 8px; + border-top: 1px solid var(--color-border); + } + .group-action-btn.svelte-181dlmc { background: none; border: none; @@ -7634,6 +7896,11 @@ label { opacity: 0.7; } + .restore-btn.svelte-13s7gu4 { + font-size: 0.85rem; + padding: 4px 6px; + } + .entry-title.svelte-13s7gu4 { font-weight: 500; } @@ -8093,6 +8360,43 @@ label { height: 100%; } + /* Modal */ + .modal-overlay.svelte-1ybayt { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.6); + display: flex; + align-items: center; + justify-content: center; + z-index: 200; + padding: 1rem; + } + + .modal.svelte-1ybayt { + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + padding: 24px; + max-width: 380px; + width: 100%; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); + } + + .modal.svelte-1ybayt h3:where(.svelte-1ybayt) { + margin-bottom: 16px; + } + + .modal.svelte-1ybayt p:where(.svelte-1ybayt) { + color: var(--color-text-muted); + font-size: 0.9rem; + margin-bottom: 20px; + } + + .modal-actions.svelte-1ybayt { + display: flex; + gap: 8px; + } + /* Responsive */ @media (max-width: 768px) { .mobile-header.svelte-1ybayt { diff --git a/src/components/EntryDetail.svelte b/src/components/EntryDetail.svelte index ce3ae76..dd47fd2 100644 --- a/src/components/EntryDetail.svelte +++ b/src/components/EntryDetail.svelte @@ -1,7 +1,8 @@
@@ -94,8 +111,13 @@

{entry.title}

- - + {#if isInTrash} + + + {:else} + + + {/if}
@@ -145,22 +167,39 @@
- + {#if showDeleteConfirm} diff --git a/src/components/EntryForm.svelte b/src/components/EntryForm.svelte index 18abc1c..575a6ec 100644 --- a/src/components/EntryForm.svelte +++ b/src/components/EntryForm.svelte @@ -1,7 +1,7 @@