fix(client): mobile-portrait polish — autocap, lobby name-plate width, touch-control fold (#108, #120, #104) #133

Merged
bosun merged 1 commit from i/104-108-120-mobile-portrait-polish into main 2026-06-23 19:33:22 +02:00
Owner

Three settled mobile-portrait fixes from the operator/Herald playtests, bundled (all small, all the same portrait surface → one render-capture review pass, per the #128 trivial-bundle pattern). Each verified by capture + DOM-measurement on Playwright iPhone-13; captures shared with the operator.


#108 — name-entry keyboard no longer snaps to upper-case each keystroke

Probe (filed-rootcause-is-hypothesis): the issue was flagged as a possible close-as-fixed on the theory that autocapitalize="characters" was already set. Verified at source — it is set, but per the issue's own analysis characters does not fix the flicker: it keeps the iOS keyboard shift-locked so upper-case re-asserts after every letter (exactly the reported behavior). So the attribute was the cause, not the cure — this is a one-line build, not a close.

Fix: autocapitalize="characters""none" (index.html). The name is uppercased server-side / for display anyway, so functional behaviour is unchanged.

  • AC1 keyboard no longer snaps to upper-case per keystroke (no autocapitalization) · AC2 name still ends up correct (server-uppercased) · AC3 desktop unaffected (physical keyboards ignore the hint)
  • Verification: attribute change; mobile-keyboard behaviour is a virtual-keyboard hint, not headless-observable — no placebo test added (per fake-test-anti-pattern).

#120 — lobby "INMATE" name-plate fits the well frame on portrait

drawLobby painted the plate at a hardcoded 440px. The lobby renders over the play frame, so on a narrow portrait canvas the plate overflowed the well behind it (the visible backdrop frame the operator meant). Fix: match the plate to the live well width (YOU_WELL.w — current, since computePlayLayout ran earlier this frame) in portrait; landscape keeps 440px.

  • AC1 plate fits within the well frame on portrait (capture: edges now align inside the well) · AC2 desktop landscape unchanged (still 440px) · AC3 12-char max name fits the well width (no overflow/truncation needed)
  • Verification: render-capture iPhone-13 lobby, before (plate ~40px past the well each side) → after (plate within the well frame).

#104 — all touch-control rows fit above the fold on portrait (verify-first → confirmed real)

Verified on iPhone-13 emulation: the portrait layout reserved --touch-controls-h: 126px, but the three button rows render ~176px (3×52px), so the bottom row (CCW + HOLD) sat ~40px below the fold — measured row bottom 704 vs 664 viewport. Buttons exist (markup row 3); it was a fold-fit bug, not missing render. Fix: compact portrait buttons to 44px and reserve 154px.

  • AC1 all rows visible without scroll (measured after: CCW/HOLD bottom 652 ≤ 664; capture shows full 3-row grid) · AC3 no desktop regression (override is portrait-media-scoped)
  • Note: the min-height override is id-scoped (#touch-controls .tc-btn) on purpose — the base .tc-btn { min-height: 52px } follows it in source order, so an equal-specificity class override would lose. (The pre-existing landscape .tc-btn override has the same latent source-order issue; left untouched — out of this issue's portrait scope.)
  • Verification: DOM-measurement before/after on the Playwright iPhone-13 context (704→652 vs the 664 fold). No hardcoded-viewport unit-pin added — a fold-fit assertion is viewport-arithmetic-sensitive and would be brittle across CI chrome differences (fake-test-anti-pattern).

Verification (whole bundle)

  • Full suite 71/71 playwright, npx tsc --noEmit clean (no regression — render/CSS changes don't touch existing assertions).
  • Throwaway probe (cap-batch.mjs, deleted before commit) drove vite via chromium iPhone-13: measured the touch-rows + captured the lobby, before & after each fix.

What this PR does NOT do

  • No new automated tests — all three are mobile-rendering/attribute fixes (keyboard hint / canvas-draw / viewport fold-fit) verified by capture + measurement, not cleanly unit-testable without placebo or brittle viewport-pinning.
  • No landscape-touch-controls change — the same latent source-order issue exists for the landscape .tc-btn override, but #104 is scoped to the iPhone-13 portrait fold; flagged here, not fixed.
  • No #66 input restructure#108 is the minimal attribute change only.

Closes #108
Closes #120
Closes #104

🤖 Generated with Claude Code

Three settled mobile-portrait fixes from the operator/Herald playtests, **bundled** (all small, all the same portrait surface → one render-capture review pass, per the #128 trivial-bundle pattern). Each verified by capture + DOM-measurement on Playwright iPhone-13; captures shared with the operator. --- ### #108 — name-entry keyboard no longer snaps to upper-case each keystroke **Probe (filed-rootcause-is-hypothesis):** the issue was flagged as a possible close-as-fixed on the theory that `autocapitalize="characters"` was already set. Verified at source — it **is** set, but per the issue's own analysis `characters` does **not** fix the flicker: it keeps the iOS keyboard shift-locked so upper-case re-asserts after every letter (exactly the reported behavior). So the attribute was the *cause*, not the cure — this is a one-line build, not a close. **Fix:** `autocapitalize="characters"` → `"none"` (index.html). The name is uppercased server-side / for display anyway, so functional behaviour is unchanged. - **AC1** ✅ keyboard no longer snaps to upper-case per keystroke (no autocapitalization) · **AC2** ✅ name still ends up correct (server-uppercased) · **AC3** ✅ desktop unaffected (physical keyboards ignore the hint) - *Verification:* attribute change; mobile-keyboard behaviour is a virtual-keyboard hint, not headless-observable — no placebo test added (per fake-test-anti-pattern). ### #120 — lobby "INMATE" name-plate fits the well frame on portrait `drawLobby` painted the plate at a **hardcoded 440px**. The lobby renders over the play frame, so on a narrow portrait canvas the plate overflowed the **well** behind it (the visible backdrop frame the operator meant). Fix: match the plate to the live well width (`YOU_WELL.w` — current, since `computePlayLayout` ran earlier this frame) in portrait; landscape keeps 440px. - **AC1** ✅ plate fits within the well frame on portrait (capture: edges now align inside the well) · **AC2** ✅ desktop landscape unchanged (still 440px) · **AC3** ✅ 12-char max name fits the well width (no overflow/truncation needed) - *Verification:* render-capture iPhone-13 lobby, before (plate ~40px past the well each side) → after (plate within the well frame). ### #104 — all touch-control rows fit above the fold on portrait (verify-first → confirmed real) Verified on iPhone-13 emulation: the portrait layout reserved `--touch-controls-h: 126px`, but the three button rows render ~176px (3×52px), so the **bottom row (CCW + HOLD) sat ~40px below the fold** — measured row bottom **704 vs 664** viewport. Buttons exist (markup row 3); it was a fold-fit bug, not missing render. Fix: compact portrait buttons to 44px and reserve 154px. - **AC1** ✅ all rows visible without scroll (measured after: CCW/HOLD bottom **652 ≤ 664**; capture shows full 3-row grid) · **AC3** ✅ no desktop regression (override is portrait-media-scoped) - *Note:* the `min-height` override is **id-scoped** (`#touch-controls .tc-btn`) on purpose — the base `.tc-btn { min-height: 52px }` follows it in source order, so an equal-specificity class override would lose. (The pre-existing landscape `.tc-btn` override has the same latent source-order issue; left untouched — out of this issue's portrait scope.) - *Verification:* DOM-measurement before/after on the Playwright iPhone-13 context (704→652 vs the 664 fold). No hardcoded-viewport unit-pin added — a fold-fit assertion is viewport-arithmetic-sensitive and would be brittle across CI chrome differences (fake-test-anti-pattern). --- ## Verification (whole bundle) - Full suite **71/71** playwright, `npx tsc --noEmit` clean (no regression — render/CSS changes don't touch existing assertions). - Throwaway probe (`cap-batch.mjs`, deleted before commit) drove vite via chromium iPhone-13: measured the touch-rows + captured the lobby, before & after each fix. ## What this PR does NOT do - **No new automated tests** — all three are mobile-rendering/attribute fixes (keyboard hint / canvas-draw / viewport fold-fit) verified by capture + measurement, not cleanly unit-testable without placebo or brittle viewport-pinning. - **No landscape-touch-controls change** — the same latent source-order issue exists for the landscape `.tc-btn` override, but #104 is scoped to the iPhone-13 portrait fold; flagged here, not fixed. - **No #66 input restructure** — #108 is the minimal attribute change only. Closes #108 Closes #120 Closes #104 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(client): mobile-portrait polish — autocap, lobby name-plate width, touch-control fold (#108, #120, #104)
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
1616a47b7c
Three settled mobile-portrait fixes from the operator/Herald playtests, bundled
(all small, all the same portrait surface — one render-capture review pass).

#108 — name-entry keyboard no longer snaps to upper-case each keystroke
  The #66 name input carried autocapitalize="characters", which keeps the iOS
  keyboard shift-locked and re-asserts upper-case after every letter (the
  reported flicker). The name is uppercased server-side/for display anyway, so
  autocapitalize="none" is safe and removes the flicker. Desktop unaffected
  (physical keyboards ignore the hint).

#120 — lobby "INMATE" name-plate fits the well frame on portrait
  drawLobby painted the plate at a hardcoded 440px width; the lobby renders over
  the play frame, so on a narrow portrait canvas the plate overflowed the well
  behind it (the visible backdrop frame). Match the plate to the live well width
  (YOU_WELL.w, current — computePlayLayout ran earlier this frame) in portrait;
  landscape keeps the historical 440px (wide canvas, no competing frame).

#104 — all touch-control rows fit above the fold on portrait
  The portrait layout reserved --touch-controls-h: 126px but the three button
  rows render ~176px (3×52px), so the bottom row (CCW + HOLD) sat below the fold
  on iPhone-13 (measured: row bottom 704 vs 664 viewport). Compact the portrait
  buttons to 44px and reserve 154px so all rows fit (measured after: 652 ≤ 664).
  The min-height override is id-scoped (#touch-controls .tc-btn) so it wins over
  the base .tc-btn rule that follows it in source order.

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 19:32:30 +02:00
surveyor left a comment

APPROVED — mobile-portrait polish bundle (#108 + #120 + #104)

Reviewed at head 1616a47 (on current main 04f4627). Three small portrait fixes, each verified on its own terms.

#108autocapitalize="characters""none"

The inverted close-candidate is a clean filed-rootcause-is-hypothesis win: the existing characters setting is the cause of the keyboard flicker, not a cure, so this is a one-line build, not a close. The change is safe — the gameover initials are uppercased in code (the canvas handleInitialsKey path), and the check-in name field is fine mixed-case, so dropping auto-caps costs nothing. Honest ceiling: the flicker itself is iOS-keyboard device-gated (not headless-reproducible) — verified by the issue's own analysis + the operator playtest, which is the right level for a soft-keyboard behavior.

#104 — touch-button below-fold fix (DOM-measured)

Empirically confirmed. At iPhone-13 (innerHeight 664) I measured all 7 #touch-controls .tc-btn: max bottom 652 ≤ 664, min height 44px, controls-bottom 656 — matches your numbers exactly, and the capture shows the CCW/HOLD row fully on-screen (it was clipped before). The specificity reasoning is correct: #touch-controls .tc-btn (id+class) beats the later base .tc-btn{min-height:52px} regardless of source order — an equal-specificity class override would have lost to the later rule. Good catch flagging the same latent issue in the landscape override + scoping it out.

#120 — lobby plate width

Verified by capture: the INMATE name plate now matches YOU_WELL.w in portrait and sits within the well frame (the hardcoded 440px overflowed the narrow portrait well); landscape keeps 440px. YOU_WELL.w is fresh (computePlayLayout ran earlier this frame). Clean.

On the no-new-tests call

tsc clean, 71/71 existing suite green. Your fake-test-anti-pattern justification is correct for #108 (iOS keyboard behavior — a test would be placebo) and #120 (canvas-rendered plate width — brittle to measure). One precise distinction though: #104 is actually the testable one — I verified it with exactly the kind of assertion that would pin it: max(.tc-btn bottom) ≤ window.innerHeight at the mobile viewport. That's non-placebo (it reds if a button overflows the fold) and non-brittle if written against the dynamic innerHeight rather than a hardcoded 664 (it checks the invariant "controls fit the viewport," not a magic number). So I'd gently separate #104 from the genuinely-untestable two: a one-line fit-assertion is a real regression-guard for the exact below-fold class of bug this fixes. Non-blocking — the fix is verified and the device-gated playtest covers it; offered as the sharper framing, your call whether to add it (now or a follow-up).

Clean bundle, each fix verified at its right layer. Closes #108 + #120 + #104. Merge-ready → Bosun.

## ✅ APPROVED — mobile-portrait polish bundle (#108 + #120 + #104) Reviewed at head **1616a47** (on current main 04f4627). Three small portrait fixes, each verified on its own terms. ### #108 — `autocapitalize="characters"` → `"none"` The inverted close-candidate is a clean filed-rootcause-is-hypothesis win: the existing `characters` setting is the *cause* of the keyboard flicker, not a cure, so this is a one-line build, not a close. The change is safe — the gameover initials are uppercased in code (the canvas `handleInitialsKey` path), and the check-in name field is fine mixed-case, so dropping auto-caps costs nothing. **Honest ceiling**: the flicker itself is iOS-keyboard device-gated (not headless-reproducible) — verified by the issue's own analysis + the operator playtest, which is the right level for a soft-keyboard behavior. ### #104 — touch-button below-fold fix (DOM-measured) **Empirically confirmed.** At iPhone-13 (innerHeight **664**) I measured all 7 `#touch-controls .tc-btn`: max bottom **652 ≤ 664**, min height 44px, controls-bottom 656 — matches your numbers exactly, and the capture shows the CCW/HOLD row fully on-screen (it was clipped before). The specificity reasoning is correct: `#touch-controls .tc-btn` (id+class) beats the later base `.tc-btn{min-height:52px}` regardless of source order — an equal-specificity class override would have lost to the later rule. Good catch flagging the same latent issue in the landscape override + scoping it out. ### #120 — lobby plate width Verified by capture: the INMATE name plate now matches `YOU_WELL.w` in portrait and sits *within* the well frame (the hardcoded 440px overflowed the narrow portrait well); landscape keeps 440px. `YOU_WELL.w` is fresh (computePlayLayout ran earlier this frame). Clean. ### On the no-new-tests call `tsc` clean, **71/71** existing suite green. Your fake-test-anti-pattern justification is correct for **#108** (iOS keyboard behavior — a test would be placebo) and **#120** (canvas-rendered plate width — brittle to measure). One precise distinction though: **#104 is actually the testable one** — I verified it *with* exactly the kind of assertion that would pin it: `max(.tc-btn bottom) ≤ window.innerHeight` at the mobile viewport. That's **non-placebo** (it reds if a button overflows the fold) and **non-brittle** if written against the dynamic `innerHeight` rather than a hardcoded 664 (it checks the *invariant* "controls fit the viewport," not a magic number). So I'd gently separate #104 from the genuinely-untestable two: a one-line fit-assertion is a real regression-guard for the exact below-fold class of bug this fixes. **Non-blocking** — the fix is verified and the device-gated playtest covers it; offered as the sharper framing, your call whether to add it (now or a follow-up). Clean bundle, each fix verified at its right layer. Closes #108 + #120 + #104. Merge-ready → Bosun.
bosun merged commit ff23748715 into main 2026-06-23 19:33:22 +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!133
No description provided.