Fix card-enter animation re-trigger and drag hover suppression

Remove card-enter class after entrance animation completes to prevent
re-triggering when card-highlight is removed. Change fill-mode from
both to backwards so stale transforms don't block hover effects.
Suppress hover globally during drag via body.cs-drag-active class.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 01:10:09 +03:00
parent 9194b978e0
commit f6977105b8
2 changed files with 48 additions and 25 deletions

View File

@@ -73,7 +73,7 @@ section {
} }
.card-enter { .card-enter {
animation: cardEnter 0.25s ease-out both; animation: cardEnter 0.25s ease-out backwards;
} }
/* ── Card drag-and-drop reordering ── */ /* ── Card drag-and-drop reordering ── */
@@ -135,14 +135,22 @@ section {
transition: none; transition: none;
} }
/* Suppress hover effects during drag */ /* Suppress hover effects globally during drag */
.cs-dragging .card, body.cs-drag-active .card,
.cs-dragging .template-card { body.cs-drag-active .template-card,
body.cs-drag-active .add-template-card {
transition: none !important; transition: none !important;
transform: none !important; transform: none !important;
} }
.cs-dragging .card-drag-handle { body.cs-drag-active .add-template-card {
pointer-events: none;
border-color: var(--border-color) !important;
background: transparent !important;
box-shadow: none !important;
}
body.cs-drag-active .card-drag-handle {
opacity: 0 !important; opacity: 0 !important;
} }

View File

@@ -178,6 +178,12 @@ export class CardSection {
* @returns {{added: Set<string>, replaced: Set<string>, removed: Set<string>}} * @returns {{added: Set<string>, replaced: Set<string>, removed: Set<string>}}
*/ */
reconcile(items) { reconcile(items) {
// Skip DOM mutations while a drag is in progress — would destroy drag state
if (this._dragState) {
this._pendingReconcile = items;
return { added: new Set(), replaced: new Set(), removed: new Set() };
}
const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`); const content = document.querySelector(`[data-cs-content="${this.sectionKey}"]`);
if (!content) return { added: new Set(), replaced: new Set(), removed: new Set() }; if (!content) return { added: new Set(), replaced: new Set(), removed: new Set() };
@@ -240,6 +246,7 @@ export class CardSection {
if (card) { if (card) {
card.style.animationDelay = `${delay}ms`; card.style.animationDelay = `${delay}ms`;
card.classList.add('card-enter'); card.classList.add('card-enter');
card.addEventListener('animationend', () => card.classList.remove('card-enter'), { once: true });
delay += 30; delay += 30;
} }
} }
@@ -347,6 +354,7 @@ export class CardSection {
cards.forEach((card, i) => { cards.forEach((card, i) => {
card.style.animationDelay = `${i * 30}ms`; card.style.animationDelay = `${i * 30}ms`;
card.classList.add('card-enter'); card.classList.add('card-enter');
card.addEventListener('animationend', () => card.classList.remove('card-enter'), { once: true });
}); });
} }
@@ -502,20 +510,17 @@ export class CardSection {
ds.clone.style.left = (e.clientX - ds.offsetX) + 'px'; ds.clone.style.left = (e.clientX - ds.offsetX) + 'px';
ds.clone.style.top = (e.clientY - ds.offsetY) + 'px'; ds.clone.style.top = (e.clientY - ds.offsetY) + 'px';
// Find drop target // Only move placeholder when cursor enters a card's rect
const target = this._getDropTarget(e.clientX, e.clientY, ds.content); const { card: target, before } = this._getDropTarget(e.clientX, e.clientY, ds.content);
if (target && target !== ds.placeholder) { if (!target) return; // cursor is in a gap — keep placeholder where it is
const rect = target.getBoundingClientRect(); if (target === ds.lastTarget && before === ds.lastBefore) return; // same position
const midX = rect.left + rect.width / 2; ds.lastTarget = target;
const midY = rect.top + rect.height / 2; ds.lastBefore = before;
// For grid: use combined horizontal+vertical check if (before) {
const insertBefore = (e.clientY < midY) || (e.clientY < midY + rect.height * 0.3 && e.clientX < midX);
if (insertBefore) {
ds.content.insertBefore(ds.placeholder, target); ds.content.insertBefore(ds.placeholder, target);
} else { } else {
ds.content.insertBefore(ds.placeholder, target.nextSibling); ds.content.insertBefore(ds.placeholder, target.nextSibling);
} }
}
// Auto-scroll near viewport edges // Auto-scroll near viewport edges
this._autoScroll(e.clientY, ds); this._autoScroll(e.clientY, ds);
@@ -548,6 +553,7 @@ export class CardSection {
// Hide original // Hide original
ds.card.style.display = 'none'; ds.card.style.display = 'none';
ds.content.classList.add('cs-dragging'); ds.content.classList.add('cs-dragging');
document.body.classList.add('cs-drag-active');
} }
_onDragEnd() { _onDragEnd() {
@@ -564,21 +570,30 @@ export class CardSection {
ds.placeholder.remove(); ds.placeholder.remove();
ds.clone.remove(); ds.clone.remove();
ds.content.classList.remove('cs-dragging'); ds.content.classList.remove('cs-dragging');
document.body.classList.remove('cs-drag-active');
// Save new order from DOM // Save new order from DOM
const keys = this._readDomOrder(ds.content); const keys = this._readDomOrder(ds.content);
this._saveOrder(keys); this._saveOrder(keys);
// Flush any reconcile that was deferred during drag
if (this._pendingReconcile) {
const items = this._pendingReconcile;
this._pendingReconcile = null;
this.reconcile(items);
}
} }
_getDropTarget(x, y, content) { _getDropTarget(x, y, content) {
// Temporarily show all cards for hit testing const cards = content.querySelectorAll(`[${this.keyAttr}]`);
const els = document.elementsFromPoint(x, y); for (const card of cards) {
for (const el of els) { if (card.style.display === 'none') continue;
if (el === content) break; const r = card.getBoundingClientRect();
const card = el.closest(`[${this.keyAttr}]`); if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
if (card && card.style.display !== 'none' && content.contains(card)) return card; return { card, before: x < r.left + r.width / 2 };
} }
return null; }
return { card: null, before: false };
} }
_readDomOrder(content) { _readDomOrder(content) {