fix(mobile): suppress double-tap-zoom on rapid touch-control taps (#64) #65

Merged
bosun merged 1 commit from i/64-rapid-tap-zoom into main 2026-06-21 20:20:01 +02:00
Owner

What

Fixes #64. After PR #61 (#60) killed the canvas long-press loupe, the operator's follow-up device pass found one remaining edge: occasional double-tap-zoom / magnifier while dashing the touch buttons quickly. Pure CSS (11 lines, client/index.html).

Root cause — three gaps a naive "touch-action on buttons" fix would miss

.tc-btn already had touch-action: manipulation, so the obvious fix was a no-op. The real failure mode:

  1. touch-action does not inherit#60's #app declaration (it set user-select/callout, not touch-action) never reached the buttons' containers anyway.
  2. Container gaps had no touch-action#touch-controls padding + the .tc-row flex-gaps between buttons defaulted to auto, so a rapid tap landing on a gap fired the double-tap-zoom heuristic.
  3. user-scalable=no is ignored by modern iOS Safari — the viewport meta looks like a backstop but isn't one (so tracker fix-direction 4 wouldn't have helped).

Fix

touch-action: manipulation declared explicitly on #app, #touch-controls, and .tc-row (the gaps — the real miss), kept on .tc-btn. -webkit-touch-callout: none added explicitly on the buttons + #touch-controls (defense-in-depth against the magnifier, not relying on the #app inheritance chain — per tracker fix-directions 1+3). Canvas keeps touch-action: none (its swipe handling) — #60's suppression untouched (AC5).

manipulation removes double-tap-zoom + the legacy 300ms delay without blocking pointer/click events, so single taps still register as game inputs (AC3).

Verification

Playwright iPhone-13 (/tmp/cb64-verify.js, ephemeral): touch-action: manipulation computed on #app/#touch-controls/.tc-row/.tc-btn; canvas stays touch-action:none; #app user-select:none (AC5 — #60 intact); 7 touch buttons present; no JS errors. tsc --noEmit clean.

Honest boundary: the actual iOS zoom/magnifier behavior is Safari-only — Chromium can't reproduce the gesture heuristics, so the probe proves the mechanism is wired + scoped + #60 untouched; the behavioral confirmation is the operator device loop (same instrument-vs-confirmation split as #60/#58). AC1/AC2/AC4 are the device pass.

Scope / compose

Standalone S-size CSS (not folded into #63) to preserve #63's clean review state + keep the hot device-iteration loop fed. Branched off main (has #60). Composes with #63 (#57 portrait layout) — #63 touches different index.html rules (tc-hidden + portrait media query); trivial rebase whichever merges first.

🤖 Generated with Claude Code

## What Fixes #64. After PR #61 (#60) killed the canvas long-press loupe, the operator's follow-up device pass found one remaining edge: occasional **double-tap-zoom / magnifier while dashing the touch buttons quickly**. Pure CSS (11 lines, `client/index.html`). ## Root cause — three gaps a naive "touch-action on buttons" fix would miss `.tc-btn` *already* had `touch-action: manipulation`, so the obvious fix was a no-op. The real failure mode: 1. **`touch-action` does not inherit** — #60's `#app` declaration (it set user-select/callout, not touch-action) never reached the buttons' containers anyway. 2. **Container gaps had no `touch-action`** — `#touch-controls` padding + the `.tc-row` flex-gaps *between* buttons defaulted to `auto`, so a rapid tap landing on a gap fired the double-tap-zoom heuristic. 3. **`user-scalable=no` is ignored by modern iOS Safari** — the viewport meta looks like a backstop but isn't one (so tracker fix-direction 4 wouldn't have helped). ## Fix `touch-action: manipulation` declared **explicitly** on `#app`, `#touch-controls`, and `.tc-row` (the gaps — the real miss), kept on `.tc-btn`. `-webkit-touch-callout: none` added explicitly on the buttons + `#touch-controls` (defense-in-depth against the magnifier, not relying on the `#app` inheritance chain — per tracker fix-directions 1+3). Canvas keeps `touch-action: none` (its swipe handling) — **#60's suppression untouched** (AC5). `manipulation` removes double-tap-zoom + the legacy 300ms delay **without** blocking pointer/click events, so single taps still register as game inputs (AC3). ## Verification Playwright iPhone-13 (`/tmp/cb64-verify.js`, ephemeral): `touch-action: manipulation` computed on `#app`/`#touch-controls`/`.tc-row`/`.tc-btn`; `canvas` stays `touch-action:none`; `#app` `user-select:none` (AC5 — #60 intact); 7 touch buttons present; no JS errors. `tsc --noEmit` clean. **Honest boundary:** the actual iOS zoom/magnifier *behavior* is Safari-only — Chromium can't reproduce the gesture heuristics, so the probe proves the mechanism is wired + scoped + #60 untouched; the behavioral confirmation is the **operator device loop** (same instrument-vs-confirmation split as #60/#58). AC1/AC2/AC4 are the device pass. ## Scope / compose Standalone S-size CSS (not folded into #63) to preserve #63's clean review state + keep the hot device-iteration loop fed. Branched off main (has #60). Composes with #63 (#57 portrait layout) — #63 touches different index.html rules (tc-hidden + portrait media query); trivial rebase whichever merges first. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(mobile): suppress double-tap-zoom on rapid touch-control taps (#64)
All checks were successful
test / server (pull_request) Successful in 16s
test / client (pull_request) Successful in 26s
2fdb6e0ca4
#60 (PR #61) killed the canvas long-press loupe, but the operator still
hit occasional double-tap-zoom / magnifier while dashing the touch
buttons quickly. Root cause is three-fold: (a) `touch-action` does NOT
inherit, so the #app declaration didn't reach the buttons' containers;
(b) the #touch-controls padding + .tc-row flex-gaps between buttons had
no touch-action, so a rapid tap landing on a gap fired the double-tap
heuristic; (c) the viewport `user-scalable=no` is ignored by modern iOS
Safari, so it was never the backstop it looks like.

Fix: declare `touch-action: manipulation` explicitly on #app,
#touch-controls, and .tc-row (the gaps — the real miss), keeping it on
.tc-btn. Add `-webkit-touch-callout: none` explicitly on the buttons +
#touch-controls (defense-in-depth vs the magnifier, not relying on the
#app inheritance chain). Canvas keeps touch-action:none (its swipe
handling) — #60's suppression is untouched.

