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 {
animation: cardEnter 0.25s ease-out both;
animation: cardEnter 0.25s ease-out backwards;
}
/* ── Card drag-and-drop reordering ── */
@@ -135,14 +135,22 @@ section {
transition: none;
}
/* Suppress hover effects during drag */
.cs-dragging .card,
.cs-dragging .template-card {
/* Suppress hover effects globally during drag */
body.cs-drag-active .card,
body.cs-drag-active .template-card,
body.cs-drag-active .add-template-card {
transition: 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;
}

View File

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