aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-09-03 21:27:29 -0700
committerAdam Soltys <[email protected]>2026-09-03 21:27:29 -0700
commitebdb209ff4480ce88ee764c36b28b1b7d5890eed (patch)
tree0dbca7006f30da27c8015e938e129382f9a1f41c
parent3b65462549269e3895345919b51164e017bcd13d (diff)
downloadcoinosv3-ebdb209ff4480ce88ee764c36b28b1b7d5890eed.tar.gz
coinosv3-ebdb209ff4480ce88ee764c36b28b1b7d5890eed.zip
The carousel stops snapping back mid-throw: on release the mandatory snap-type was restored on a fixed 400ms timer, which during rapid back-and-forth fired while the next glide was only partway — re-applying mandatory snap then makes the browser snap to the nearest point, the card you started from, yanking it back. Snap now restores on the scrollend of that specific glide, guarded by a per-drag token so a stale restore from an earlier throw can't fire during a later one (timer kept only as a fallback for the no-scroll case)
Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr
-rw-r--r--src/app.js17
1 files changed, 16 insertions, 1 deletions
diff --git a/src/app.js b/src/app.js
index 2173b68..4e4ab32 100644
--- a/src/app.js
+++ b/src/app.js
@@ -3446,6 +3446,12 @@ function balanceCard() {
if (e.target.closest && e.target.closest('button, a, input')) return;
e.preventDefault(); // no text selection while pulling
const el = e.currentTarget;
+ // A new grab invalidates any pending snap-restore from the previous
+ // release: bumping the token makes a stale restore a no-op, so it can
+ // never re-apply mandatory snap mid-glide and yank the card back to
+ // where it started. Kill its timer too so they don't pile up.
+ el._dragGen = (el._dragGen || 0) + 1;
+ clearTimeout(el._snapT);
const x0 = e.clientX, s0 = el.scrollLeft;
const kids0 = [...el.children];
// the card the drag started on — a flick throws relative to it
@@ -3493,7 +3499,16 @@ function balanceCard() {
if (Math.abs(vel) > 0.25) best = Math.max(0, Math.min(kids.length - 1, fromIdx + (vel < 0 ? 1 : -1)));
const k = kids[best];
if (k) el.scrollTo({ left: k.offsetLeft + k.offsetWidth / 2 - el.clientWidth / 2, behavior: 'smooth' });
- setTimeout(() => { el.style.scrollSnapType = ''; }, 400);
+ // Restore mandatory snap only once THIS glide has come to rest, and
+ // only if no newer drag has taken over — a fixed timer used to fire
+ // mid-glide and snap the card back to its origin. scrollend marks
+ // the true end; the timer is just a fallback for when the target is
+ // already reached (no scroll, no scrollend).
+ const gen = el._dragGen;
+ const restore = () => { if (el._dragGen === gen && !el._dragging) el.style.scrollSnapType = ''; };
+ el.addEventListener('scrollend', restore, { once: true });
+ clearTimeout(el._snapT);
+ el._snapT = setTimeout(restore, 700);
// the glide's own scroll events reschedule this; a release already
// at rest on the target needs the switch to fire regardless
clearTimeout(el._settle);