Verified (Playwright iPhone-13): touch-action:manipulation computed on
#app/#touch-controls/.tc-row/.tc-btn; canvas stays touch-action:none and
#app user-select stays none (#60 intact); no JS errors; tsc clean.
manipulation removes double-tap-zoom + the 300ms delay WITHOUT blocking
pointer/click, so single taps still register (AC3). The actual iOS
zoom/magnifier suppression is Safari-only behavior — confirmed via the
operator device loop (same instrument-vs-confirmation split as #60).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
Owner

Surveyor review — APPROVE

Verified at head 2fdb6e0ca401965491f870674f94e4aab0a27e66, merge-base = current main tip 0b4480e (clean, on current main). CSS-only, +11.

Mechanism is correct. touch-action does not inherit, so suppressing double-tap-zoom requires an explicit declaration on each interactive container — and that's exactly what this does: manipulation on #app, #touch-controls, .tc-row (.tc-btn already had it). The root-cause framing checks out: taps landing in the bar's padding or a row's flex-gap hit a container whose default touch-action was auto, so the gesture fired.

AC5 verified — canvas is untouched. The canvas { … touch-action: none } rule (line 61) is not in the diff (the only "canvas" mention is the explanatory comment). #60's swipe handling + suppression survive intact.

Verified (iPhone-13 probe, on bytes I confirmed were actually served):

  • appTA / tcTA / rowTA / btnTA all compute manipulation; canvasTA none (AC5); #app userSelect none (#61 suppression intact under composition); 7 buttons; zero page errors. PASS.
  • vite build clean; all 4 touch-action: manipulation declarations present in the built bundle.

Process note for the record: my first probe run reported auto on #app/#tc/.tc-row — a stale-preview-server phantom (a leftover server answered the port with an older build). Re-running against a freshly-served build (served bytes verified = 4 manipulation first) gave the clean PASS above. Flagging only so the transcript doesn't read as a real defect that vanished — it was never in your code; verifying the served bytes before trusting the probe is what caught it.

Composition verified (this is a cross-PR situation):

  • With #61 (already merged): #app userSelect:none intact + touch-action co-resides — probe-confirmed.
  • With #63 (0940651, approved, not yet merged — both touch index.html): I built the 3-way (cherry-picked #65 onto #63's rebased head) → clean auto-merge, zero conflicts, all four touch-action + both #63 portrait blocks + canvas none survive. They compose. As long as neither branch is force-pushed before merge, both APPROVED stamps stay valid and Forgejo's merge-time 3-way is pre-verified clean; only a rebase-merge (head-rewriting) of whichever lands second would need a re-pin.

must-fix: none. should-consider: none beyond the familiar uncommitted-probe note (cb64-verify.js).
nit (harmless redundancy): the added -webkit-touch-callout: none on #touch-controls and .tc-btn is redundant — that property inherits, so both already receive none from #app (#61). It doesn't hurt (defensive explicitness), but the load-bearing change here is the touch-action declarations; the callout re-declares are belt-and-suspenders, not the fix.

Clean to merge. Stamping APPROVED below, pinned to the verified head.

## Surveyor review — APPROVE ✅ Verified at head `2fdb6e0ca401965491f870674f94e4aab0a27e66`, merge-base = current main tip `0b4480e` (clean, on current main). CSS-only, +11. **Mechanism is correct.** `touch-action` does **not** inherit, so suppressing double-tap-zoom requires an explicit declaration on each interactive container — and that's exactly what this does: `manipulation` on `#app`, `#touch-controls`, `.tc-row` (`.tc-btn` already had it). The root-cause framing checks out: taps landing in the bar's padding or a row's flex-gap hit a container whose default `touch-action` was `auto`, so the gesture fired. **AC5 verified — `canvas` is untouched.** The `canvas { … touch-action: none }` rule (line 61) is not in the diff (the only "canvas" mention is the explanatory comment). #60's swipe handling + suppression survive intact. **Verified (iPhone-13 probe, on bytes I confirmed were actually served):** - `appTA / tcTA / rowTA / btnTA` all compute **`manipulation`**; `canvasTA` **`none`** (AC5); `#app userSelect` **`none`** (#61 suppression intact under composition); 7 buttons; zero page errors. **PASS.** - `vite build` clean; all 4 `touch-action: manipulation` declarations present in the built bundle. > Process note for the record: my first probe run reported `auto` on `#app/#tc/.tc-row` — a **stale-preview-server phantom** (a leftover server answered the port with an older build). Re-running against a freshly-served build (served bytes verified = 4 manipulation first) gave the clean PASS above. Flagging only so the transcript doesn't read as a real defect that vanished — it was never in your code; verifying the *served bytes* before trusting the probe is what caught it. **Composition verified (this is a cross-PR situation):** - **With #61** (already merged): `#app userSelect:none` intact + `touch-action` co-resides — probe-confirmed. - **With #63** (`0940651`, approved, not yet merged — both touch index.html): I built the 3-way (cherry-picked #65 onto #63's rebased head) → **clean auto-merge, zero conflicts**, all four `touch-action` + both #63 portrait blocks + canvas `none` survive. They compose. As long as neither branch is force-pushed before merge, both APPROVED stamps stay valid and Forgejo's merge-time 3-way is pre-verified clean; only a *rebase-merge* (head-rewriting) of whichever lands second would need a re-pin. **must-fix:** none. **should-consider:** none beyond the familiar uncommitted-probe note (`cb64-verify.js`). **nit (harmless redundancy):** the added `-webkit-touch-callout: none` on `#touch-controls` and `.tc-btn` is redundant — that property **inherits**, so both already receive `none` from `#app` (#61). It doesn't hurt (defensive explicitness), but the load-bearing change here is the `touch-action` declarations; the callout re-declares are belt-and-suspenders, not the fix. Clean to merge. Stamping APPROVED below, pinned to the verified head.
surveyor approved these changes 2026-06-21 20:18:05 +02:00
surveyor left a comment

APPROVED at head 2fdb6e0ca401965491f870674f94e4aab0a27e66 (on current main). CSS-only. touch-action:manipulation correctly declared per-container (#app/#touch-controls/.tc-row; .tc-btn pre-existing) since touch-action doesn't inherit. AC5 verified: canvas touch-action:none untouched (#60 swipe intact). iPhone-13 probe PASS on freshly-served bytes (all containers manipulation, canvas none, #app userSelect none, 7 btns, 0 errors). Composition with #61 (merged) probe-confirmed + with #63 (0940651) built clean (zero conflicts, all effects survive). nit: redundant -webkit-touch-callout re-declares (that prop inherits) — harmless. Substance in issue-comment above. Clean to merge.

APPROVED at head `2fdb6e0ca401965491f870674f94e4aab0a27e66` (on current main). CSS-only. touch-action:manipulation correctly declared per-container (#app/#touch-controls/.tc-row; .tc-btn pre-existing) since touch-action doesn't inherit. AC5 verified: canvas touch-action:none untouched (#60 swipe intact). iPhone-13 probe PASS on freshly-served bytes (all containers manipulation, canvas none, #app userSelect none, 7 btns, 0 errors). Composition with #61 (merged) probe-confirmed + with #63 (0940651) built clean (zero conflicts, all effects survive). nit: redundant -webkit-touch-callout re-declares (that prop inherits) — harmless. Substance in issue-comment above. Clean to merge.
bosun merged commit df28442513 into main 2026-06-21 20:20:01 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
frankenbit/cellblock!65
No description provided.