Server web UI: make photo-card dragging actually feel like dragging
Build and push server image / build-and-push (push) Successful in 34s
Build and push server image / build-and-push (push) Successful in 34s
The card being dragged never moved -- it just faded in place while a static outline highlighted whatever was underneath the finger. Now the card tracks the pointer 1:1 (translate + a slight scale-up "lift"), gets a stronger shadow while airborne, and leaves its grid slot looking like an empty gap until dropped (transform doesn't remove it from layout flow, so the reserved space stays put -- no reflow needed until drop). pointer-events:none while dragging so elementFromPoint's drop-target hit-test sees through to the card underneath instead of hitting the translated one. Also: a short vibration tick when the touch hold-to-arm fires and another on a successful drop (Chrome/Android only, iOS Safari has no Vibration API -- harmless no-op there), and trimmed the arm delay from 350ms to 250ms now that there's actual feedback confirming the hold registered.
This commit is contained in:
@@ -261,8 +261,20 @@
|
||||
}
|
||||
.photo-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-1px); }
|
||||
.photo-card:active { cursor: grabbing; }
|
||||
.photo-card.dragging { opacity: 0.35; }
|
||||
.photo-card.drag-armed { box-shadow: 0 0 0 3px var(--focus-ring) inset; }
|
||||
.photo-card.drag-armed {
|
||||
box-shadow: 0 0 0 3px var(--focus-ring) inset;
|
||||
transform: scale(0.97);
|
||||
}
|
||||
/* No transform transition here -- the JS drives transform on every
|
||||
pointermove to track the finger 1:1, and the .15s base transition
|
||||
would otherwise make it visibly lag behind a fast swipe. */
|
||||
.photo-card.dragging {
|
||||
z-index: 20;
|
||||
box-shadow: 0 16px 32px rgba(0, 0, 0, 0.35);
|
||||
transition: box-shadow .15s ease;
|
||||
pointer-events: none; /* so elementFromPoint hits the card underneath, not this one */
|
||||
cursor: grabbing;
|
||||
}
|
||||
.photo-card.drag-over { outline: 3px solid var(--accent); outline-offset: -3px; }
|
||||
.photo-card img { width: 100%; height: 100%; object-fit: cover; display: block; pointer-events: none; }
|
||||
.photo-card .badge { position: absolute; top: 6px; left: 6px; background: var(--overlay); color: white; font-size: 10px; padding: 2px 6px; border-radius: 4px; }
|
||||
|
||||
@@ -207,9 +207,16 @@
|
||||
// hold entirely (no scroll-vs-drag ambiguity with a mouse).
|
||||
let dragState = null; // { pointerId, fromIndex, toIndex, moved, armed, holdTimer }
|
||||
const DRAG_START_THRESHOLD_PX = 6; // ignore tiny jitter once armed, before committing to a drag
|
||||
const DRAG_HOLD_MS = 350;
|
||||
const DRAG_HOLD_MS = 250;
|
||||
const HOLD_CANCEL_THRESHOLD_PX = 10; // movement before the hold timer fires -> treat as a scroll, not a drag
|
||||
|
||||
function vibrate(ms) {
|
||||
// Chrome/Android only -- iOS Safari has no Vibration API. Wrapped so
|
||||
// it's a harmless no-op everywhere else rather than needing a
|
||||
// feature check at every call site.
|
||||
try { navigator.vibrate?.(ms); } catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
function clearDragOverStyling() {
|
||||
document.querySelectorAll('.photo-card.drag-over').forEach((el) => el.classList.remove('drag-over'));
|
||||
}
|
||||
@@ -219,9 +226,11 @@
|
||||
clearTimeout(dragState.holdTimer);
|
||||
}
|
||||
card.style.touchAction = '';
|
||||
card.style.transform = '';
|
||||
card.classList.remove('dragging', 'drag-armed');
|
||||
clearDragOverStyling();
|
||||
if (dragState && dragState.moved && dragState.toIndex !== undefined && dragState.toIndex !== dragState.fromIndex) {
|
||||
vibrate(15);
|
||||
moveItem(dragState.fromIndex, dragState.toIndex);
|
||||
}
|
||||
dragState = null;
|
||||
@@ -287,6 +296,7 @@
|
||||
dragState.armed = true;
|
||||
card.classList.add('drag-armed');
|
||||
card.style.touchAction = 'none';
|
||||
vibrate(10);
|
||||
try { card.setPointerCapture(e.pointerId); } catch (err) { /* pointer may have already left */ }
|
||||
}
|
||||
}, DRAG_HOLD_MS);
|
||||
@@ -313,9 +323,10 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const dx = e.clientX - dragState.startX;
|
||||
const dy = e.clientY - dragState.startY;
|
||||
|
||||
if (!dragState.moved) {
|
||||
const dx = e.clientX - dragState.startX;
|
||||
const dy = e.clientY - dragState.startY;
|
||||
if (Math.hypot(dx, dy) < DRAG_START_THRESHOLD_PX) {
|
||||
return;
|
||||
}
|
||||
@@ -323,6 +334,12 @@
|
||||
card.classList.remove('drag-armed');
|
||||
card.classList.add('dragging');
|
||||
}
|
||||
|
||||
// Follows the finger 1:1 -- the actual "pick it up and carry it"
|
||||
// feedback that was missing before (the card used to just fade
|
||||
// in place while a static outline highlighted the drop target).
|
||||
card.style.transform = `translate(${dx}px, ${dy}px) scale(1.06)`;
|
||||
|
||||
const overCard = document.elementFromPoint(e.clientX, e.clientY)?.closest('.photo-card');
|
||||
clearDragOverStyling();
|
||||
if (overCard && overCard !== card) {
|
||||
|
||||
Reference in New Issue
Block a user