Add drag-drop grouping
This commit is contained in:
parent
0fdcaf4ecc
commit
66ed3935a4
213
dist/index.html
vendored
213
dist/index.html
vendored
@ -5760,8 +5760,9 @@ var root_1$7 = /* @__PURE__ */ from_html(`<div class="group-row svelte-181dlmc">
|
||||
var root_3$5 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-181dlmc"> </div>`);
|
||||
var root_4$5 = /* @__PURE__ */ from_html(`<button></button>`);
|
||||
var root_2$5 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Group settings" tabindex="-1"><h3 class="svelte-181dlmc"> </h3> <!> <div class="form-group"><label for="group-name">Group Name</label> <input id="group-name" type="text" placeholder="e.g. Work, Personal"/></div> <div class="form-group"><span class="field-label">Color</span> <div class="color-picker svelte-181dlmc"></div></div> <div class="modal-actions svelte-181dlmc"><button class="btn btn-primary"> </button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
||||
var root_5$4 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Delete group confirmation" tabindex="-1"><h3 class="svelte-181dlmc">Delete Group</h3> <p class="svelte-181dlmc">Delete "<strong> </strong>"? Entries in this group will become ungrouped.</p> <div class="modal-actions svelte-181dlmc"><button class="btn btn-danger">Yes, delete</button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
||||
var root$6 = /* @__PURE__ */ from_html(`<div class="sidebar-content svelte-181dlmc"><div class="sidebar-header svelte-181dlmc"><h2 class="svelte-181dlmc">🔐 Vault</h2></div> <div class="search-box svelte-181dlmc"><input type="text" placeholder="Search entries..." class="svelte-181dlmc"/></div> <nav class="groups-nav svelte-181dlmc"><button><span class="group-icon svelte-181dlmc">📋</span> <span class="group-name svelte-181dlmc">All Entries</span></button> <!></nav> <div class="sidebar-footer svelte-181dlmc"><button class="btn btn-ghost btn-sm w-full">+ New Group</button></div> <!> <!></div>`);
|
||||
var root_5$4 = /* @__PURE__ */ from_html(`<div class="drop-indicator svelte-181dlmc">Drop here to move</div>`);
|
||||
var root_6$5 = /* @__PURE__ */ from_html(`<div class="modal-overlay svelte-181dlmc" role="presentation"><div class="modal svelte-181dlmc" role="dialog" aria-modal="true" aria-label="Delete group confirmation" tabindex="-1"><h3 class="svelte-181dlmc">Delete Group</h3> <p class="svelte-181dlmc">Delete "<strong> </strong>"? Entries in this group will become ungrouped.</p> <div class="modal-actions svelte-181dlmc"><button class="btn btn-danger">Yes, delete</button> <button class="btn btn-ghost">Cancel</button></div></div></div>`);
|
||||
var root$6 = /* @__PURE__ */ from_html(`<div class="sidebar-content svelte-181dlmc"><div class="sidebar-header svelte-181dlmc"><h2 class="svelte-181dlmc">🔐 Vault</h2></div> <div class="search-box svelte-181dlmc"><input type="text" placeholder="Search entries..." class="svelte-181dlmc"/></div> <nav class="groups-nav svelte-181dlmc"><div class="group-wrapper svelte-181dlmc"><button><span class="group-icon svelte-181dlmc">📋</span> <span class="group-name svelte-181dlmc">All Entries</span></button></div> <!></nav> <div class="sidebar-footer svelte-181dlmc"><button class="btn btn-ghost btn-sm w-full">+ New Group</button></div> <!> <!> <!></div>`);
|
||||
function Sidebar($$anchor, $$props) {
|
||||
push($$props, true);
|
||||
let groups = /* @__PURE__ */ state(proxy([]));
|
||||
@ -5773,6 +5774,7 @@ 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 dropTargetGroupId = /* @__PURE__ */ state(null);
|
||||
const GROUP_COLORS = [
|
||||
"#6c63ff",
|
||||
"#e5484d",
|
||||
@ -5842,65 +5844,96 @@ function Sidebar($$anchor, $$props) {
|
||||
set(groupError, "Failed to delete group: " + e.message);
|
||||
}
|
||||
}
|
||||
function onDragOverGroup(e, groupId) {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
set(dropTargetGroupId, groupId, true);
|
||||
}
|
||||
function onDragLeaveGroup() {
|
||||
set(dropTargetGroupId, null);
|
||||
}
|
||||
async function onDropGroup(groupId, e) {
|
||||
set(dropTargetGroupId, null);
|
||||
const entryId = e.dataTransfer.getData("text/plain");
|
||||
if (!entryId) return;
|
||||
try {
|
||||
const entry = await getEntryById(entryId);
|
||||
if (!entry) return;
|
||||
const newGroupId = groupId === "all" ? "" : groupId;
|
||||
await updateEntry({
|
||||
...entry,
|
||||
groupId: newGroupId,
|
||||
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
||||
});
|
||||
await loadData();
|
||||
} catch (err) {
|
||||
console.error("Failed to move entry:", err);
|
||||
}
|
||||
}
|
||||
var div = root$6();
|
||||
var div_1 = sibling(child(div), 2);
|
||||
var input = child(div_1);
|
||||
remove_input_defaults(input);
|
||||
reset(div_1);
|
||||
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 div_2 = child(nav);
|
||||
var button = child(div_2);
|
||||
reset(div_2);
|
||||
each(sibling(div_2, 2), 17, () => get(groups), index, ($$anchor, group) => {
|
||||
var div_3 = root_1$7();
|
||||
var button_1 = child(div_3);
|
||||
var span = child(button_1);
|
||||
var span_1 = sibling(span, 2);
|
||||
var text = child(span_1, true);
|
||||
reset(span_1);
|
||||
reset(button_1);
|
||||
var div_3 = sibling(button_1, 2);
|
||||
var button_2 = child(div_3);
|
||||
var div_4 = sibling(button_1, 2);
|
||||
var button_2 = child(div_4);
|
||||
var button_3 = sibling(button_2, 2);
|
||||
reset(div_4);
|
||||
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(dropTargetGroupId) === get(group).id ? "drop-target" : ""}`, "svelte-181dlmc");
|
||||
set_style(span, `background-color: ${(get(group).color || "#6c63ff") ?? ""}`);
|
||||
set_text(text, get(group).name);
|
||||
});
|
||||
event("DragOver", div_3, (e) => onDragOverGroup(e, get(group).id));
|
||||
event("DragLeave", div_3, onDragLeaveGroup);
|
||||
event("Drop", div_3, (e) => onDropGroup(get(group).id, e));
|
||||
delegated("click", button_1, () => search.activeGroupId = get(group).id);
|
||||
delegated("click", button_2, () => openGroupForm(get(group)));
|
||||
delegated("click", button_3, () => set(showDeleteGroupConfirm, get(group).id, true));
|
||||
append($$anchor, div_2);
|
||||
append($$anchor, div_3);
|
||||
});
|
||||
reset(nav);
|
||||
var div_4 = sibling(nav, 2);
|
||||
var button_4 = child(div_4);
|
||||
reset(div_4);
|
||||
var node_1 = sibling(div_4, 2);
|
||||
var div_5 = sibling(nav, 2);
|
||||
var button_4 = child(div_5);
|
||||
reset(div_5);
|
||||
var node_1 = sibling(div_5, 2);
|
||||
var consequent_1 = ($$anchor) => {
|
||||
var div_5 = root_2$5();
|
||||
var div_6 = child(div_5);
|
||||
var h3 = child(div_6);
|
||||
var div_6 = root_2$5();
|
||||
var div_7 = child(div_6);
|
||||
var h3 = child(div_7);
|
||||
var text_1 = 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);
|
||||
var div_8 = root_3$5();
|
||||
var text_2 = child(div_8, true);
|
||||
reset(div_8);
|
||||
template_effect(() => set_text(text_2, get(groupError)));
|
||||
append($$anchor, div_7);
|
||||
append($$anchor, div_8);
|
||||
};
|
||||
if_block(node_2, ($$render) => {
|
||||
if (get(groupError)) $$render(consequent);
|
||||
});
|
||||
var div_8 = sibling(node_2, 2);
|
||||
var input_1 = sibling(child(div_8), 2);
|
||||
var div_9 = sibling(node_2, 2);
|
||||
var input_1 = sibling(child(div_9), 2);
|
||||
remove_input_defaults(input_1);
|
||||
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) => {
|
||||
reset(div_9);
|
||||
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_5 = root_4$5();
|
||||
template_effect(() => {
|
||||
set_class(button_5, 1, `color-swatch ${get(groupColor) === get(color) ? "selected" : ""}`, "svelte-181dlmc");
|
||||
@ -5910,62 +5943,72 @@ function Sidebar($$anchor, $$props) {
|
||||
delegated("click", button_5, () => set(groupColor, get(color), true));
|
||||
append($$anchor, button_5);
|
||||
});
|
||||
reset(div_11);
|
||||
reset(div_10);
|
||||
reset(div_9);
|
||||
var div_11 = sibling(div_9, 2);
|
||||
var button_6 = child(div_11);
|
||||
var div_12 = sibling(div_10, 2);
|
||||
var button_6 = child(div_12);
|
||||
var text_3 = child(button_6, true);
|
||||
reset(button_6);
|
||||
var button_7 = sibling(button_6, 2);
|
||||
reset(div_11);
|
||||
reset(div_12);
|
||||
reset(div_7);
|
||||
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");
|
||||
});
|
||||
delegated("click", div_5, () => set(showGroupForm, false));
|
||||
delegated("click", div_6, (e) => e.stopPropagation());
|
||||
delegated("click", div_6, () => set(showGroupForm, false));
|
||||
delegated("click", div_7, (e) => e.stopPropagation());
|
||||
bind_value(input_1, () => get(groupName), ($$value) => set(groupName, $$value));
|
||||
delegated("click", button_6, saveGroup);
|
||||
delegated("click", button_7, () => set(showGroupForm, false));
|
||||
append($$anchor, div_5);
|
||||
append($$anchor, div_6);
|
||||
};
|
||||
if_block(node_1, ($$render) => {
|
||||
if (get(showGroupForm)) $$render(consequent_1);
|
||||
});
|
||||
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);
|
||||
append($$anchor, root_5$4());
|
||||
};
|
||||
if_block(node_3, ($$render) => {
|
||||
if (get(dropTargetGroupId)) $$render(consequent_2);
|
||||
});
|
||||
var node_4 = sibling(node_3, 2);
|
||||
var consequent_3 = ($$anchor) => {
|
||||
var div_14 = root_6$5();
|
||||
var div_15 = child(div_14);
|
||||
var p = sibling(child(div_15), 2);
|
||||
var strong = sibling(child(p));
|
||||
var text_4 = child(strong, true);
|
||||
reset(strong);
|
||||
next();
|
||||
reset(p);
|
||||
var div_14 = sibling(p, 2);
|
||||
var button_8 = child(div_14);
|
||||
var div_16 = sibling(p, 2);
|
||||
var button_8 = child(div_16);
|
||||
var button_9 = sibling(button_8, 2);
|
||||
reset(div_16);
|
||||
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", div_14, () => set(showDeleteGroupConfirm, null));
|
||||
delegated("click", div_15, (e) => e.stopPropagation());
|
||||
delegated("click", button_8, () => confirmDeleteGroup(get(deletingGroup).id));
|
||||
delegated("click", button_9, () => set(showDeleteGroupConfirm, null));
|
||||
append($$anchor, div_12);
|
||||
append($$anchor, div_14);
|
||||
};
|
||||
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, 1, `group-item ${search.activeGroupId === "all" ? "active" : ""} ${get(dropTargetGroupId) === "all" ? "drop-target" : ""}`, "svelte-181dlmc");
|
||||
});
|
||||
event("Input", input, (e) => search.query = e.target.value);
|
||||
event("DragOver", div_2, (e) => onDragOverGroup(e, "all"));
|
||||
event("DragLeave", div_2, onDragLeaveGroup);
|
||||
event("Drop", div_2, (e) => onDropGroup("all", e));
|
||||
delegated("click", button, () => search.activeGroupId = "all");
|
||||
delegated("click", button_4, () => openGroupForm(null));
|
||||
append($$anchor, div);
|
||||
@ -5979,7 +6022,7 @@ var root_2$4 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-13s7gu
|
||||
var root_4$4 = /* @__PURE__ */ from_html(`<button class="btn btn-primary mt-3">+ New Entry</button>`);
|
||||
var root_3$4 = /* @__PURE__ */ from_html(`<div class="empty-state svelte-13s7gu4"><p class="empty-icon svelte-13s7gu4"> </p> <p class="empty-text svelte-13s7gu4"> </p> <p class="empty-hint svelte-13s7gu4"> </p> <!></div>`);
|
||||
var root_6$4 = /* @__PURE__ */ from_html(`matching "<strong> </strong>"`, 1);
|
||||
var root_7$2 = /* @__PURE__ */ from_html(`<tr class="entry-row svelte-13s7gu4"><td class="svelte-13s7gu4"><span class="entry-title svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-username svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-url truncate svelte-13s7gu4"> </span></td></tr>`);
|
||||
var root_7$2 = /* @__PURE__ */ from_html(`<tr draggable="true"><td class="svelte-13s7gu4"><span class="drag-handle svelte-13s7gu4" title="Drag to move to another group">⠿</span> <span class="entry-title svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-username svelte-13s7gu4"> </span></td><td class="svelte-13s7gu4"><span class="entry-url truncate svelte-13s7gu4"> </span></td></tr>`);
|
||||
var root_5$3 = /* @__PURE__ */ from_html(`<div class="results-info svelte-13s7gu4"><span class="text-sm text-muted"> <!></span></div> <table class="entries-table svelte-13s7gu4"><thead><tr><th class="svelte-13s7gu4">Title</th><th class="svelte-13s7gu4">Username</th><th class="svelte-13s7gu4">URL</th></tr></thead><tbody></tbody></table>`, 1);
|
||||
var root$5 = /* @__PURE__ */ from_html(`<div class="entry-list"><!></div>`);
|
||||
function EntryList($$anchor, $$props) {
|
||||
@ -5988,6 +6031,13 @@ function EntryList($$anchor, $$props) {
|
||||
let loading = /* @__PURE__ */ state(true);
|
||||
let error = /* @__PURE__ */ state("");
|
||||
let resultCount = /* @__PURE__ */ state(0);
|
||||
let draggedEntryId = /* @__PURE__ */ state(null);
|
||||
function handleDragStart(entryId) {
|
||||
set(draggedEntryId, entryId, true);
|
||||
}
|
||||
function handleDragEnd() {
|
||||
set(draggedEntryId, null);
|
||||
}
|
||||
async function loadEntries() {
|
||||
set(loading, true);
|
||||
set(error, "");
|
||||
@ -6076,7 +6126,7 @@ function EntryList($$anchor, $$props) {
|
||||
each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => {
|
||||
var tr = root_7$2();
|
||||
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);
|
||||
@ -6092,11 +6142,18 @@ function EntryList($$anchor, $$props) {
|
||||
reset(td_2);
|
||||
reset(tr);
|
||||
template_effect(() => {
|
||||
set_class(tr, 1, `entry-row ${get(draggedEntryId) === get(entry).id ? "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) => {
|
||||
e.dataTransfer.setData("text/plain", get(entry).id);
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
handleDragStart(get(entry).id);
|
||||
});
|
||||
event("DragEnd", tr, handleDragEnd);
|
||||
append($$anchor, tr);
|
||||
});
|
||||
reset(tbody);
|
||||
@ -7261,6 +7318,17 @@ label {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.group-item.drop-target.svelte-181dlmc {
|
||||
background: rgba(108, 99, 255, 0.25);
|
||||
color: var(--color-primary);
|
||||
border: 1px dashed var(--color-primary);
|
||||
}
|
||||
|
||||
.group-wrapper.svelte-181dlmc {
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color 150ms;
|
||||
}
|
||||
|
||||
.group-icon.svelte-181dlmc {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@ -7310,6 +7378,27 @@ label {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.drop-indicator.svelte-181dlmc {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
animation: svelte-181dlmc-fadeIn 150ms ease;
|
||||
}
|
||||
|
||||
@keyframes svelte-181dlmc-fadeIn {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay.svelte-181dlmc {
|
||||
position: fixed;
|
||||
@ -7435,7 +7524,7 @@ label {
|
||||
}
|
||||
|
||||
.entry-row.svelte-13s7gu4 {
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
transition: background-color 150ms;
|
||||
}
|
||||
|
||||
@ -7443,12 +7532,32 @@ label {
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.entry-row.dragging.svelte-13s7gu4 {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.entry-row.svelte-13s7gu4:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.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);
|
||||
margin-right: 6px;
|
||||
user-select: none;
|
||||
opacity: 0.5;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.entry-row.svelte-13s7gu4:hover .drag-handle:where(.svelte-13s7gu4) {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.entry-title.svelte-13s7gu4 {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -9,6 +9,16 @@
|
||||
|
||||
let { onSelect, onAdd } = $props()
|
||||
|
||||
let draggedEntryId = $state(null)
|
||||
|
||||
function handleDragStart(entryId) {
|
||||
draggedEntryId = entryId
|
||||
}
|
||||
|
||||
function handleDragEnd() {
|
||||
draggedEntryId = null
|
||||
}
|
||||
|
||||
async function loadEntries() {
|
||||
loading = true
|
||||
error = ''
|
||||
@ -82,8 +92,19 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each entries as entry (entry.id)}
|
||||
<tr onclick={() => onSelect(entry.id)} class="entry-row">
|
||||
<tr
|
||||
onclick={() => onSelect(entry.id)}
|
||||
class="entry-row {draggedEntryId === entry.id ? 'dragging' : ''}"
|
||||
draggable="true"
|
||||
onDragStart={(e) => {
|
||||
e.dataTransfer.setData('text/plain', entry.id)
|
||||
e.dataTransfer.effectAllowed = 'move'
|
||||
handleDragStart(entry.id)
|
||||
}}
|
||||
onDragEnd={handleDragEnd}
|
||||
>
|
||||
<td>
|
||||
<span class="drag-handle" title="Drag to move to another group">⠿</span>
|
||||
<span class="entry-title">{entry.title}</span>
|
||||
</td>
|
||||
<td>
|
||||
@ -153,7 +174,7 @@
|
||||
}
|
||||
|
||||
.entry-row {
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
transition: background-color 150ms;
|
||||
}
|
||||
|
||||
@ -161,12 +182,32 @@
|
||||
background: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.entry-row.dragging {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.entry-row:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.entry-row td {
|
||||
padding: 10px 12px;
|
||||
font-size: 0.875rem;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.drag-handle {
|
||||
color: var(--color-text-muted);
|
||||
margin-right: 6px;
|
||||
user-select: none;
|
||||
opacity: 0.5;
|
||||
transition: opacity 150ms;
|
||||
}
|
||||
|
||||
.entry-row:hover .drag-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.entry-title {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script>
|
||||
import { getGroups, addGroup, updateGroup, deleteGroup, getEntryCountsByGroup } from '../lib/storage/db.js'
|
||||
import { getGroups, addGroup, updateGroup, deleteGroup, getEntryCountsByGroup, getEntryById, updateEntry as updateEntryDb } from '../lib/storage/db.js'
|
||||
import { createGroup, validateGroup } from '../lib/models/schema.js'
|
||||
import { search as searchStore } from '../lib/stores/search.svelte.js'
|
||||
|
||||
@ -14,6 +14,7 @@
|
||||
let groupError = $state('')
|
||||
let showDeleteGroupConfirm = $state(null) // groupId being confirmed for deletion
|
||||
let deletingGroup = $derived(groups.find(g => g.id === showDeleteGroupConfirm))
|
||||
let dropTargetGroupId = $state(null) // groupId currently being hovered during drag
|
||||
|
||||
const GROUP_COLORS = [
|
||||
'#6c63ff', '#e5484d', '#34d399', '#fbbf24', '#3b82f6',
|
||||
@ -83,6 +84,41 @@
|
||||
groupError = 'Failed to delete group: ' + e.message
|
||||
}
|
||||
}
|
||||
|
||||
// Drag-and-drop: move entry to a group
|
||||
function onDragOverGroup(e, groupId) {
|
||||
e.preventDefault()
|
||||
e.dataTransfer.dropEffect = 'move'
|
||||
dropTargetGroupId = groupId
|
||||
}
|
||||
|
||||
function onDragLeaveGroup() {
|
||||
dropTargetGroupId = null
|
||||
}
|
||||
|
||||
async function onDropGroup(groupId, e) {
|
||||
dropTargetGroupId = null
|
||||
const entryId = e.dataTransfer.getData('text/plain')
|
||||
if (!entryId) return
|
||||
|
||||
try {
|
||||
const entry = await getEntryById(entryId)
|
||||
if (!entry) return
|
||||
|
||||
// If dropping on "All Entries" (no group), clear the groupId
|
||||
const newGroupId = groupId === 'all' ? '' : groupId
|
||||
|
||||
const updated = {
|
||||
...entry,
|
||||
groupId: newGroupId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
}
|
||||
await updateEntryDb(updated)
|
||||
await loadData()
|
||||
} catch (err) {
|
||||
console.error('Failed to move entry:', err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="sidebar-content">
|
||||
@ -100,18 +136,30 @@
|
||||
</div>
|
||||
|
||||
<nav class="groups-nav">
|
||||
<button
|
||||
class="group-item {searchStore.activeGroupId === 'all' ? 'active' : ''}"
|
||||
onclick={() => searchStore.activeGroupId = 'all'}
|
||||
<div
|
||||
class="group-wrapper"
|
||||
onDragOver={(e) => onDragOverGroup(e, 'all')}
|
||||
onDragLeave={onDragLeaveGroup}
|
||||
onDrop={(e) => onDropGroup('all', e)}
|
||||
>
|
||||
<span class="group-icon">📋</span>
|
||||
<span class="group-name">All Entries</span>
|
||||
</button>
|
||||
<button
|
||||
class="group-item {searchStore.activeGroupId === 'all' ? 'active' : ''} {dropTargetGroupId === 'all' ? 'drop-target' : ''}"
|
||||
onclick={() => searchStore.activeGroupId = 'all'}
|
||||
>
|
||||
<span class="group-icon">📋</span>
|
||||
<span class="group-name">All Entries</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#each groups as group}
|
||||
<div class="group-row">
|
||||
<div
|
||||
class="group-row"
|
||||
onDragOver={(e) => onDragOverGroup(e, group.id)}
|
||||
onDragLeave={onDragLeaveGroup}
|
||||
onDrop={(e) => onDropGroup(group.id, e)}
|
||||
>
|
||||
<button
|
||||
class="group-item {searchStore.activeGroupId === group.id ? 'active' : ''}"
|
||||
class="group-item {searchStore.activeGroupId === group.id ? 'active' : ''} {dropTargetGroupId === group.id ? 'drop-target' : ''}"
|
||||
onclick={() => searchStore.activeGroupId = group.id}
|
||||
>
|
||||
<span class="group-color" style="background-color: {group.color || '#6c63ff'}"></span>
|
||||
@ -169,6 +217,11 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Entry moved toast -->
|
||||
{#if dropTargetGroupId}
|
||||
<div class="drop-indicator">Drop here to move</div>
|
||||
{/if}
|
||||
|
||||
<!-- Delete group confirmation -->
|
||||
{#if deletingGroup}
|
||||
<div class="modal-overlay" role="presentation" onclick={() => showDeleteGroupConfirm = null}>
|
||||
@ -249,6 +302,17 @@
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.group-item.drop-target {
|
||||
background: rgba(108, 99, 255, 0.25);
|
||||
color: var(--color-primary);
|
||||
border: 1px dashed var(--color-primary);
|
||||
}
|
||||
|
||||
.group-wrapper {
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color 150ms;
|
||||
}
|
||||
|
||||
.group-icon {
|
||||
font-size: 1rem;
|
||||
}
|
||||
@ -298,6 +362,27 @@
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.drop-indicator {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 8px 16px;
|
||||
background: var(--color-primary);
|
||||
color: #fff;
|
||||
border-radius: var(--radius-md);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
z-index: 100;
|
||||
pointer-events: none;
|
||||
animation: fadeIn 150ms ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateX(-50%) translateY(10px); }
|
||||
to { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
}
|
||||
|
||||
/* Modal */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user