v2.0.14 — ghost + spinner on card move
Moving a card to another bucket took a visible round-trip (PATCH + reloadAll) during which the card sat unchanged in the source bucket, leaving the user wondering if the drop registered. Now every bucket-move path marks the affected cards `.card-moving` as soon as the action fires: 50% opacity, dark scrim and a centred accent-ring spinner. The next renderBoard() rebuilds the DOM so the class comes off automatically — no explicit cleanup needed. Call sites: - drag-drop onto a bucket tile (single + multi-select) - drag onto an existing bucket to nest it (auto-super-bucket create) - context-menu → Bucket → <target> Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8cbec8d601
commit
4baeee159e
6 files changed
+55
-3
No files matched your search
@@ -50,6 +50,20 @@ export function animateCardRemoval(ids) {
|
||||
return new Promise(r => setTimeout(r, 260));
|
||||
}
|
||||
|
||||
/** Ghost-and-spinner overlay for cards that are being moved between
|
||||
* buckets. The PATCH + reloadAll round-trip used to leave the original
|
||||
* tile sitting stale in the current bucket for a couple hundred ms;
|
||||
* marking them gives immediate feedback. The class comes off naturally
|
||||
* on the next renderBoard() (the DOM is rebuilt from fresh state). */
|
||||
export function markCardsMoving(ids) {
|
||||
if (!ids || !ids.length) return;
|
||||
for (const id of ids) {
|
||||
const el = board.querySelector(`[data-id="${id}"]`);
|
||||
if (el) el.classList.add('card-moving');
|
||||
}
|
||||
}
|
||||
window._markCardsMoving = markCardsMoving;
|
||||
|
||||
/** Add a pulse class to matching board cards and strip it after the
|
||||
* animation. `cls` controls the variant: 'card-pulse-new' (3×, for
|
||||
* relay-synced cards) or 'card-pulse-local' (1×, for cards the user
|
||||
|
||||
@@ -627,6 +627,7 @@ function renderBar() {
|
||||
const targetBid = isSuperTile ? (t.dataset.superShown || bid) : bid;
|
||||
const bucketId = targetBid === 'none' ? null : (['all','current','journal'].includes(targetBid) ? undefined : targetBid);
|
||||
if (bucketId === undefined) return;
|
||||
window._markCardsMoving?.(ids);
|
||||
await Promise.all(ids.map(id => api(`api/cards/${id}`, { method: 'PATCH', body: { bucket_id: bucketId, parent_id: null } })));
|
||||
window._clearSelection?.();
|
||||
await window._reloadAll();
|
||||
@@ -672,6 +673,7 @@ function renderBar() {
|
||||
const selected = window._getSelectedIds?.() || [];
|
||||
const ids = selected.includes(String(cardId)) ? selected : [String(cardId)];
|
||||
const card = state.cards.find(c => String(c.id) === String(cardId));
|
||||
window._markCardsMoving?.(ids);
|
||||
const created = await api('api/buckets', { method: 'POST', body: { name: autoBucketNameFromCard(card) } });
|
||||
if (!created?.id) return;
|
||||
await Promise.all(ids.map(id => api(`api/cards/${id}`, { method: 'PATCH', body: { bucket_id: created.id, parent_id: null } })));
|
||||
|
||||
@@ -416,7 +416,13 @@ export function showContextMenu(x, y, card) {
|
||||
}));
|
||||
|
||||
grid.querySelectorAll('[data-ctx-color]').forEach(btn => btn.addEventListener('click', async e => { e.stopPropagation(); hideContextMenu(); pushCardEdit('color', card.id, { color: card.color }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { color: btn.dataset.ctxColor } }); await window._reloadAll(); }));
|
||||
buckets.querySelectorAll('[data-ctx-bucket]').forEach(btn => btn.addEventListener('click', async e => { e.stopPropagation(); hideContextMenu(); pushCardEdit('bucket', card.id, { bucket_id: card.bucket_id }); await api(`api/cards/${card.id}`, { method: 'PATCH', body: { bucket_id: btn.dataset.ctxBucket || null } }); await window._reloadAll(); }));
|
||||
buckets.querySelectorAll('[data-ctx-bucket]').forEach(btn => btn.addEventListener('click', async e => {
|
||||
e.stopPropagation(); hideContextMenu();
|
||||
pushCardEdit('bucket', card.id, { bucket_id: card.bucket_id });
|
||||
window._markCardsMoving?.([card.id]);
|
||||
await api(`api/cards/${card.id}`, { method: 'PATCH', body: { bucket_id: btn.dataset.ctxBucket || null } });
|
||||
await window._reloadAll();
|
||||
}));
|
||||
members.querySelectorAll('[data-ctx-member]').forEach(btn => btn.addEventListener('click', e => { e.stopPropagation(); if (e.target.closest('[data-ctx-kick]')) return; hideContextMenu(); window._openDetail(btn.dataset.ctxMember); }));
|
||||
members.querySelectorAll('[data-ctx-kick]').forEach(btn => btn.addEventListener('click', async e => { e.stopPropagation(); hideContextMenu(); await api(`api/cards/${btn.dataset.ctxKick}`, { method: 'PATCH', body: { parent_id: null } }); await window._reloadAll(); }));
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user