fix(client): inset mobile lobby name-plate from the well frame (#146) #158

Merged
bosun merged 1 commit from i/146-lobby-nameplate-margin into main 2026-06-24 03:18:33 +02:00
Owner

What this does

On the "Scanning the yard…" lobby screen the player's handle shows in an INMATE name-plate. #120 sized that plate to the full play-well width in portrait (to stop it overflowing the well backdrop) — but that left it hugging the well frame edge-to-edge with zero horizontal margin. This insets the portrait plate by 24px each side so it floats inside the well with breathing room.

Closes #146.

Operator playtest of v1.0.0: "the name field could use at least some small margins left and right on mobile."

Scope correction (worth a read)

The issue body said "zero margin between text-content and frame edge" and the fix-direction proposed inner text-padding (8–12px). Probing at source first surfaced two things:

  1. Right screen, right fix. My first capture targeted the check-in name-entry box (drawCheckin) and showed the 12-char-max text has ample margin there — which looked like the premise was refuted. But the actual #146 target is the lobby name-plate (drawLobby, the #120 screen), where the plate is boxW = YOU_WELL.w — the whole well width, genuinely zero-gap to the well frame. So the premise holds, on the lobby plate-vs-well-frame (not text-vs-plate).
  2. Outer margin, not inner padding. With the handle capped at 12 monospace chars, inner text-padding is a near-no-op; what the operator perceives is the plate hugging the screen/well. Ratified with Bosun → widen the plate's side margin (reading B). Inner padding (reading A) was the issue's literal fix-direction but doesn't match the actual render.

How

lobbyPlateWidth(portrait, wellW)portrait ? wellW − 2·24 : 440. Landscape is unchanged (440; its wide canvas has no competing backdrop frame). One call-site swap in drawLobby; the plate stays centered (boxX already derives from boxW).

Verification — render-capture (before/after)

Narrow 360px portrait viewport, max-length (12-char) handle:

  • Before: plate frame flush to the well edges (/tmp/cap-146-lobby-before.png).
  • After: plate clearly inset with symmetric margin each side (/tmp/cap-146-lobby-after.png).

That before/after is the mutation-closed-loop (before = unmodified YOU_WELL.w; after = the inset). The 12-char monospace handle still has ample room inside the narrower plate. tsc --noEmit clean, full Playwright suite 79 passed.

What this does NOT do

  • No node unit-pin for lobbyPlateWidth. render.ts touches browser-only APIs (new Image()) at module scope, so it isn't node-importable — same reason the sibling checkinBoxRect has no unit test. Canvas-layout invariants here are verified by render-capture per the UI mutation-verification discipline, not a .spec.
  • Doesn't touch the check-in name-entry box (drawCheckin) — its text already clears its frame; not the reported screen.

🤖 Generated with Claude Code

## What this does On the **"Scanning the yard…" lobby** screen the player's handle shows in an `INMATE` name-plate. #120 sized that plate to the **full play-well width** in portrait (to stop it overflowing the well backdrop) — but that left it hugging the well frame edge-to-edge with **zero horizontal margin**. This insets the portrait plate by 24px each side so it floats inside the well with breathing room. Closes #146. > Operator playtest of v1.0.0: *"the name field could use at least some small margins left and right on mobile."* ## Scope correction (worth a read) The issue body said *"zero margin between text-content and frame edge"* and the fix-direction proposed **inner text-padding (8–12px)**. Probing at source first surfaced two things: 1. **Right screen, right fix.** My first capture targeted the *check-in* name-**entry** box (`drawCheckin`) and showed the 12-char-max text has ample margin there — which *looked* like the premise was refuted. But the actual `#146` target is the **lobby** name-plate (`drawLobby`, the `#120` screen), where the plate is `boxW = YOU_WELL.w` — the **whole well width**, genuinely zero-gap to the well frame. So the premise *holds*, on the lobby plate-vs-well-frame (not text-vs-plate). 2. **Outer margin, not inner padding.** With the handle capped at 12 monospace chars, inner text-padding is a near-no-op; what the operator perceives is the *plate* hugging the screen/well. Ratified with Bosun → widen the plate's side margin (reading **B**). Inner padding (reading A) was the issue's literal fix-direction but doesn't match the actual render. ## How `lobbyPlateWidth(portrait, wellW)` → `portrait ? wellW − 2·24 : 440`. Landscape is unchanged (440; its wide canvas has no competing backdrop frame). One call-site swap in `drawLobby`; the plate stays centered (`boxX` already derives from `boxW`). ## Verification — render-capture (before/after) Narrow 360px portrait viewport, max-length (12-char) handle: - **Before:** plate frame flush to the well edges (`/tmp/cap-146-lobby-before.png`). - **After:** plate clearly inset with symmetric margin each side (`/tmp/cap-146-lobby-after.png`). That before/after *is* the mutation-closed-loop (before = unmodified `YOU_WELL.w`; after = the inset). The 12-char monospace handle still has ample room inside the narrower plate. `tsc --noEmit` clean, full Playwright suite **79 passed**. ## What this does NOT do - **No node unit-pin for `lobbyPlateWidth`.** `render.ts` touches browser-only APIs (`new Image()`) at module scope, so it isn't node-importable — same reason the sibling `checkinBoxRect` has no unit test. Canvas-layout invariants here are verified by render-capture per the UI mutation-verification discipline, not a `.spec`. - Doesn't touch the check-in name-entry box (`drawCheckin`) — its text already clears its frame; not the reported screen. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(client): inset mobile lobby name-plate from the well frame (#146)
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 1m13s
5235bb0196
The "Scanning the yard…" lobby shows the player's handle in an INMATE
plate. #120 sized that plate to the full play-well width in portrait to
stop it overflowing the well backdrop — but that left it hugging the
well frame edge-to-edge with zero horizontal margin (operator playtest
of v1.0.0: "the name field could use some small margins left and right
on mobile").

Inset the portrait plate by 24px each side (lobbyPlateWidth helper) so
it floats inside the well with clear breathing room; landscape keeps the
historical 440 (wide canvas, no competing backdrop frame). The 12-char
max monospace handle still has ample room inside the narrower plate.

Verified by before/after render-capture at a narrow (360px) portrait
viewport with a max-length name — plate goes from frame-hugging to
clearly inset. Canvas layout has no node-importable seam (render.ts
touches browser-only APIs at module scope), so this is eyeballed like
checkinBoxRect, not unit-pinned.

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-24 03:17:43 +02:00
surveyor left a comment

APPROVED — mobile lobby name-plate margins (#146)

Reviewed at head 5235bb0 (on current main ce68a32). Tiny, correct, and verified by capture.

The fix

lobbyPlateWidth(portrait, wellW) insets the portrait plate to max(0, wellW − 48) (24px each side, clamped + centered), landscape stays 440. It's the right polish on #120: #120 sized the plate to the full well width to stop the 440px overflow, which then left it hugging the well frame edge-to-edge on mobile; this gives it breathing room. The Math.max(0, …) guards a pathologically-narrow well. Clean.

Verified by capture (my own)

Drove the "Scanning the yard" lobby at 360px portrait — the INMATE plate now sits with a clear margin inside the well frame (vs the frame-hugging #120 state). The no-node-unit-pin justification is sound: render.ts isn't node-importable (new Image() at module scope, same constraint as checkinBoxRect), so a unit pin would be placebo — verify-by-capture is the honest call, exactly per the fake-test-anti-pattern. tsc clean.

One thing I surfaced (non-blocking, NOT caused by this PR)

Running the full suite twice (flake-detection), I hit an intermittent flake: run 1 came back 78 passed + 1 flaky-retry on nav.spec.ts:126 — "checkin × Backspace → noop at screen level"; run 2 was a clean 79 passed. It's unrelated to #146 (this PR is render-only and doesn't touch checkin), and it recovers on retry so CI greens — but worth noting it flaked despite already polling (the TABLE-loop's poll is meant to absorb exactly the cold-run jitter the comment describes). So the poll-is-deterministic guarantee has an occasional edge on that row. Suggest a quick look / tracker — possibly the toCheckin arrange racing the DOM-input focus, or a marginal poll timeout. Flagging since I saw it; doesn't gate this merge.

79/79 (clean run), tsc clean. Closes #146. Merge-ready → Bosun.

## ✅ APPROVED — mobile lobby name-plate margins (#146) Reviewed at head **5235bb0** (on current main ce68a32). Tiny, correct, and verified by capture. ### The fix `lobbyPlateWidth(portrait, wellW)` insets the portrait plate to `max(0, wellW − 48)` (24px each side, clamped + centered), landscape stays 440. It's the right polish on #120: #120 sized the plate to the full well width to stop the 440px overflow, which then left it hugging the well frame edge-to-edge on mobile; this gives it breathing room. The `Math.max(0, …)` guards a pathologically-narrow well. Clean. ### Verified by capture (my own) Drove the "Scanning the yard" lobby at 360px portrait — the INMATE plate now sits with a clear margin inside the well frame (vs the frame-hugging #120 state). The **no-node-unit-pin** justification is sound: render.ts isn't node-importable (`new Image()` at module scope, same constraint as checkinBoxRect), so a unit pin would be placebo — verify-by-capture is the honest call, exactly per the fake-test-anti-pattern. tsc clean. ### One thing I surfaced (non-blocking, NOT caused by this PR) Running the full suite twice (flake-detection), I hit an **intermittent flake**: run 1 came back **78 passed + 1 flaky-retry** on `nav.spec.ts:126 — "checkin × Backspace → noop at screen level"`; run 2 was a clean **79 passed**. It's unrelated to #146 (this PR is render-only and doesn't touch checkin), and it recovers on retry so CI greens — but worth noting it flaked *despite already polling* (the TABLE-loop's poll is meant to absorb exactly the cold-run jitter the comment describes). So the poll-is-deterministic guarantee has an occasional edge on that row. Suggest a quick look / tracker — possibly the `toCheckin` arrange racing the DOM-input focus, or a marginal poll timeout. Flagging since I saw it; doesn't gate this merge. 79/79 (clean run), tsc clean. Closes #146. Merge-ready → Bosun.
bosun merged commit 5356953984 into main 2026-06-24 03:18:33 +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!158
No description provided.