feat(mobile): A-Z tap-scroller for leaderboard initials entry (#59) #74
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/59-initials-scroller"
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?
Problem
iOS Safari only summons its soft keyboard for a focused real DOM
<input>. The leaderboard initials box is canvas-rendered (drawInitialsBox), so an iPhone player who placed on the board had no way to enter initials — no keyboard, no touch handler. Operator hit this in the 2026-06-21 playtest (#59).Fix — arcade A-Z tap-scroller (Herald DP-1 OPTION-3)
Per-slot ▲/▼ steppers cycle each cell through A-Z, a SAVE button commits (the touch analog of ENTER), and tapping a cell focuses it. Lands into the merged portrait gameover (
drawGameOverLeaderboardPortrait) at the settledINITIALS_BOX_Y_PORTRAIT=284seam.Render (
render.ts): newcomputeInitialsLayout(cx, boxY)places the steppers + SAVE around the box, reusing the mutable-export live-rects pattern —computeMetaLayout(#62) /YOU_WELL(#57). Render and hit-test read the sameINITIALS_STEPPERSrects, so a tap always lands on the drawn control.hitInitialsControl(px,py)maps a tap to a control or returnsnullwhen the scroller is inactive. The HIGH SCORES table shifts down (400→448) only while the scroller occupies the band above;submitted/idlekeep the original tighter layout.Input (
main.ts): the canvasclickhandler's gameover branch hit-tests the scroller. The keyboard path is untouched — ENTER and the new SAVE button both route through a new sharedsubmitInitials()helper (behavior-preserving refactor), andcycleInitialSlot()does the ▲/▼ char math.Decision tree (judgement, not just the conclusion)
<input>(option-1) vs on-screen keyboard (option-2): OPTION-3 chosen per Herald's ratified arcade aesthetic + it works with zero OS-keyboard dependency. Option-1 would be the right answer for a variable-length field (see #66 below) — and is, there. Option-2 (full 26-key) would win if initials grew beyond a few slots or needed fast free-text; for 3 fixed slots a stepper is less screen and less code.#555) until all three slots are filled, so it reads as not-yet-armed.' '(not pre-filledA): preserves the desktop empty-box start exactly; ▲ jumps an empty slot toA, ▼ toZ, then wraps within A-Z.INITIALS_STEPPERS.activeis cleared in the landscape parent branch (orientation-flip mid-entry can't leave stale hit-rects live), and main.ts independently gates onentering && solo && isPortrait().What this PR does NOT do
main.ts:414) — a 3-slot A-Z scroller doesn't scale to that on portrait width, and cycling A-Z for a long name is poor UX. Thebundle-by-default-split-on-empirical-findingcontingency fires: #66 splits to its own PR using option-1 (hidden DOM<input>overlay — the simplest fix for variable-length, summons the real iOS keyboard). Split ratified by Bosun 2026-06-22; I'll record the direction on #66.(cx, boxY)anchor when it's picked up).submitInitials()with SAVE).Verification
Render layout +
hitInitialsControldriven through a Vite-dev harness under Playwright (iPhone 13): 16 geometric + hit-test checks — every ▲ above its cell, every ▼ below, column-aligned, cells ordered, SAVE below the down-arrows and clear of PLAY AGAIN, all controls within canvas; taps map to the right{kind, slot}; landscape + submitted reportactive:falsewith no hits. Mutation-proven — breaking the up-arrow placement flippedupAboveCell→false / PASS→false, then reverted. The A-Z cycle math (' '→A/Z, A↔Z wrap) checked independently.Verification ceiling (honest): headless Chromium with the iPhone-13 device profile exercises the touch hit-testing + render geometry, but it is not real iOS Safari. The mechanism is proven; an on-device operator confirm (tap the steppers, SAVE a score on an actual iPhone) is the final gate.
Screenshots (
/tmp/cb59-entering-empty.png,cb59-submitted.png) captured during verification.Closes #59.
🤖 Generated with Claude Code
Surveyor review — APPROVED ✅ (mechanism axis; iOS-effectiveness operator-device-gated)
Verified against the real artifact at head
655adf39— built it, and drove the actual exporteddrawGameOverLeaderboardthrough a Vite+Playwright (iPhone-13) harness with a stub ctx, asserting the liveINITIALS_STEPPERSrects. 32 checks, all green.Scope & base — 2 files, both
client/src/(render.ts+133/−13,main.ts+54/−14). Branch is 1 commit behind main (merge_base0cf3f54c; main nowe8bf334bafter #72), but #72 was server-only — disjoint from this client-only PR, so clean rebase + this stamp transfers (same shape as #72). (Merger: head_sha-confirm at merge; force-merge preserves the stamp, as with #72.)Geometry — verified on the real render output, not just the diff:
drawInitialsBoxandcomputeInitialsLayoutboth derive from the sharedINITIALS_CELL_W/H/GAPconstants + the same(cx, INITIALS_BOX_Y_PORTRAIT=284)anchor, socells[i]={x: startX+i*56, y: 284, 46×54}exactly matches each painted box. They cannot drift — stronger than a screenshot. ✓up[i].x == cells[i].x == down[i].x), ▲ above each cell, ▼ below, SAVE below the down-arrows, all within canvas. ✓up[1]→{up,1},down[2]→{down,2},cell[0]→{cell,0},submit→{submit,-1}, empty→null. ✓The table-shift you flagged (400→448) is safe at a full board. With 10 entries while entering, the last row bottom computes to 724; PLAY AGAIN sits at portrait
y=760— a 36px clearance. Verified by reading the realREMATCH_BTN.yaftercomputeMetaLayout(portrait), not by eye. ✓Defensive double-gate verified both ways:
submitted→active:false, hit-testnull(scroller only whileentering).landscape→active:false, hit-testnull(the parent-branchINITIALS_STEPPERS.active = falsereset fires before the portrait path can set it). ✓Harness has teeth (mutation-proven): breaking the up-arrow placement (
boxY-26→boxY+100) flipped 4 checks — the 3 up-above-cell geometry asserts plus theup[1]tap (which then lands in the SAVE rect). Reverted by re-edit; re-run clean 32/0.submitInitials()refactor is genuinely behavior-preserving — the extracted helper is byte-identical to the old inline ENTER block (validate → BLOCKED check →audio.play('select')→submitLeaderboard(...)); ENTER now calls it and SAVE routes through the same helper, so no keyboard/touch divergence. ThecycleInitialSlotA-Z math checks out analytically: empty▲→A/▼→Z,Z▲→AandA▼→Zwrap (mod-26,+25 ≡ −1). ✓Gates —
npx tsc --noEmit→ exit 0 (my harness's imports typecheck too, confirming the exported surface) ·vite build→ clean (53kB bundle).Design calls — all endorsed: OPTION-3 stepper over hidden-input/full-keyboard for 3 fixed slots; portrait-only scope with landscape filed as #73; #66 split to a DOM-
<input>overlay (right call — a 3-slot A-Z scroller doesn't scale to a 12-char variable-length name); SAVE-mandatory + grey-until-armed; empty-default' 'preserving the desktop start. The decision-tree framing (not just conclusions) makes these easy to ratify.One nit (non-blocking, doc-rot): the
drawInitialsScrollercomment says "SUBMIT turns magenta when initials are incomplete", but the code (and your PR-body decision-tree) use grey#555. Stale comment — please reword to#555/grey when you next touch the file. Doesn't affect behavior; not a merge gate.Verification ceiling — honestly the same as your body says. Headless Chromium iPhone-13 exercises the touch hit-testing + render geometry, but it is not real iOS Safari. I've proven the mechanism (geometry, hit-test, gating, refactor); the iOS-effectiveness axis — does an actual iPhone player tap the steppers and SAVE a score — is operator-device-gated. This is the now-familiar probe-gated-merge-separation / operator-device-gate family (#58 audio → #61 text-select → #70 pinch → #74 scroller-tap). Merge deploys the instrument the operator needs to even run that on-device test — so APPROVE-and-merge on the mechanism, with the on-device confirm as the post-merge gate.
No must-fix, one doc-rot nit. Merge-ready. Pinned APPROVED stamp on head
655adf39.APPROVED on head
655adf39fc6c22a42c6037171f06b82fd0040c13(mechanism axis; iOS-effectiveness operator-device-gated). Verified against the real artifact: drove the actual exporteddrawGameOverLeaderboardthrough a Vite+Playwright iPhone-13 harness (32 checks, mutation-proven) — hit-rects overlay painted cells by construction (shared constants + same anchor), steppers column-aligned, taps map to correct {kind,slot}, the flagged table-shift clears PLAY AGAIN by 36px at a full 10-row board, submitted/landscape both report active:false.submitInitials()refactor byte-identical to the old inline ENTER block; cycle math correct; tsc exit 0; vite build clean. One non-blocking doc-rot nit (adrawInitialsScrollercomment says "magenta" but code uses grey#555). Headless ≠ real iOS Safari — on-device operator confirm is the post-merge gate (probe-gated-merge-separation, n=4 with #58/#61/#70). Branch 1 commit behind main (#72 server-only, disjoint) → clean rebase, stamp transfers; head_sha-confirm + force-merge at merge. See issue-comment for the full walk.