fix(mobile): block pinch-zoom lock-in on touch surfaces (#69) #70
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/69-block-pinch-zoom"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #69.
What & why
The operator got locked into an iOS Safari browser-zoom state during play (post-round-11, controls cropped on the right edge with no obvious escape). Root cause:
touch-action: manipulationPERMITS pinch-zoom — by spec it only suppresses double-tap-zoom and the 300ms click delay. #64/PR#65 usedmanipulationto kill rapid-tap-zoom, but a pinch landing on the controls still fired browser zoom. The name "manipulation" suggests broader gesture suppression than it delivers — that's the trap.The change
Tighten the four game-touch surfaces from
manipulation→none(canvas was alreadynonefor swipes):#app(frame + dead-space)manipulationnone#touch-controls(bar)manipulationnone.tc-row(button gaps)manipulationnone.tc-btn(buttons)manipulationnonecanvasnone(unchanged)noneOne file (
client/index.html), CSS-only + comment updates. No JS, no layout.Decision tree (not just the conclusion)
The issue's fix-tree offered
pan-x pan-y(option 1) vsnone(option 2) vs an escape-UI (option 3/4).noneoverpan-x pan-y: both block pinch-zoom.pan-x pan-ywould be the right call if any game surface had scrollable overflow (e.g. a future controls bar that scrolls). None do — the page isoverflow: hidden, fits the viewport, and the controls are fixed tap targets. Sononeloses nothing legitimate and is strictly more zoom-proof. It also matches the canvas treatment, keeping the whole game surface uniform.noneon every viewport-covering surface, the in-scope zoom-entry paths (pinch, double-tap) are prevented, so a recovery button addresses a state that can no longer be entered through them. The only remaining path is iOS accessibility triple-tap zoom — which is OS-level, out-of-scope per AC3, and a page-level button cannot reset it anyway. So an escape-UI would be dead weight. The documented escape (Safari page reload) remains as the backstop for the accessibility path.touch-action: noneovere.preventDefault()-only: the buttons alreadypreventDefaultonpointerdown, but that doesn't stop multi-touch pinch at the browser-gesture layer —touch-actionis the declarative mechanism for that. They compose:touch-actionblocks the gesture,preventDefaultsuppresses the ghost click.Why tap/swipe input is unaffected (AC5)
touch-action: noneblocks browser default gestures (scroll/zoom), not pointer events. The buttons fire onpointerdown(main.ts:539) — which still dispatches undertouch-action: none. Verified: a syntheticpointerdownon a control still toggles.tc-pressedon an emulated iPhone-13. Canvas swipe gestures already relied ontouch-action: none, so they're unchanged.Acceptance criteria
none). ✓touch-action: noneon every touch surface. ✓touch-action: none; pointer events unaffected. ✓Verification
npx tsc --noEmitclean;vite buildclean.dist/index.html(and the preview-served bytes) carry all 5touch-action: nonedeclarations.touch-actionisnoneon#app,#board,#touch-controls,.tc-row,.tc-btn; synthetic tap toggles.tc-pressed(AC5); no page errors; portrait title renders cleanly (no layout regression).Note on the verification ceiling
Headless Playwright can't truly exercise iOS Safari's pinch-gesture engine, so the deterministic proof here is the mechanism (computed
touch-action: noneon the served bundle) plus no input regression. Final confirmation that the lock-in is gone is an on-device operator check — the operator is the device tester this campaign.🤖 Generated with Claude Code
Surveyor review — APPROVE ✅
Verified at head
299b18c49ce95554d11d670bf0439513e1e9009d, merge-base = current main tip0a2ae1b(clean, on current main). CSS-only, +17/−10.Root-cause framing is accurate and the fix is right.
touch-action: manipulation(from #65/#64) only kills double-tap-zoom + the click-delay — it permits pan and pinch-zoom. So #65 correctly closed #64 (double-tap) but left the pinch path open, which is the #69 lock-in the operator hit.noneblocks every browser gesture (pinch / double-tap / pan); on a fullscreen game with no scrollable or zoomable surface there's nothing legitimate to lose. This is a tightening, not a regression of #65 — different gesture, same surfaces.Decision-tree (none vs pan-x/pan-y vs escape-UI) lands on the right branch:
noneis correct here precisely because no surface wants pan or zoom;pan-x/pan-ywould be for a scroll container (none exist), and an escape-UI is overkill when the gesture can just be suppressed at the source.Verified (served bytes confirmed before trusting the probe):
vite buildclean; dist serves exactly 5×touch-action: none(canvas + #app + #touch-controls + .tc-row + .tc-btn), 0manipulation— confirmed on the wire (curl | grep), then probed.cb69-verify.js): all 5 surfaces computetouch-action: none; AC5 no-regression holds — a real touchpointerdownon#tc-leftstill fires its handler (.tc-pressedtoggles) undertouch-action: none; zero page errors. (touch-action: nonesuppresses browser gestures, not tap/click dispatch — so the controls keep working, which the probe confirms empirically.)Honest verification-ceiling — operator-device-gated (probe-gated-merge-separation, n=3 of the operator-device-gate variant after audio #58 + text-selection #61): headless can't exercise a real iOS pinch, so the probe proves the mechanism (computed
noneeverywhere + tap still works); the actual iOS pinch-lock-in resolution is the operator's on-device confirm. The stamp claims the mechanism axis, not the iOS-effectiveness axis — and merging deploys the fix so the operator can confirm.must-fix: none. should-consider: none. nit: none. The comments are accurate and the change is minimal + complete.
Clean to merge. Stamping APPROVED below, pinned to the verified head. → Bosun's gate.
APPROVED at head
299b18c49ce95554d11d670bf0439513e1e9009d(on current main0a2ae1b). CSS-only. Root cause accurate: manipulation permits pinch-zoom (#69 lock-in path); none blocks all browser gestures while tap/click still fire. Tightening of #65, not a regression. build clean; served bytes = 5× touch-action:none / 0 manipulation (verified on the wire before probing); iPhone-13 probe PASS (all 5 surfaces computed none + AC5 tap-handler fires under none + zero errors). Operator-device-gated for the real iOS pinch (probe-gated-merge-separation, n=3 variant) — stamp claims the mechanism axis, not iOS-effectiveness. No must-fix/should-consider/nit. Substance in the issue-comment above. Clean to merge → Bosun's gate.