Add dragdrop grouping

This commit is contained in:
Timothy Farrell 2026-05-16 00:25:49 +00:00
parent a6589fb1f3
commit 7d9d3aef0e
4 changed files with 221 additions and 14 deletions

128
dist/index.html vendored
View File

@ -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)); 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<void>}
*/
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. * Count entries per group.
* @returns {Promise<Map<string, number>>} * @returns {Promise<Map<string, number>>}
*/ */
@ -5818,12 +5832,12 @@ var SearchStore = class {
var search = new SearchStore(); var search = new SearchStore();
//#endregion //#endregion
//#region src/components/Sidebar.svelte //#region src/components/Sidebar.svelte
var root_1$7 = /* @__PURE__ */ from_html(`<div class="group-row svelte-181dlmc"><button><span class="group-color svelte-181dlmc"></span> <span class="group-name svelte-181dlmc"> </span></button> <div class="group-actions svelte-181dlmc"><button class="group-action-btn svelte-181dlmc" title="Edit group">✏️</button> <button class="group-action-btn svelte-181dlmc" title="Delete group">🗑</button></div></div>`); var root_1$7 = /* @__PURE__ */ from_html(`<div class="group-row svelte-181dlmc"><button><span class="group-color svelte-181dlmc"></span> <span class="group-name svelte-181dlmc"> </span> <span class="drop-icon svelte-181dlmc">📥</span></button> <div class="group-actions svelte-181dlmc"><button class="group-action-btn svelte-181dlmc" title="Edit group">✏️</button> <button class="group-action-btn svelte-181dlmc" title="Delete group">🗑</button></div></div>`);
var root_3$5 = /* @__PURE__ */ from_html(`<div class="error-banner svelte-181dlmc"> </div>`); 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_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_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 svelte-181dlmc"><label for="group-name" class="svelte-181dlmc">Group Name</label> <input id="group-name" type="text" placeholder="e.g. Work, Personal" class="svelte-181dlmc"/></div> <div class="form-group svelte-181dlmc"><span class="field-label svelte-181dlmc">Color</span> <div class="color-picker svelte-181dlmc"></div></div> <div class="modal-actions svelte-181dlmc"><button class="btn btn-primary svelte-181dlmc"> </button> <button class="btn btn-ghost svelte-181dlmc">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_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 class="svelte-181dlmc"> </strong>"? Entries in this group will become ungrouped.</p> <div class="modal-actions svelte-181dlmc"><button class="btn btn-danger svelte-181dlmc">Yes, delete</button> <button class="btn btn-ghost svelte-181dlmc">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$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 svelte-181dlmc">+ New Group</button></div> <!> <!></div>`);
function Sidebar($$anchor, $$props) { function Sidebar($$anchor, $$props) {
push($$props, true); push($$props, true);
let groups = /* @__PURE__ */ state(proxy([])); let groups = /* @__PURE__ */ state(proxy([]));
@ -5835,6 +5849,22 @@ function Sidebar($$anchor, $$props) {
let groupError = /* @__PURE__ */ state(""); let groupError = /* @__PURE__ */ state("");
let showDeleteGroupConfirm = /* @__PURE__ */ state(null); let showDeleteGroupConfirm = /* @__PURE__ */ state(null);
let deletingGroup = /* @__PURE__ */ user_derived(() => get(groups).find((g) => g.id === get(showDeleteGroupConfirm))); 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 = [ const GROUP_COLORS = [
"#6c63ff", "#6c63ff",
"#e5484d", "#e5484d",
@ -5918,6 +5948,7 @@ function Sidebar($$anchor, $$props) {
var span_1 = sibling(span, 2); var span_1 = sibling(span, 2);
var text = child(span_1, true); var text = child(span_1, true);
reset(span_1); reset(span_1);
next(2);
reset(button_1); reset(button_1);
var div_3 = sibling(button_1, 2); var div_3 = sibling(button_1, 2);
var button_2 = child(div_3); var button_2 = child(div_3);
@ -5925,11 +5956,29 @@ function Sidebar($$anchor, $$props) {
reset(div_3); reset(div_3);
reset(div_2); reset(div_2);
template_effect(() => { 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_style(span, `background-color: ${(get(group).color || "#6c63ff") ?? ""}`);
set_text(text, get(group).name); set_text(text, get(group).name);
}); });
delegated("click", button_1, () => search.activeGroupId = get(group).id); 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_2, () => openGroupForm(get(group)));
delegated("click", button_3, () => set(showDeleteGroupConfirm, get(group).id, true)); delegated("click", button_3, () => set(showDeleteGroupConfirm, get(group).id, true));
append($$anchor, div_2); append($$anchor, div_2);
@ -6042,7 +6091,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_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_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_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><td class="svelte-13s7gu4"><span class="drag-handle svelte-13s7gu4" aria-hidden="true"></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$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>`); var root$5 = /* @__PURE__ */ from_html(`<div class="entry-list"><!></div>`);
function EntryList($$anchor, $$props) { function EntryList($$anchor, $$props) {
@ -6051,6 +6100,7 @@ function EntryList($$anchor, $$props) {
let loading = /* @__PURE__ */ state(true); let loading = /* @__PURE__ */ state(true);
let error = /* @__PURE__ */ state(""); let error = /* @__PURE__ */ state("");
let resultCount = /* @__PURE__ */ state(0); let resultCount = /* @__PURE__ */ state(0);
let dragging = /* @__PURE__ */ state(false);
async function loadEntries() { async function loadEntries() {
set(loading, true); set(loading, true);
set(error, ""); set(error, "");
@ -6138,8 +6188,9 @@ function EntryList($$anchor, $$props) {
var tbody = sibling(child(table)); var tbody = sibling(child(table));
each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => { each(tbody, 21, () => get(entries), (entry) => entry.id, ($$anchor, entry) => {
var tr = root_7$2(); var tr = root_7$2();
set_attribute(tr, "draggable", true);
var td = child(tr); var td = child(tr);
var span_1 = child(td); var span_1 = sibling(child(td), 2);
var text_6 = child(span_1, true); var text_6 = child(span_1, true);
reset(span_1); reset(span_1);
reset(td); reset(td);
@ -6155,11 +6206,20 @@ function EntryList($$anchor, $$props) {
reset(td_2); reset(td_2);
reset(tr); reset(tr);
template_effect(() => { template_effect(() => {
set_class(tr, 1, `entry-row ${get(dragging) ? "dragging" : ""}`, "svelte-13s7gu4");
set_text(text_6, get(entry).title); set_text(text_6, get(entry).title);
set_text(text_7, get(entry).username || "—"); set_text(text_7, get(entry).username || "—");
set_text(text_8, get(entry).url || "—"); set_text(text_8, get(entry).url || "—");
}); });
delegated("click", tr, () => $$props.onSelect(get(entry).id)); 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); append($$anchor, tr);
}); });
reset(tbody); reset(tbody);
@ -7330,6 +7390,35 @@ label {
color: var(--color-primary); 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 { .group-icon.svelte-181dlmc {
font-size: 1rem; font-size: 1rem;
} }
@ -7504,20 +7593,41 @@ label {
} }
.entry-row.svelte-13s7gu4 { .entry-row.svelte-13s7gu4 {
cursor: pointer; cursor: grab;
transition: background-color 150ms; transition: background-color 150ms, opacity 150ms;
}
.entry-row.svelte-13s7gu4:active {
cursor: grabbing;
} }
.entry-row.svelte-13s7gu4:hover { .entry-row.svelte-13s7gu4:hover {
background: var(--color-surface-hover); background: var(--color-surface-hover);
} }
.entry-row.dragging.svelte-13s7gu4 {
opacity: 0.35;
}
.entry-row.svelte-13s7gu4 td:where(.svelte-13s7gu4) { .entry-row.svelte-13s7gu4 td:where(.svelte-13s7gu4) {
padding: 10px 12px; padding: 10px 12px;
font-size: 0.875rem; font-size: 0.875rem;
border-bottom: 1px solid var(--color-border); 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 { .entry-title.svelte-13s7gu4 {
font-weight: 500; font-weight: 500;
} }

View File

@ -6,6 +6,7 @@
let loading = $state(true) let loading = $state(true)
let error = $state('') let error = $state('')
let resultCount = $state(0) let resultCount = $state(0)
let dragging = $state(false)
let { onSelect, onAdd } = $props() let { onSelect, onAdd } = $props()
@ -82,8 +83,15 @@
</thead> </thead>
<tbody> <tbody>
{#each entries as entry (entry.id)} {#each entries as entry (entry.id)}
<tr onclick={() => onSelect(entry.id)} class="entry-row"> <tr
draggable={true}
onclick={() => 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' : ''}"
>
<td> <td>
<span class="drag-handle" aria-hidden="true"></span>
<span class="entry-title">{entry.title}</span> <span class="entry-title">{entry.title}</span>
</td> </td>
<td> <td>
@ -153,20 +161,41 @@
} }
.entry-row { .entry-row {
cursor: pointer; cursor: grab;
transition: background-color 150ms; transition: background-color 150ms, opacity 150ms;
}
.entry-row:active {
cursor: grabbing;
} }
.entry-row:hover { .entry-row:hover {
background: var(--color-surface-hover); background: var(--color-surface-hover);
} }
.entry-row.dragging {
opacity: 0.35;
}
.entry-row td { .entry-row td {
padding: 10px 12px; padding: 10px 12px;
font-size: 0.875rem; font-size: 0.875rem;
border-bottom: 1px solid var(--color-border); 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 { .entry-title {
font-weight: 500; font-weight: 500;
} }

View File

@ -1,5 +1,5 @@
<script> <script>
import { getGroups, addGroup, updateGroup, deleteGroup, getEntryCountsByGroup } from '../lib/storage/db.js' import { getGroups, addGroup, updateGroup, deleteGroup, getEntryCountsByGroup, moveEntryToGroup } from '../lib/storage/db.js'
import { createGroup, validateGroup } from '../lib/models/schema.js' import { createGroup, validateGroup } from '../lib/models/schema.js'
import { search as searchStore } from '../lib/stores/search.svelte.js' import { search as searchStore } from '../lib/stores/search.svelte.js'
import { autofocus } from '../lib/autofocus.js' import { autofocus } from '../lib/autofocus.js'
@ -16,6 +16,26 @@
let showDeleteGroupConfirm = $state(null) // groupId being confirmed for deletion let showDeleteGroupConfirm = $state(null) // groupId being confirmed for deletion
let deletingGroup = $derived(groups.find(g => g.id === showDeleteGroupConfirm)) let deletingGroup = $derived(groups.find(g => g.id === showDeleteGroupConfirm))
// Drag-and-drop state
let dragOverGroupId = $state(null)
let droppedGroupId = $state(null)
async function handleDrop(groupId, entryId) {
try {
await moveEntryToGroup(entryId, groupId)
droppedGroupId = groupId
setTimeout(() => { droppedGroupId = null }, 600)
await loadData()
searchStore.refresh()
} catch (e) {
// silent fail
}
}
function canDrop(groupId) {
return groupId !== searchStore.activeGroupId
}
const GROUP_COLORS = [ const GROUP_COLORS = [
'#6c63ff', '#e5484d', '#34d399', '#fbbf24', '#3b82f6', '#6c63ff', '#e5484d', '#34d399', '#fbbf24', '#3b82f6',
'#ec4899', '#8b5cf6', '#14b8a6', '#f97316', '#06b6d4', '#ec4899', '#8b5cf6', '#14b8a6', '#f97316', '#06b6d4',
@ -112,11 +132,15 @@
{#each groups as group} {#each groups as group}
<div class="group-row"> <div class="group-row">
<button <button
class="group-item {searchStore.activeGroupId === group.id ? 'active' : ''}" class="group-item {searchStore.activeGroupId === group.id ? 'active' : ''} {dragOverGroupId === group.id ? 'drag-over' : ''} {droppedGroupId === group.id ? 'dropped' : ''}"
onclick={() => searchStore.activeGroupId = group.id} onclick={() => searchStore.activeGroupId = group.id}
ondragover={(e) => { if (canDrop(group.id)) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; dragOverGroupId = group.id; } }}
ondragleave={() => { if (dragOverGroupId === group.id) dragOverGroupId = null; }}
ondrop={(e) => { e.preventDefault(); dragOverGroupId = null; if (canDrop(group.id)) { const entryId = e.dataTransfer.getData('text/plain'); if (entryId) handleDrop(group.id, entryId); } }}
> >
<span class="group-color" style="background-color: {group.color || '#6c63ff'}"></span> <span class="group-color" style="background-color: {group.color || '#6c63ff'}"></span>
<span class="group-name">{group.name}</span> <span class="group-name">{group.name}</span>
<span class="drop-icon">📥</span>
</button> </button>
<div class="group-actions"> <div class="group-actions">
<button class="group-action-btn" onclick={() => openGroupForm(group)} title="Edit group">✏️</button> <button class="group-action-btn" onclick={() => openGroupForm(group)} title="Edit group">✏️</button>
@ -250,6 +274,35 @@
color: var(--color-primary); color: var(--color-primary);
} }
.group-item.drag-over {
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 .drop-icon {
opacity: 1;
transform: scale(1.2);
}
.group-item.dropped {
animation: dropFlash 600ms ease;
}
@keyframes dropFlash {
0% { background: rgba(52, 211, 153, 0.35); }
100% { background: transparent; }
}
.drop-icon {
opacity: 0;
font-size: 0.85rem;
transition: opacity 150ms, transform 150ms;
flex-shrink: 0;
}
.group-icon { .group-icon {
font-size: 1rem; font-size: 1rem;
} }

View File

@ -261,6 +261,21 @@ export async function searchEntries(query, options = {}) {
) )
} }
/**
* Move an entry to a different group (or ungroup it by passing empty string).
* @param {string} entryId
* @param {string} groupId
* @returns {Promise<void>}
*/
export 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 = new Date().toISOString()
await db.put('entries', entry)
}
/** /**
* Count entries per group. * Count entries per group.
* @returns {Promise<Map<string, number>>} * @returns {Promise<Map<string, number>>}