diff --git a/config.php b/config.php index 3dbe3b6..f141c59 100644 --- a/config.php +++ b/config.php @@ -1,7 +1,7 @@ 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 diff --git a/public/js/modules/buckets.js b/public/js/modules/buckets.js index 4026c6b..d5e662b 100644 --- a/public/js/modules/buckets.js +++ b/public/js/modules/buckets.js @@ -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 } }))); diff --git a/public/js/modules/context-menu.js b/public/js/modules/context-menu.js index 0663b84..167de9f 100644 --- a/public/js/modules/context-menu.js +++ b/public/js/modules/context-menu.js @@ -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(); })); }