From 7d9d3aef0e75a6a3dbac832742e8f5d6dcdf8777 Mon Sep 17 00:00:00 2001 From: Timothy Farrell Date: Sat, 16 May 2026 00:25:49 +0000 Subject: [PATCH] Add dragdrop grouping --- dist/index.html | 128 +++++++++++++++++++++++++++++--- src/components/EntryList.svelte | 35 ++++++++- src/components/Sidebar.svelte | 57 +++++++++++++- src/lib/storage/db.js | 15 ++++ 4 files changed, 221 insertions(+), 14 deletions(-) diff --git a/dist/index.html b/dist/index.html index 7b2549d..cbf57e4 100644 --- a/dist/index.html +++ b/dist/index.html @@ -5538,6 +5538,20 @@ async function searchEntries(query, options = {}) { return entries.filter((e) => e.title.toLowerCase().includes(lower) || e.username.toLowerCase().includes(lower) || e.url && e.url.toLowerCase().includes(lower) || e.notes && e.notes.toLowerCase().includes(lower)); } /** +* Move an entry to a different group (or ungroup it by passing empty string). +* @param {string} entryId +* @param {string} groupId +* @returns {Promise} +*/ +async function moveEntryToGroup(entryId, groupId) { + const db = await getDb(); + const entry = await db.get("entries", entryId); + if (!entry) throw new Error("Entry not found"); + entry.groupId = groupId; + entry.updatedAt = (/* @__PURE__ */ new Date()).toISOString(); + await db.put("entries", entry); +} +/** * Count entries per group. * @returns {Promise>} */ @@ -5818,12 +5832,12 @@ var SearchStore = class { var search = new SearchStore(); //#endregion //#region src/components/Sidebar.svelte -var root_1$7 = /* @__PURE__ */ from_html(`
`); +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_5$4 = /* @__PURE__ */ from_html(``); +var root$6 = /* @__PURE__ */ from_html(``); function Sidebar($$anchor, $$props) { push($$props, true); let groups = /* @__PURE__ */ state(proxy([])); @@ -5835,6 +5849,22 @@ function Sidebar($$anchor, $$props) { let groupError = /* @__PURE__ */ state(""); let showDeleteGroupConfirm = /* @__PURE__ */ state(null); let deletingGroup = /* @__PURE__ */ user_derived(() => get(groups).find((g) => g.id === get(showDeleteGroupConfirm))); + let dragOverGroupId = /* @__PURE__ */ state(null); + let droppedGroupId = /* @__PURE__ */ state(null); + async function handleDrop(groupId, entryId) { + try { + await moveEntryToGroup(entryId, groupId); + set(droppedGroupId, groupId, true); + setTimeout(() => { + set(droppedGroupId, null); + }, 600); + await loadData(); + search.refresh(); + } catch (e) {} + } + function canDrop(groupId) { + return groupId !== search.activeGroupId; + } const GROUP_COLORS = [ "#6c63ff", "#e5484d", @@ -5918,6 +5948,7 @@ function Sidebar($$anchor, $$props) { 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); @@ -5925,11 +5956,29 @@ function Sidebar($$anchor, $$props) { reset(div_3); reset(div_2); template_effect(() => { - set_class(button_1, 1, `group-item ${search.activeGroupId === get(group).id ? "active" : ""}`, "svelte-181dlmc"); + 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(); + 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); @@ -6042,7 +6091,7 @@ var root_2$4 = /* @__PURE__ */ from_html(`
+ New Entry`); 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_7$2 = /* @__PURE__ */ from_html(` `); var root_5$3 = /* @__PURE__ */ from_html(`
TitleUsernameURL
`, 1); var root$5 = /* @__PURE__ */ from_html(`
`); function EntryList($$anchor, $$props) { @@ -6051,6 +6100,7 @@ function EntryList($$anchor, $$props) { let loading = /* @__PURE__ */ state(true); let error = /* @__PURE__ */ state(""); let resultCount = /* @__PURE__ */ state(0); + let dragging = /* @__PURE__ */ state(false); async function loadEntries() { set(loading, true); set(error, ""); @@ -6138,8 +6188,9 @@ function EntryList($$anchor, $$props) { var tbody = sibling(child(table)); 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 = child(td); + var span_1 = sibling(child(td), 2); var text_6 = child(span_1, true); reset(span_1); reset(td); @@ -6155,11 +6206,20 @@ function EntryList($$anchor, $$props) { reset(td_2); reset(tr); template_effect(() => { + set_class(tr, 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 || "—"); }); 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"; + }); + event("dragend", tr, () => { + set(dragging, false); + }); append($$anchor, tr); }); reset(tbody); @@ -7330,6 +7390,35 @@ label { color: var(--color-primary); } + .group-item.drag-over.svelte-181dlmc { + background: rgba(108, 99, 255, 0.2); + border: 2px dashed var(--color-primary); + outline: 2px solid rgba(108, 99, 255, 0.25); + outline-offset: -4px; + transform: scale(1.02); + } + + .group-item.drag-over.svelte-181dlmc .drop-icon:where(.svelte-181dlmc) { + opacity: 1; + transform: scale(1.2); + } + + .group-item.dropped.svelte-181dlmc { + animation: svelte-181dlmc-dropFlash 600ms ease; + } + + @keyframes svelte-181dlmc-dropFlash { + 0% { background: rgba(52, 211, 153, 0.35); } + 100% { background: transparent; } + } + + .drop-icon.svelte-181dlmc { + opacity: 0; + font-size: 0.85rem; + transition: opacity 150ms, transform 150ms; + flex-shrink: 0; + } + .group-icon.svelte-181dlmc { font-size: 1rem; } @@ -7504,20 +7593,41 @@ label { } .entry-row.svelte-13s7gu4 { - cursor: pointer; - transition: background-color 150ms; + cursor: grab; + transition: background-color 150ms, opacity 150ms; + } + + .entry-row.svelte-13s7gu4:active { + cursor: grabbing; } .entry-row.svelte-13s7gu4:hover { background: var(--color-surface-hover); } + .entry-row.dragging.svelte-13s7gu4 { + opacity: 0.35; + } + .entry-row.svelte-13s7gu4 td:where(.svelte-13s7gu4) { padding: 10px 12px; font-size: 0.875rem; border-bottom: 1px solid var(--color-border); } + .drag-handle.svelte-13s7gu4 { + color: var(--color-text-muted); + opacity: 0.3; + margin-right: 6px; + font-size: 0.9rem; + user-select: none; + transition: opacity 150ms; + } + + .entry-row.svelte-13s7gu4:hover .drag-handle:where(.svelte-13s7gu4) { + opacity: 0.7; + } + .entry-title.svelte-13s7gu4 { font-weight: 500; } diff --git a/src/components/EntryList.svelte b/src/components/EntryList.svelte index 9acff08..5d7e025 100644 --- a/src/components/EntryList.svelte +++ b/src/components/EntryList.svelte @@ -6,6 +6,7 @@ let loading = $state(true) let error = $state('') let resultCount = $state(0) + let dragging = $state(false) let { onSelect, onAdd } = $props() @@ -82,8 +83,15 @@ {#each entries as entry (entry.id)} - onSelect(entry.id)} class="entry-row"> + onSelect(entry.id)} + ondragstart={(e) => { dragging = true; e.dataTransfer.setData('text/plain', entry.id); e.dataTransfer.effectAllowed = 'move'; }} + ondragend={() => { dragging = false; }} + class="entry-row {dragging ? 'dragging' : ''}" + > + {entry.title} @@ -153,20 +161,41 @@ } .entry-row { - cursor: pointer; - transition: background-color 150ms; + cursor: grab; + transition: background-color 150ms, opacity 150ms; + } + + .entry-row:active { + cursor: grabbing; } .entry-row:hover { background: var(--color-surface-hover); } + .entry-row.dragging { + opacity: 0.35; + } + .entry-row td { padding: 10px 12px; font-size: 0.875rem; border-bottom: 1px solid var(--color-border); } + .drag-handle { + color: var(--color-text-muted); + opacity: 0.3; + margin-right: 6px; + font-size: 0.9rem; + user-select: none; + transition: opacity 150ms; + } + + .entry-row:hover .drag-handle { + opacity: 0.7; + } + .entry-title { font-weight: 500; } diff --git a/src/components/Sidebar.svelte b/src/components/Sidebar.svelte index 02c39c5..e80a7cc 100644 --- a/src/components/Sidebar.svelte +++ b/src/components/Sidebar.svelte @@ -1,5 +1,5 @@