Widen the edge-tab tap target, split scroll from reposition #9

Merged
sulthan merged 5 commits from userscript-edge-tab-hitbox into main 2026-07-28 20:36:26 +07:00
Owner

The 7px edge tab is well below a usable touch target on a phone. This widens
only its hit area — the visible sliver still measures exactly 7 × 44 and
offsetWidth/offsetHeight still report it, so placeFab, applyFabPos and
the edge-snap maths are untouched.

  • An invisible #hit child extends the tappable box inward to 28 × 72.
    overflow: hidden had to go (it would clip #hit), so the rounded-corner
    clip for the dwell-progress fill moves onto #fill via border-radius: inherit.
  • touch-action is resolved by the browser at gesture start, so the strip
    cannot be both browser-scrolled and script-dragged. #fab keeps
    touch-action: none, owns every gesture, and makeDraggable splits by
    intent: a plain swipe from #hit scrolls the page via window.scrollBy, a
    ~400ms hold arms a reposition drag (tab brightens and grows a ring), and the
    visible sliver still drags immediately with no hold.
  • Adds the previously missing pointercancel reset, and snaps + saves on
    cancel so an OS-claimed gesture (Android's swipe-back starts in exactly this
    screen region) cannot strand the tab mid-screen.
  • Clears a stale dataset.dragged on pointerdown, so a scroll or drag that
    produces no trailing click cannot swallow the next tap.

Testing

node --check clean, node --test userscript/test/logic.test.js 14/14 pass.
The gesture code has no unit test — logic.test.js runs under node with no DOM
and this branch deliberately does not add a DOM harness. Manual device checks
(tap / swipe-to-scroll / hold-to-arm / sliver-drag on a chapter page) are the
real verification and are pending.

Known ceilings

  • The scroll is hand-rolled: no momentum or fling, and it assumes the document
    is the scroller rather than a nested container. Flagged in a ponytail:
    comment with the upgrade path.
  • Gesture state is not keyed by pointerId, so a second finger corrupts an
    in-progress gesture. Pre-existing, not a regression.
  • A >400ms still press on the visible sliver shows the armed ring even though
    the sliver never needs a hold. Cosmetic only.

🤖 Generated with Claude Code

The 7px edge tab is well below a usable touch target on a phone. This widens only its *hit* area — the visible sliver still measures exactly 7 × 44 and `offsetWidth`/`offsetHeight` still report it, so `placeFab`, `applyFabPos` and the edge-snap maths are untouched. - An invisible `#hit` child extends the tappable box inward to `28 × 72`. `overflow: hidden` had to go (it would clip `#hit`), so the rounded-corner clip for the dwell-progress fill moves onto `#fill` via `border-radius: inherit`. - `touch-action` is resolved by the browser at gesture start, so the strip cannot be both browser-scrolled and script-dragged. `#fab` keeps `touch-action: none`, owns every gesture, and `makeDraggable` splits by intent: a plain swipe from `#hit` scrolls the page via `window.scrollBy`, a ~400ms hold arms a reposition drag (tab brightens and grows a ring), and the visible sliver still drags immediately with no hold. - Adds the previously missing `pointercancel` reset, and snaps + saves on cancel so an OS-claimed gesture (Android's swipe-back starts in exactly this screen region) cannot strand the tab mid-screen. - Clears a stale `dataset.dragged` on pointerdown, so a scroll or drag that produces no trailing click cannot swallow the *next* tap. ## Testing `node --check` clean, `node --test userscript/test/logic.test.js` 14/14 pass. The gesture code has no unit test — `logic.test.js` runs under node with no DOM and this branch deliberately does not add a DOM harness. Manual device checks (tap / swipe-to-scroll / hold-to-arm / sliver-drag on a chapter page) are the real verification and are pending. ## Known ceilings - The scroll is hand-rolled: no momentum or fling, and it assumes the document is the scroller rather than a nested container. Flagged in a `ponytail:` comment with the upgrade path. - Gesture state is not keyed by `pointerId`, so a second finger corrupts an in-progress gesture. Pre-existing, not a regression. - A >400ms still press on the *visible* sliver shows the armed ring even though the sliver never needs a hold. Cosmetic only. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
sulthan added 4 commits 2026-07-28 20:18:57 +07:00
The 7px sliver is well below a usable touch target on a phone. An
invisible #hit child extends the hit area inward while the visible tab
stays exactly 7x44. Drops overflow:hidden (it would clip #hit) and moves
the corner clip onto #fill via border-radius: inherit.
touch-action is resolved at gesture start, so the strip cannot be both
browser-scrolled and script-dragged. Own the gesture and split by intent:
a swipe from the strip scrolls the page, a ~400ms hold arms a reposition
drag, and the visible sliver still drags with no hold. Adds the missing
pointercancel reset.
pointercancel fires when the OS claims the gesture mid-drag — likely on
the screen edge where the widened hit strip lives, which is also where
Android's swipe-back gesture starts. Bare reset() left the tab stranded
wherever the finger last was, unsnapped and unsaved, until a resize or
reload. Share the pointerup snap block with pointercancel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sulthan added 1 commit 2026-07-28 20:30:29 +07:00
touch-action: none stops panning and zooming but not Chromium's
long-press gesture. It fires at ~500ms, just after ARM_MS, raises the
context menu and cancels the pointer stream — so the hold armed the drag
and the OS stole it back a frame later, leaving the page's own long-press
UI on screen and the tab where it was.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
sulthan merged commit 1dc5b2ab65 into main 2026-07-28 20:36:26 +07:00
sulthan deleted branch userscript-edge-tab-hitbox 2026-07-31 01:01:54 +07:00
Sign in to join this conversation.