From 79ebdaa0426cc9dc474a61876207ff19fbc5aa22 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 2 Sep 2026 12:14:25 -0700 Subject: Tab feedback at touch speed: the strip's active state flips with a direct DOM write that paints in the same frame as the finger, and the heavy pane render runs after that paint (double-rAF) — running it synchronously on pointerdown meant even the button's own light-up waited out a 100ms+ rebuild MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01MrkLsNScXe9b6BviUSdmMr --- src/app.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/app.js b/src/app.js index e0b2c04..a818d67 100644 --- a/src/app.js +++ b/src/app.js @@ -815,15 +815,27 @@ function tabBtn(label, active, onClick) { // touch-down for exactly this reason. The click handler stays for // keyboard/AT activation; re-selecting the current tab is a no-op, so the // pair firing together costs nothing. + // + // And feedback comes FIRST: the full re-render can take 100ms+ on a + // phone, and running it synchronously meant even the button's own active + // state waited for the whole pane to build. Flip the strip's classes with + // a direct DOM write (paints this frame), then run the real render after + // that paint — double-rAF is the "after the next paint" idiom. let downAt = 0; + const select = (btn) => { + if (btn && btn.parentElement) { + for (const b of btn.parentElement.children) b.classList.toggle('active', b === btn); + } + requestAnimationFrame(() => requestAnimationFrame(onClick)); + }; return h('button', { class: active ? 'active' : '', onPointerdown: (e) => { if (e.button !== 0) return; downAt = performance.now(); - onClick(); + select(e.currentTarget); }, - onClick: () => { if (performance.now() - downAt > 500) onClick(); }, + onClick: (e) => { if (performance.now() - downAt > 500) select(e.currentTarget); }, }, label); } -- cgit v1.2.3