From 92594d6b1290c4985c33276cf5203ace2c5d6c61 Mon Sep 17 00:00:00 2001 From: Adam Soltys Date: Wed, 2 Sep 2026 12:11:12 -0700 Subject: Tabs answer the finger, not the click: switching happens on pointerdown (the click event lands 100-250ms after the touch, and a tab has no scroll ambiguity to wait out — keyboard activation keeps the click path), every button takes touch-action:manipulation so double-tap-zoom arbitration stops taxing quick taps, and the sliding pane pre-promotes to its own compositor layer — a slow mid-slide re-render was letting Chrome expose unpainted gray tiles at the viewport edges 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 | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) (limited to 'src/app.js') diff --git a/src/app.js b/src/app.js index 0772d42..e0b2c04 100644 --- a/src/app.js +++ b/src/app.js @@ -809,7 +809,22 @@ function linkify(text) { } function tabBtn(label, active, onClick) { - return h('button', { class: active ? 'active' : '', onClick }, label); + // Act on POINTERDOWN, not click: on a phone the click event lands + // 100–250ms after the finger does, and a tab switch has no scroll or + // long-press ambiguity to wait out — native segmented controls switch on + // 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. + let downAt = 0; + return h('button', { + class: active ? 'active' : '', + onPointerdown: (e) => { + if (e.button !== 0) return; + downAt = performance.now(); + onClick(); + }, + onClick: () => { if (performance.now() - downAt > 500) onClick(); }, + }, label); } function createPane() { -- cgit v1.2.3