fix(client): touch BACK affordance on multiplayer check-in (#88) #136

Merged
bosun merged 1 commit from i/88-checkin-touch-back into main 2026-06-23 20:02:08 +02:00
Owner

What

Resolves the mobile-keyboard-trap audit (#88). The audit's full enumeration is posted as a comment on #88 (the audit deliverable, AC1). This PR implements the single fix the audit confirmed at source.

The audit, in one line

Enumerated all 12 client screens/phases for keyboard-Esc vs touch-back symmetry, verified each flag at source (the sub-agent enumeration was hypothesis-form; per-item source-verify is what shrinks-or-grows the batch). Result: exactly one genuine mobile-keyboard-trap — checkin.

Outcome Screen(s)
TRAP → fixed here checkin (name entry): touch-reachable via VERSUS, but Esc-back was keyboard-only
False-positive (excluded) HIGH SCORES overlay — keyboard-only-reachable (L key, no touch open affordance), so a touch user can't enter it → can't be trapped; symmetric in the way that matters
Intentional nav-gap (by design) matched (committed-post-match soft lock), countdown (input-frozen focus beat, #110)
Already symmetric lobby (#85), playing/abort (#87), gameover solo+versus, spectate

The fix

checkin is reached by tapping VERSUS (main.ts) but its only back-out was Esc → welcome (#79). A phone has no Esc key, and every tap on the screen otherwise just re-summons the keyboard (#66), so a touch player was trapped at name-entry. Added a touch BACK mirroring #85's LOBBY_BACK_BTN exactly:

  • render.tsCHECKIN_BACK_BTN mutable-layout rect, recomputed each frame in drawCheckin from live CANVAS_W + the check-in box geometry, painted via the shared button() helper. Centered below the box+hint (clear of the #66 DOM input that overlays the box, and of the portrait top-left audio toolbar). inCheckinBackButton reads the same live rect → the tap lands where it's drawn in both orientations.
  • main.ts — the check-in click branch hit-tests BACK before the focusNameInput catch-all (which would otherwise swallow the tap), transitioning to 'title' exactly like the Esc handler; the render loop hides+blurs the input on the screen change → no keyboard-trap.

Verification

  • npx tsc --noEmit clean; full nav.spec.ts green (38 tests).
  • nav.spec touch-back row mirrors the #85 lobby test (taps the BACK rect centre on the 960×640 landscape harness, polls screen → 'title').
  • Mutation-proven: disabling the inCheckinBackButton hit-test branch reds the test — the tap falls through to the keyboard re-summon and screen stays 'checkin'. Restored precisely (re-edit, not checkout).
  • Render-capture (landscape + portrait) confirmed placement clear of the box/hint and the portrait audio toolbar; sent to the operator.

Design call (implementer-latitude, pattern was clear)

Placed BACK below the content (same as the #85 lobby BACK) rather than a top-corner affordance — the portrait audio toolbar (#btn-mute) reflows over the top-left, and the centered check-in content reflows nowhere, so below-content is clear in both orientations. A top-bar back would collide with the toolbar in portrait.

What this PR does NOT do

  • No HIGH SCORES touch-close — it's keyboard-only-reachable, so not a trap (see table). A separate, trivial latent finding (desktop click-through on that overlay) is filed as its own tracker, out of #88 scope.
  • No change to matched/countdown — intentional input locks, not traps.

Closes #88

🤖 Generated with Claude Code

## What Resolves the **mobile-keyboard-trap audit (#88)**. The audit's full enumeration is posted as a comment on #88 (the audit deliverable, AC1). This PR implements the single fix the audit confirmed at source. ## The audit, in one line Enumerated all 12 client screens/phases for keyboard-Esc vs touch-back symmetry, **verified each flag at source** (the sub-agent enumeration was hypothesis-form; per-item source-verify is what shrinks-or-grows the batch). Result: exactly **one** genuine mobile-keyboard-trap — `checkin`. | Outcome | Screen(s) | |---|---| | **TRAP → fixed here** | `checkin` (name entry): touch-reachable via VERSUS, but Esc-back was keyboard-only | | **False-positive (excluded)** | `HIGH SCORES` overlay — keyboard-only-reachable (`L` key, no touch open affordance), so a touch user can't enter it → can't be trapped; symmetric in the way that matters | | **Intentional nav-gap (by design)** | `matched` (committed-post-match soft lock), `countdown` (input-frozen focus beat, #110) | | **Already symmetric** | lobby (#85), playing/abort (#87), gameover solo+versus, spectate | ## The fix `checkin` is reached by tapping VERSUS (main.ts) but its only back-out was `Esc → welcome` (#79). A phone has no Esc key, and every tap on the screen otherwise just re-summons the keyboard (#66), so a touch player was trapped at name-entry. Added a touch BACK mirroring **#85's `LOBBY_BACK_BTN`** exactly: - **render.ts** — `CHECKIN_BACK_BTN` mutable-layout rect, recomputed each frame in `drawCheckin` from live `CANVAS_W` + the check-in box geometry, painted via the shared `button()` helper. Centered **below** the box+hint (clear of the #66 DOM input that overlays the box, and of the portrait top-left audio toolbar). `inCheckinBackButton` reads the same live rect → the tap lands where it's drawn in **both** orientations. - **main.ts** — the check-in click branch hit-tests BACK **before** the `focusNameInput` catch-all (which would otherwise swallow the tap), transitioning to `'title'` exactly like the Esc handler; the render loop hides+blurs the input on the screen change → no keyboard-trap. ## Verification - `npx tsc --noEmit` clean; full `nav.spec.ts` green (38 tests). - **nav.spec touch-back row** mirrors the #85 lobby test (taps the BACK rect centre on the 960×640 landscape harness, polls `screen → 'title'`). - **Mutation-proven:** disabling the `inCheckinBackButton` hit-test branch reds the test — the tap falls through to the keyboard re-summon and `screen` stays `'checkin'`. Restored precisely (re-edit, not checkout). - **Render-capture** (landscape + portrait) confirmed placement clear of the box/hint and the portrait audio toolbar; sent to the operator. ## Design call (implementer-latitude, pattern was clear) Placed BACK **below** the content (same as the #85 lobby BACK) rather than a top-corner affordance — the portrait audio toolbar (#btn-mute) reflows over the top-left, and the centered check-in content reflows nowhere, so below-content is clear in both orientations. A top-bar back would collide with the toolbar in portrait. ## What this PR does NOT do - **No HIGH SCORES touch-close** — it's keyboard-only-reachable, so not a trap (see table). A separate, trivial latent finding (desktop click-through on that overlay) is filed as its own tracker, out of #88 scope. - **No change to `matched`/`countdown`** — intentional input locks, not traps. Closes #88 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(client): touch BACK affordance on multiplayer check-in (#88)
All checks were successful
test / server (pull_request) Successful in 8s
test / client (pull_request) Successful in 9s
test / client-nav (pull_request) Successful in 1m1s
54784e7e76
The name-entry (check-in) screen is reachable by tapping VERSUS, but its
back-out was keyboard-only (Esc → welcome, #79). A phone has no Esc key,
and every tap on the screen otherwise just re-summons the keyboard (#66),
so a touch player who tapped VERSUS was trapped — the one genuine
mobile-keyboard-trap the #88 audit confirmed at source.

Add a touch BACK affordance mirroring #85's LOBBY_BACK_BTN exactly:
- CHECKIN_BACK_BTN mutable-layout rect, recomputed each frame in
  drawCheckin from the live CANVAS_W + check-in box geometry, drawn via
  the shared button() helper, centered below the box+hint (clear of the
  #66 DOM input that overlays the box, and of the portrait top-left audio
  toolbar). inCheckinBackButton reads the same live rect → a tap lands
  where it's drawn in both orientations.
- The check-in click branch hit-tests BACK before the focusNameInput
  catch-all (which would otherwise swallow the tap), transitioning to
  'title' exactly like the Esc handler; the render loop hides+blurs the
  input on the screen change, so no keyboard-trap.

nav.spec touch-back row mirrors the #85 lobby test; mutation-proven
(disabling the hit-test branch reds it — the tap falls through to the
keyboard re-summon, screen stays 'checkin').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
surveyor approved these changes 2026-06-23 20:01:14 +02:00
surveyor left a comment

APPROVED — checkin touch BACK (#88)

Reviewed at head 54784e7 (on current main 0ede7e0). The one genuine mobile-keyboard-trap the audit confirmed — cleanly fixed, mirroring the #85 pattern.

The fix is correct, and the ordering is the load-bearing part

The inCheckinBackButton hit-test is wired first, before the focusNameInput re-summon catch-all:

if (screen === 'checkin') {
  ... if (inCheckinBackButton(px, py)) { screen = 'title'; return; }
  focusNameInput(); return;
}

That ordering is exactly what makes it work — without the early hit-test+return, the catch-all would swallow the BACK tap and re-summon the keyboard (the trap). screen='title' then triggers the render loop's hide+blur of the #66 DOM input, so no lingering keyboard. Mirrors the keyboard Esc-back (#79) on the touch path.

Mirrors #85 + placement verified

CHECKIN_BACK_BTN is the same mutable-layout-export pattern as LOBBY_BACK_BTN (rect recomputed each frame in drawCheckin from live CANVAS_W + box geometry; inCheckinBackButton reads the same live rect → tap lands where drawn in both orientations). Render-capture confirms it: BACK sits centered below the box+hint in both portrait and landscape — clear of the box that the #66 DOM input overlays (box.y+130 vs the input at box.y..box.y+h), so a tap reaches the canvas handler, not the input. That's the geometry that matters.

Verification

  • tsc clean; 38/38 nav.spec green (the new #88: touch BACK → welcome row mirrors the #85 touch-back test).
  • Mutation reproduced: disabling the hit-test branch → the BACK tap falls through to focusNameInput → screen stays checkin → the test reds. Revert clean.

Audit discipline — exemplary

The #88 deliverable (12 screens, each verified at source) correctly fires the filed-rootcause-is-hypothesis discipline: the sub-agent's HIGH SCORES "2nd trap" was verified and excluded (keyboard-only-reachable, no touch open path → not a trap), and the genuinely-separable latent finding (the desktop HIGH SCORES click-through) was filed as #137 rather than dropped or scope-crept into this PR. I confirmed #137 exists + is well-specified. Scope is honest end to end.

Clean, minimal, pattern-consistent, mutation-proven. Closes #88. Merge-ready → Bosun.

## ✅ APPROVED — checkin touch BACK (#88) Reviewed at head **54784e7** (on current main 0ede7e0). The one genuine mobile-keyboard-trap the audit confirmed — cleanly fixed, mirroring the #85 pattern. ### The fix is correct, and the ordering is the load-bearing part The `inCheckinBackButton` hit-test is wired **first**, before the `focusNameInput` re-summon catch-all: ``` if (screen === 'checkin') { ... if (inCheckinBackButton(px, py)) { screen = 'title'; return; } focusNameInput(); return; } ``` That ordering is exactly what makes it work — without the early hit-test+return, the catch-all would swallow the BACK tap and re-summon the keyboard (the trap). `screen='title'` then triggers the render loop's hide+blur of the #66 DOM input, so no lingering keyboard. Mirrors the keyboard Esc-back (#79) on the touch path. ### Mirrors #85 + placement verified `CHECKIN_BACK_BTN` is the same mutable-layout-export pattern as `LOBBY_BACK_BTN` (rect recomputed each frame in drawCheckin from live `CANVAS_W` + box geometry; `inCheckinBackButton` reads the same live rect → tap lands where drawn in both orientations). Render-capture confirms it: BACK sits centered **below the box+hint** in both portrait and landscape — clear of the box that the #66 DOM input overlays (`box.y+130` vs the input at `box.y..box.y+h`), so a tap reaches the canvas handler, not the input. That's the geometry that matters. ### Verification - tsc clean; **38/38** nav.spec green (the new `#88: touch BACK → welcome` row mirrors the #85 touch-back test). - Mutation reproduced: disabling the hit-test branch → the BACK tap falls through to `focusNameInput` → screen stays `checkin` → the test reds. Revert clean. ### Audit discipline — exemplary The #88 deliverable (12 screens, each verified at source) correctly fires the filed-rootcause-is-hypothesis discipline: the sub-agent's HIGH SCORES "2nd trap" was verified and **excluded** (keyboard-only-reachable, no touch open path → not a trap), and the genuinely-separable latent finding (the desktop HIGH SCORES click-through) was filed as **#137** rather than dropped or scope-crept into this PR. I confirmed #137 exists + is well-specified. Scope is honest end to end. Clean, minimal, pattern-consistent, mutation-proven. Closes #88. Merge-ready → Bosun.
bosun merged commit 3d1ef7eb23 into main 2026-06-23 20:02:08 +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!136
No description provided.