From c377f1bbc452da5019eff1e32542219e42fe1f42 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Fri, 11 Sep 2026 23:16:49 -0700 Subject: A tap on the balance card waited 700ms to show anything. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tapping the unit tag on a phone took the best part of a second to change the number. Measured on the device: 824ms, three taps running. The same tap on a laptop was 30ms. The balance carousel freezes itself while a finger is down — renders are held and the strip is marked _skipMorph, so a background repaint can't rebuild it mid-drag and yank the scroll. Touch-end lifts the render hold but leaves the strip frozen until a scroll settles... and a TAP produces no scroll events at all. So the freeze stood until the 700ms safety net fired, and every render in between was refused by the morph — including the one the tap had just asked for. The mouse path has always had this guard (it only freezes once the pointer has moved four pixels, and unfreezes immediately on a release that never moved), which is why this never showed up on a laptop. The touch path now notices the same thing the only way it can: the strip's scroll position is recorded when the finger lands, and if it hasn't moved by the time the finger lifts, that was a tap and the strip unfreezes there and then. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_019mrt228Upn9nNCwGixzSLx --- src/app.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/app.js b/src/app.js index 5aabd57..273eb37 100644 --- a/src/app.js +++ b/src/app.js @@ -3708,10 +3708,23 @@ function balanceCard() { // (scrollend would be the semantic signal but it doesn't fire reliably // for programmatic scrolls across browsers, so it's not relied on.) onScrollend: (e) => { const el = e.currentTarget; if (!el._dragging) { clearTimeout(el._settle); settle(el); } }, - onTouchstart: (e) => { const el = e.currentTarget; el._dragging = true; _carDragging = true; el._skipMorph = true; clearTimeout(el._settle); }, + onTouchstart: (e) => { + const el = e.currentTarget; + el._dragging = true; _carDragging = true; el._skipMorph = true; + el._x0 = el.scrollLeft; // where the finger found it — a tap leaves this alone + clearTimeout(el._settle); + }, onTouchend: (e) => { const el = e.currentTarget; el._dragging = false; // onScroll's debounce settles it + // A TAP, not a drag: the strip never moved. Unfreeze it right now. + // Otherwise the freeze stood until a scroll settled — and a tap + // produces no scroll events, so it stood until the 700ms safety net, + // and the morph refused every render in between. That's why tapping + // the unit tag on a phone took most of a second to change the number + // while the same tap on a laptop (the mouse path has always had this + // guard) was instant. + if (Math.abs(el.scrollLeft - (el._x0 || 0)) < 2) { endCarouselDrag(); return; } // The finger is up: lift the render hold (the strip stays frozen via // _skipMorph) and commit the predicted landing NOW, so the history // below loads under the glide instead of after it. -- cgit v1.2.3