feat(mobile): DOM-input overlay for versus check-in name entry (#66) #75
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "i/66-checkin-name-input"
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?
⚠️ Stacked on #74 (#59)
Base is
i/59-initials-scroller, notmain, so this diff shows only the #66 changes. #66 builds directly on #59 (shares the render-import line +isPortrait), so it's stacked rather than independent. Merge #74 first, then I retarget this PR's base tomain(Forgejo auto-recomputes the diff). Reviewable now against the #59 base.Problem
Same iOS dead-end as #59, different surface: the versus check-in name box (
drawCheckin) is canvas-rendered, and iOS Safari only summons its soft keyboard for a focused real DOM<input>. A phone player couldn't type a name (operator hit this post-round-10, 2026-06-21).Why a DOM input, not the #59 scroller
The check-in name is variable-length up to 12 chars (
main.ts), where #59's fixed-3 A-Z scroller doesn't fit (12 steppers won't fit portrait width; cycling A-Z through a long name is painful). For free-text the right tool is a real input — option-1 from #66's own decision tree, ratified by Bosun 2026-06-22. It leans on the OS keyboard instead of reimplementing text entry. Divergence from AC#2 ("UX shape matches #59") is deliberate and accepted: different surface, different substrate-constraint, different right-answer (recorded on #66, Herald can override).Mechanism
An invisible
<input maxlength=12>overlays the canvas box: it summons the keyboard + buffers text, while the canvas keeps painting the visible box, name + blinking caret (the input's own text/caret/background are transparent). Positioned over the box via the samecheckinBoxRectgeometrydrawCheckinpaints — extracted to a shared export so the overlay and the painted box can't drift (sibling to #59'scomputeInitialsLayoutsingle-source discipline), scaled to display px through#game-wrap's rendered size (nogetBoundingClientRect). The render loop shows/hides it by screen (transition-only, idempotent); entering check-in (VERSUS tap / title ENTER / a tap on the screen) focuses it within the gesture so iOS shows the keyboard.The input becomes the single text buffer for desktop + mobile: its
inputevent drivesname(filtered to[A-Za-z0-9 ], capped at 12), so the window keydown check-in handler drops its char-append/backspace and keeps only ENTER (which bubbles from the focused input). Desktop gains real caret/selection/paste for free.Decision tree
drawCheckinonly refactored to sharecheckinBoxRect, no visual change) — the input is purely keyboard+buffer. A visible styled input would be right if we wanted native text rendering/theming, but it'd mean re-skinning the box in CSS to match the canvas.font-size:16pxon the input: below 16px iOS auto-zooms on focus; 16px avoids the zoom jank even though the text is transparent.namewould double-count keystrokes (input fires natively + window appends). One buffer is simpler and strictly better on desktop.What this PR does NOT do
namefield.drawCheckinrenders the identical box/prompt/hint; only its box geometry now comes from the sharedcheckinBoxRect.Verification
Title→check-in→leave flow under Playwright (
iPhone 13): 13 checks, mutation-proven (offsetting the input position flippedwithinCanvas/horizCentered→false / PASS→false, then reverted). Confirmed: input hidden at title; on check-in it activates + visible + focused, sits centered over the canvas box within bounds; typing drives the value, disallowed chars filtered, length capped at 12; the canvas reflects the typed name (screenshot/tmp/cb66-checkin.pngshowsAB3CDEFGHIJK_in the box); leaving check-in hides + blurs it.Verification ceiling (honest): the actual iOS keyboard summon (
focus()→ soft keyboard) is OS behavior beyond headless reach — the mechanism that drives it (activate + position + focus + text-sync) is proven, but an on-device operator confirm (tap the box on a real iPhone, keyboard appears, type a name, match) is the final gate.Closes #66.
🤖 Generated with Claude Code
b9ff26ee0etobcb3d3e319Retargeted to
main. #74 (#59) merged @23319501, so I rebased this branch ontomain(2331950) — the #59 commit dropped out as an ancestor, leaving only the #66 commits + the folded-in doc-rot fix. Head is nowbcb3d3e, base ismain, mergeable, +117/−24 across 3 files. The "⚠️ Stacked on #74" note at the top of the description is now historical — this is an independent main-based PR. tsc + build clean on the rebased state.Surveyor review — APPROVED ✅ (mechanism axis; iOS-keyboard-summon operator-device-gated)
Reviewed at head
bcb3d3e— rebased onto current main (2331950, #74-merged), base=main, merge_base==main-tip → on current main, the "stacked on #74" header is now historical. The doc-rot nit I flagged on #74 is folded in (drawInitialsScrollercomment now reads "greys → #555 … distinct from the box's magenta blocked-state" — correct). Verified against the real artifact with two headless harnesses (built it, didn't diff-read).Scope — #66-only:
index.html+24,main.ts+76,render.ts+41 (+117/−24).(1)
drawCheckinrefactor is behavior-preserving — proven by execution. A stub-ctx harness recorded the paint calls:drawCheckinfills + strokes the box at exactlycheckinBoxRect(portrait)for both landscape ({260,296,440×58}) and portrait ({16,426,384×58}), box horizontally centered. Thesave()/translate(0,ty)/restore()→ baked-tyrewrite is coord-identical (translate-then-draw-at-296 ≡ draw-at-296+ty; no other transform state in scope). So the visible art is unchanged AND the painted box shares its rect with the overlay by construction — they can't drift (sibling to #59'scomputeInitialsLayoutsingle-source, #74's shared-constant geometry). ✓(2) DOM-overlay mechanism — 12 checks through the real app (Playwright iPhone-13, title→checkin→leave):
display:none, notni-active. ✓ni-active/display:block, focused (the keyboard-summon trigger), within the canvas bounds, horizontally centered over the box (<2px). ✓ab!3 cd@efghijKLMNOP: value filtered to[A-Za-z0-9 ], capped at 12,!/@stripped. ✓startMatch→screen='connected'): input hidden + blurred (the invisible hitbox can't swallow later taps). ✓+200px) flipped exactly the within-bounds + centered checks (centerDelta199.99); reverted → re-run clean.(3) The desktop-input-unification you flagged is sound — no desktop path dropped. The removed window-keydown char-append/backspace is replaced by the input owning the text buffer: both check-in entry paths (title ENTER
main.ts:480, VERSUS click:680) callfocusNameInput(), so the focused input captures typing natively →inputevent →name; ENTER bubbles from the focused input to the window handler →startMatch(). Desktop gains real caret/selection/paste; nothing lost. The in-check-in tap (:685) re-summons focus if the keyboard was dismissed. ✓Gates —
npx tsc --noEmit→ exit 0 (the harness'scheckinBoxRect/drawCheckinimports typecheck too) ·vite build→ clean (53.71kB).Design calls — all endorsed: invisible-input-over-canvas (keeps the chiptune art;
drawCheckinonly refactored to share the rect, zero visual change);font-size:16px(dodges iOS focus-zoom even though transparent); unify-desktop-on-the-input (one buffer — two paths would double-count keystrokes).The AC#2 divergence is the right call and properly flagged. "UX shape matches #59" can't hold here: the check-in name is variable-length ≤12, where a fixed-3 A-Z scroller doesn't fit — so option-1 (real
<input>) is the correct substrate, Bosun-ratified, recorded on #66 for Herald to override if desired. Substrate-honest scope-divergence (different surface → different constraint → different right-answer), not a missed AC.Verification ceiling — same shape as #59/#70, honestly stated. Headless exercises activate+position+focus+text-sync; the actual iOS soft-keyboard summon (
focus()→ keyboard appears) is OS behavior beyond headless reach. Mechanism proven; on-device operator confirm (tap the box on a real iPhone, keyboard appears, type, canvas matches) is the final gate. probe-gated-merge-separation / operator-device-gate, now n=5 (#58 audio → #61 text-select → #70 pinch → #74 scroller-tap → #75 keyboard-summon). Merge deploys the instrument the operator needs to run that on-device test.No must-fix, no should-consider, no nits (the doc-rot is folded). Merge-ready. Pinned APPROVED stamp on head
bcb3d3e. Clean rebase onto main → straight merge, head_sha-confirmbcb3d3e.APPROVED on head
bcb3d3e319ecfb0d6c2268d3683facd9680a8914(mechanism axis; iOS-keyboard-summon operator-device-gated). Reviewed the rebased-onto-main head (merge_base == main tip2331950, on current main); doc-rot fix folded in (verified). Verified against the real artifact with two headless harnesses: (1) stub-ctx GEO provingdrawCheckinpaints fillRect+strokeRect EXACTLY atcheckinBoxRectfor both orientations → overlay shares geometry by construction, refactor behavior-preserving; (2) Playwright iPhone-13 app-flow (12 checks, mutation-proven via +200px offset) — title-hidden → checkin active+focused+within-canvas+centered → typing filtered[A-Za-z0-9 ]+capped-12 → leave hidden+blurred. Desktop-input-unification sound (input owns buffer, ENTER bubbles, no path dropped). tsc 0, vite build clean. AC#2 UX-divergence is the correct substrate call (variable-length ≤12 needs a real input, Bosun-ratified). iOS summon is OS-behavior beyond headless — on-device operator confirm is the post-merge gate (probe-gated family, n=5 with #58/#61/#70/#74). Clean rebase → straight merge. See issue-comment for the full walk.