Make every tap in the dealer land instantly on a phone #44

Merged
jheaps merged 1 commits from feature/casino-holdem-touch-mobile into master 2026-09-01 09:29:54 -06:00
Owner

Mobile-feel round. Four measured problems, all of which only show up under a thumb. CSS only — stacked on #43.

1. Every tap waited on the browser — 53 of 53 controls

Not one interactive element set touch-action, so a mobile browser held every tap for ~300ms to see whether a second one was coming that would mean "zoom in". That's on the fold button, the call button, every seat, every card slot.

Worse: this tool uses double-tap on a seat as the fold shortcut — which is the browser's zoom gesture. The dealer's fastest shortcut was fighting the page zooming under their thumb. A single seat tap cost ~300ms of browser hesitation before the app's own 220ms double-tap window even started.

touch-action: manipulation keeps panning and pinch-zoom and drops only double-tap-to-zoom — precisely the one that was in the way.

2. Press feedback was the OS grey box — 1 :active rule in 2385 lines

Only .actionBtn had a pressed state. Everything else — seats, header buttons, card slots, picker cards, menu rows, toasts, wizard choices — leaned on the translucent grey rectangle the browser paints on tap.

So the tap highlight couldn't just be switched off; the controls needed real pressed states first. They now press down 1px and brighten in 0.04s, matching the action bar that already did. Then the grey box goes.

3. Hover stuck after every tap — 21 unguarded rules

:hover has no meaning on touch but still applies, and stays applied until you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though still under a cursor — and it would have fought the new pressed states directly. All are behind @media (hover: hover) now. The one rule sharing a block with :focus-visible was split so keyboard focus survives.

4. The notes field zoomed the page (iOS)

#tableNotes was 13.3px. iOS zooms the viewport when a field under 16px takes focus, and the dealer is then pinching back out to see the table. Every other input was already 16px; this was the straggler.

Seats deliberately have no :active rule

Worth flagging because it nearly went in. A seat's transform is what positions it around the felt, so a pressed state setting transform drops the translate(-50%, -50%) and throws every pressed seat ~38px off its spot.

Validation caught it before it shipped: seats already dip via .seatPressing, which pointerdown adds in render.js — earlier than :active resolves, and it restates the translate correctly. The rule is gone and the reason is a comment in the stylesheet.

Validation (Playwright, 390x844)

Check Result
touch-action: auto 53 → 0, across table, all 4 overlays, Quick Split
Tap-highlighted controls 0 everywhere
Text fields still selectable 12/12 table, 4/4 Quick Split
Buttons made unselectable all
Hover rules gated 14/14, 0 unguarded (both sheets)
Fold press, measured live translateY(1px) + brightness(1.3)
Seat press, measured live keeps its translate; 2px drift from the shrink alone

Plus a hand played through the UI including a double-tap fold, no console errors, and unit tests still 58/58.

