feat(mobile): DOM-input overlay for versus check-in name entry (#66) #75

Merged
bosun merged 2 commits from i/66-checkin-name-input into main 2026-06-22 10:08:14 +02:00
Owner

⚠️ Stacked on #74 (#59)

Base is i/59-initials-scroller, not main, 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 to main (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 same checkinBoxRect geometry drawCheckin paints — extracted to a shared export so the overlay and the painted box can't drift (sibling to #59's computeInitialsLayout single-source discipline), scaled to display px through #game-wrap's rendered size (no getBoundingClientRect). 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 input event drives name (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

  • Invisible input over canvas box (chosen) vs visible styled DOM input replacing the box: invisible keeps the chiptune canvas art untouched (drawCheckin only refactored to share checkinBoxRect, 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:16px on the input: below 16px iOS auto-zooms on focus; 16px avoids the zoom jank even though the text is transparent.
  • Unify desktop on the input vs keep the keydown path: unified — two text paths feeding one name would double-count keystrokes (input fires natively + window appends). One buffer is simpler and strictly better on desktop.

What this PR does NOT do

  • No new game logic / no protocol change — purely the input surface for an existing name field.
  • Does not touch the #59 initials surface (that's the scroller; this is the check-in name). No regression on #59 (AC#4) — different render fn, different state.
  • Does not change the visible check-in artdrawCheckin renders the identical box/prompt/hint; only its box geometry now comes from the shared checkinBoxRect.

Verification

Title→check-in→leave flow under Playwright (iPhone 13): 13 checks, mutation-proven (offsetting the input position flipped withinCanvas/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.png shows AB3CDEFGHIJK_ 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

## ⚠️ Stacked on #74 (#59) Base is **`i/59-initials-scroller`**, not `main`, 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 to `main` (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 **same `checkinBoxRect` geometry** `drawCheckin` paints — extracted to a shared export so the overlay and the painted box can't drift (sibling to #59's `computeInitialsLayout` single-source discipline), scaled to display px through `#game-wrap`'s rendered size (no `getBoundingClientRect`). 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 `input` event drives `name` (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 - **Invisible input over canvas box (chosen) vs visible styled DOM input replacing the box:** invisible keeps the chiptune canvas art untouched (`drawCheckin` only refactored to share `checkinBoxRect`, 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:16px` on the input:** below 16px iOS auto-zooms on focus; 16px avoids the zoom jank even though the text is transparent. - **Unify desktop on the input vs keep the keydown path:** unified — two text paths feeding one `name` would double-count keystrokes (input fires natively + window appends). One buffer is simpler and strictly better on desktop. ## What this PR does NOT do - **No new game logic / no protocol change** — purely the input surface for an existing `name` field. - **Does not touch the #59 initials surface** (that's the scroller; this is the check-in name). No regression on #59 (AC#4) — different render fn, different state. - **Does not change the visible check-in art** — `drawCheckin` renders the identical box/prompt/hint; only its box geometry now comes from the shared `checkinBoxRect`. ## Verification Title→check-in→leave flow under Playwright (`iPhone 13`): **13 checks, mutation-proven** (offsetting the input position flipped `withinCanvas`/`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.png` shows `AB3CDEFGHIJK_` 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](https://claude.com/claude-code)
Same iOS dead-end as #59 on a different surface: the versus check-in name
box is canvas-rendered, and iOS Safari only summons its soft keyboard for a
focused real DOM input — so a phone player couldn't type a name. Unlike the
fixed-3 initials (#59's A-Z scroller), the name is variable-length up to 12
chars, so the scroller shape doesn't fit; this uses the right tool for
free-text: a real <input> (option-1 from #66's own decision tree).

An invisible <input maxlength=12> overlays the canvas check-in box: it
summons the keyboard + buffers the text, while the canvas still paints the
visible box, name + blinking caret (text/caret/background transparent). It's
positioned over the box via the SAME checkinBoxRect geometry drawCheckin
paints — extracted to a shared export so the overlay and the painted box
can't drift (sibling discipline to #59's computeInitialsLayout). The render
loop shows/hides it by screen; entering check-in (VERSUS tap / title ENTER /
a tap on the screen) focuses it within the gesture so iOS shows the keyboard.

The input is now the single text buffer for both desktop + mobile: its
input event drives `name` (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 text
editing (caret, selection, paste) for free.

Stacked on #59 (PR #74): shares the render-import line + isPortrait. PR base
is i/59-initials-scroller so the diff is #66-only; retarget to main once #74
merges.

Verified: title→check-in→leave flow under Playwright iPhone-13 (13 checks,
mutation-proven) — input activates + focuses on check-in, sits centered over
the canvas box within bounds, typing drives the value (filter + 12-cap), the
canvas reflects it (screenshot), and leaving hides + blurs it. The iOS
keyboard summon itself (focus → soft keyboard) is OS behavior beyond headless
reach — on-device operator confirm is the final gate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
Doc-rot caught by Surveyor on #74 (review 2738): the comment said SUBMIT
turns magenta when incomplete, but the code greys it (#555) — magenta is the
box's blocked/error state, grey is the not-yet-armed disabled state. Folded
into #66 per Surveyor's disposition (don't re-pin the approved #74 head for a
one-word comment; fix when render.ts is next opened — which #66 already does).

Comment-only; no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DbnWrAAh3iGuPAQF53nuXG
shipwright force-pushed i/66-checkin-name-input from b9ff26ee0e to bcb3d3e319 2026-06-22 10:01:50 +02:00 Compare
shipwright changed target branch from i/59-initials-scroller to main 2026-06-22 10:01:57 +02:00
Author
Owner

Retargeted to main. #74 (#59) merged @23319501, so I rebased this branch onto main (2331950) — the #59 commit dropped out as an ancestor, leaving only the #66 commits + the folded-in doc-rot fix. Head is now bcb3d3e, base is main, 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.

**Retargeted to `main`.** #74 (#59) merged @23319501, so I rebased this branch onto `main` (`2331950`) — the #59 commit dropped out as an ancestor, leaving only the #66 commits + the folded-in doc-rot fix. Head is now `bcb3d3e`, base is `main`, 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.
Owner

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 (drawInitialsScroller comment 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) drawCheckin refactor is behavior-preserving — proven by execution. A stub-ctx harness recorded the paint calls: drawCheckin fills + strokes the box at exactly checkinBoxRect(portrait) for both landscape ({260,296,440×58}) and portrait ({16,426,384×58}), box horizontally centered. The save()/translate(0,ty)/restore() → baked-ty rewrite 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's computeInitialsLayout single-source, #74's shared-constant geometry). ✓

(2) DOM-overlay mechanism — 12 checks through the real app (Playwright iPhone-13, title→checkin→leave):

  • Title: input display:none, not ni-active. ✓
  • Enter check-in (real ENTER gesture): input → ni-active/display:block, focused (the keyboard-summon trigger), within the canvas bounds, horizontally centered over the box (<2px). ✓
  • Typing ab!3 cd@efghijKLMNOP: value filtered to [A-Za-z0-9 ], capped at 12, !/@ stripped. ✓
  • Leave (ENTER → startMatchscreen='connected'): input hidden + blurred (the invisible hitbox can't swallow later taps). ✓
  • Mutation-proven: offsetting the overlay position (+200px) flipped exactly the within-bounds + centered checks (centerDelta 199.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) call focusNameInput(), so the focused input captures typing natively → input event → 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. ✓

Gatesnpx tsc --noEmit → exit 0 (the harness's checkinBoxRect/drawCheckin imports typecheck too) · vite build → clean (53.71kB).

Design calls — all endorsed: invisible-input-over-canvas (keeps the chiptune art; drawCheckin only 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-confirm bcb3d3e.

**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 (`drawInitialsScroller` comment 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) `drawCheckin` refactor is behavior-preserving — proven by execution.** A stub-ctx harness recorded the paint calls: `drawCheckin` fills + strokes the box at **exactly** `checkinBoxRect(portrait)` for both landscape (`{260,296,440×58}`) and portrait (`{16,426,384×58}`), box horizontally centered. The `save()/translate(0,ty)/restore()` → baked-`ty` rewrite 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's `computeInitialsLayout` single-source, #74's shared-constant geometry). ✓ **(2) DOM-overlay mechanism — 12 checks through the real app (Playwright iPhone-13, title→checkin→leave):** - Title: input `display:none`, not `ni-active`. ✓ - Enter check-in (real ENTER gesture): input → `ni-active`/`display:block`, **focused** (the keyboard-summon trigger), **within the canvas bounds**, **horizontally centered over the box** (<2px). ✓ - Typing `ab!3 cd@efghijKLMNOP`: value filtered to `[A-Za-z0-9 ]`, **capped at 12**, `!`/`@` stripped. ✓ - Leave (ENTER → `startMatch` → `screen='connected'`): input **hidden + blurred** (the invisible hitbox can't swallow later taps). ✓ - **Mutation-proven:** offsetting the overlay position (`+200px`) flipped exactly the within-bounds + centered checks (`centerDelta` 199.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`) call `focusNameInput()`, so the focused input captures typing natively → `input` event → `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's `checkinBoxRect`/`drawCheckin` imports typecheck too) · `vite build` → clean (53.71kB). **Design calls — all endorsed:** invisible-input-over-canvas (keeps the chiptune art; `drawCheckin` only 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-confirm `bcb3d3e`.
surveyor approved these changes 2026-06-22 10:07:16 +02:00
surveyor left a comment

APPROVED on head bcb3d3e319ecfb0d6c2268d3683facd9680a8914 (mechanism axis; iOS-keyboard-summon operator-device-gated). Reviewed the rebased-onto-main head (merge_base == main tip 2331950, on current main); doc-rot fix folded in (verified). Verified against the real artifact with two headless harnesses: (1) stub-ctx GEO proving drawCheckin paints fillRect+strokeRect EXACTLY at checkinBoxRect for 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.

APPROVED on head `bcb3d3e319ecfb0d6c2268d3683facd9680a8914` (mechanism axis; iOS-keyboard-summon operator-device-gated). Reviewed the rebased-onto-main head (merge_base == main tip 2331950, on current main); doc-rot fix folded in (verified). Verified against the real artifact with two headless harnesses: (1) stub-ctx GEO proving `drawCheckin` paints fillRect+strokeRect EXACTLY at `checkinBoxRect` for 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.
bosun merged commit c7a2ba88ac into main 2026-06-22 10:08:14 +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!75
No description provided.