Mobile-feel round. Four measured problems, all of which only show up under a thumb. CSS only — stacked on #43. ## 1. Every tap waited on the browser — 53 of 53 controls Not one interactive element set `touch-action`, so a mobile browser held **every tap for ~300ms** to see whether a second one was coming that would mean "zoom in". That's on the fold button, the call button, every seat, every card slot. Worse: this tool uses **double-tap on a seat as the fold shortcut** — which *is* the browser's zoom gesture. The dealer's fastest shortcut was fighting the page zooming under their thumb. A single seat tap cost ~300ms of browser hesitation before the app's own 220ms double-tap window even started. `touch-action: manipulation` keeps panning and pinch-zoom and drops only double-tap-to-zoom — precisely the one that was in the way. ## 2. Press feedback was the OS grey box — 1 `:active` rule in 2385 lines Only `.actionBtn` had a pressed state. Everything else — seats, header buttons, card slots, picker cards, menu rows, toasts, wizard choices — leaned on the translucent grey rectangle the browser paints on tap. So the tap highlight couldn't just be switched off; the controls needed real pressed states first. They now press down 1px and brighten in 0.04s, matching the action bar that already did. Then the grey box goes. ## 3. Hover stuck after every tap — 21 unguarded rules `:hover` has no meaning on touch but still applies, and stays applied until you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though still under a cursor — and it would have fought the new pressed states directly. All are behind `@media (hover: hover)` now. The one rule sharing a block with `:focus-visible` was split so keyboard focus survives. ## 4. The notes field zoomed the page (iOS) `#tableNotes` was 13.3px. iOS zooms the viewport when a field under 16px takes focus, and the dealer is then pinching back out to see the table. Every other input was already 16px; this was the straggler. ## Seats deliberately have no `:active` rule Worth flagging because it nearly went in. A seat's `transform` is what positions it around the felt, so a pressed state setting `transform` drops the `translate(-50%, -50%)` and throws every pressed seat **~38px off its spot**. Validation caught it before it shipped: seats already dip via `.seatPressing`, which `pointerdown` adds in `render.js` — earlier than `:active` resolves, and it restates the translate correctly. The rule is gone and the reason is a comment in the stylesheet. ## Validation (Playwright, 390x844) | Check | Result | | --- | --- | | `touch-action: auto` | 53 → **0**, across table, all 4 overlays, Quick Split | | Tap-highlighted controls | **0** everywhere | | Text fields still selectable | 12/12 table, 4/4 Quick Split | | Buttons made unselectable | all | | Hover rules gated | **14/14**, 0 unguarded (both sheets) | | Fold press, measured live | `translateY(1px)` + `brightness(1.3)` | | Seat press, measured live | keeps its translate; 2px drift from the shrink alone | Plus a hand played through the UI including a double-tap fold, no console errors, and unit tests still 58/58.
jheaps changed target branch from chore/casino-holdem-tech-debt to master 2026-09-01 09:29:52 -06:00
jheaps added 1 commit 2026-09-01 09:29:52 -06:00
Four measured problems, all of which only show up under a thumb.

Every tap waited on the browser (53 of 53 controls)

Not one interactive element set `touch-action`, so a mobile browser held every
tap for ~300ms to see whether a second one was coming that would mean "zoom in".
That is on the fold button, the call button, every seat, every card slot.

Worse, this tool uses double-tap on a seat as the fold shortcut — which *is* the
browser's zoom gesture. The dealer's fastest shortcut was fighting the page
zooming under their thumb. A single seat tap cost ~300ms of browser hesitation
before the app's own 220ms double-tap window even started.

`touch-action: manipulation` everywhere keeps panning and pinch-zoom and drops
only double-tap-to-zoom, which is precisely the one that was in the way.

Press feedback was the OS grey box (1 `:active` rule in 2385 lines)

Only `.actionBtn` had a pressed state. Everything else — seats, header buttons,
card slots, picker cards, menu rows, toasts, wizard choices — relied on the
translucent grey rectangle the browser paints on tap. So the tap highlight
couldn't just be switched off; the controls needed real pressed states first.
They press down 1px and brighten, in 0.04s, matching the action bar that already
did. Then the grey box goes.

Hover stuck after every tap (21 unguarded rules)

`:hover` has no meaning on touch, but it still applies — and stays applied until
you tap elsewhere. Tapping "Next hand" left it lifted and lightened as though
still under a cursor, and it would have fought the new pressed states directly.
All of them are behind `@media (hover: hover)` now. The one rule that shared a
block with `:focus-visible` was split so keyboard focus survives.

The notes field zoomed the page (iOS)

`#tableNotes` was 13.3px. iOS zooms the viewport when a field under 16px takes
focus, and the dealer is then pinching back out to see the table. Every other
input here was already 16px; this was the straggler.

Seats deliberately have no `:active` rule

Worth writing down because it nearly went in. A seat's `transform` is what
positions it around the felt, so a pressed state setting `transform` drops the
`translate(-50%, -50%)` and throws every pressed seat ~38px off its spot.
Validation caught it: seats already dip via `.seatPressing`, which `pointerdown`
adds in render.js — earlier than `:active` resolves, and it restates the
translate correctly.

Validated with Playwright at 390x844: 53/53 controls off `touch-action: auto` and
0 tap-highlighted across the table, all four overlays and Quick Split; text
fields still selectable and buttons not; 14/14 hover rules gated; presses
measured live via mouse-down (fold reads translateY(1px)/brightness(1.3), a seat
holds its translate with 2px of drift from the shrink alone). Hand played through
the UI including a double-tap fold. No console errors. Unit tests 58/58.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
jheaps merged commit ee37cf7bd9 into master 2026-09-01 09:29:54 -06:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: jheaps/JoshHeaps.Net#44