Take the dealer button out of the writing and put it on the rim

A puck can say four things about where a seat sits in the hand: small blind,
big blind, straddle, button. Three of them were pills hanging off the top-left
edge. The fourth was a 22px disc inside the top-right corner, on a layer above
the name — so the dealer's eye had two places to look for one class of fact,
and the corner one was charging the name 25px of its line to be there.

What that cost, measured across the sweep's tables with the twelve packed names:

  Galaxy S8  9-handed   name box  56px   11 of 12 fit — 7 on the button seat
  Galaxy S8 12-handed   name box  34px    7 of 12 fit — 1 on the button seat
  iPhone 14  9-handed   name box  68px   11 of 12 fit — 7 on the button seat
  desktop    9-handed   name box 105px   12 of 12 fit — 11 on the button seat

Four names in twelve, on a phone, cut on one seat and whole on the other eight.
And the button is the one marker on the felt that moves every hand, so that is
not four unlucky players — it is every player in turn, once a round, all night.

The chip now hangs off the same edge as the badges, at the same 5px, at the
other end of it. That distance is not free — SEAT_BADGE_OVERHANG_PX charges
every chair on the ring for it — but it is already spent, and it is charged
once per seat rather than once per marker, which stays right precisely because
the two markers share an edge and sit at opposite ends of it. So the ring does
not move at all. 12px is what fits between -5px and the top of the name on the
tight ring, and it is enough: a finger never reaches this chip anyway — on
touch the ghosts are pointer-events: none and the live one is inert, so giving
the button is the seat's long-press menu, and what is left is a mouse on a
ghost that only appears under the pointer.

Round where the badges are pills, and on the right where they are on the left,
because the button's gold and the big blind's now share one edge: what tells
them apart is the shape and the side, not the colour.

--

The other half of this was a name that could not be trusted to be one line.

The label was a `-webkit-box` clamped to one line, which hides the lines past
the first — but not the part of the second one that the row's own min-height
leaves uncovered. The row is a px token, 15px, "12px of name at 1.2", while
.puckNameTight steps the name down to 10.2px, whose line box is 12.24. That
leaves 2.76px. Measured on a 390px phone: "Christopher" on the button seat drew
as "Christ…" with the ascenders of "opher" showing underneath it as a stray
mark, which reads as a rendering fault on the first seat a dealer looks at.

`white-space: nowrap` with `text-overflow: ellipsis` has no second line to
leak. It also makes the box measure honestly, which the clamp did not: a name
that wrapped reported scrollWidth == clientWidth, so fitSeatNames() had only
ever been seeing the overflow as height — the right answer to the wrong
question, and only right while the row and the line agreed.

1046 tests pass; the 418-audit sweep is clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-10 16:20:37 -06:00
co-authored by Claude Opus 5
parent 8136025544
commit 37abc72de1
4 changed files with 172 additions and 94 deletions
+30
View File
@@ -204,6 +204,36 @@ describe("the blind badge counts", () => {
"the same distance at both ends, or one arc is charged for the other's badge");
});
test("and the dealer chip shares that edge on exactly the same terms", () => {
// It moved out of the puck's top-right corner and onto the rim beside the badges,
// which is only free while it hangs off by the same 5px they do. A pixel more and
// the ring is charging every chair for a distance it isn't measuring.
const top = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])top:\s*(-?[\d.]+)px/);
const bottom = CSS.match(/\.seat\.seatBelowCentre \.dealerChip \{[^}]*?(?<![\w-])bottom:\s*(-?[\d.]+)px/);
assert.ok(top && bottom, "the chip should hang off both arcs, like the badge");
assert.equal(Math.abs(Number(top[1])), Render.SEAT_BADGE_OVERHANG_PX);
assert.equal(Math.abs(Number(bottom[1])), Render.SEAT_BADGE_OVERHANG_PX);
});
test("and it stays inside the puck's own width, where the badges are", () => {
// Sideways is the direction that lands on the seat next door. This chip has been
// there: at -10px it took the centre of seat 1's taps and gave the button to seat
// 9. `right` is a positive inset for the same reason `left: 2px` is.
const inset = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])right:\s*(-?[\d.]+)px/);
assert.ok(inset, "the chip should be inset from the right edge");
assert.ok(Number(inset[1]) >= 0, "a negative inset hangs it over the neighbour");
});
test("and it is short enough to clear the name it sits above", () => {
// The tight ring is the close one: 1px of border, 3px of padding and 4px of
// margin put the name's top 8px below the puck's, and the chip reaches
// height - overhang below it.
const height = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])height:\s*([\d.]+)px/);
assert.ok(height, "the chip should have a height to check");
assert.ok(Number(height[1]) - Render.SEAT_BADGE_OVERHANG_PX <= 8,
"the chip dips into the name row on the tight ring");
});
test("and it is counted once, not once per seat", () => {
// seatsCollide is given one box and compares centres against it, so the
// separation it asks for is one puck plus one badge. That is exactly right for
+38 -27
View File
@@ -117,35 +117,27 @@ describe("the bar lines up with the table", () => {
// ---------------------------------------------------------------------------
describe("the button seat's name", () => {
// The dealer button's chip sits in the puck's top-right corner, and the name has to
// keep out from under it. `padding-right` takes the chip's width off the name's line,
// which is the whole of the answer now that there is only ever one line.
// The button seat's name is the same name in the same box as every other seat's.
//
// A float used to do this on the roomier rings — three rules of it, indenting the
// first line so a name too long to fit beside the chip could drop below it onto a
// second. That only worked because there was a second line. There isn't: see the row
// heights in holdem.css, and the fitSeatNames tests below for what happens instead to
// a name that does not fit.
const base = CSS.match(/\n\.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
// Three attempts said otherwise, in this order: a float, a float with a first-line
// indent, and `padding-right: 25px` — the dealer chip's width plus its inset, taken
// off the name because the chip sat inside the puck's top-right corner. All three
// were answers to a chip that had no business being in the writing, and the price
// was a quarter of the name's line on whichever seat currently held the button.
// Measured with the sweep's twelve names: 7 of 12 fitted on the button seat at
// 9-handed on an iPhone against 11 anywhere else, and 1 of 12 against 7 at
// 12-handed on a Galaxy S8.
const name = CSS.match(/\n\.seatPuckName \{([^}]*)\}/);
test("the chip's width comes off the name, at every size", () => {
assert.ok(base, "the base rule should reserve the chip's width");
assert.match(base[1], /padding-right:\s*25px/);
});
test("and it is as wide as the chip it stands in for", () => {
const chip = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])width:\s*(\d+)px/);
const inset = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])right:\s*(\d+)px/);
const pad = base[1].match(/padding-right:\s*(\d+)px/);
assert.ok(chip && inset && pad);
assert.equal(Number(pad[1]), Number(chip[1]) + Number(inset[1]),
"the padding should cover the chip and the gap it is inset by");
test("nothing reserves a gutter inside the name any more", () => {
assert.equal(/\.seat\.seatHasButton \.seatPuckName/.test(CSS), false,
"the button seat's name should be the same box as every other seat's");
});
test("the float that used to do this is gone, not merely unused", () => {
// Left in place with a one-line clamp it would clip the name to nothing: the
// word cannot fit beside the float, so it moves to a second line, and the second
// line is what the clamp hides. Measured — "Christopher" drew as an empty label.
// Left in place with a one-line name it would clip the name to nothing: the word
// cannot fit beside the float, so it moves to a second line, and there is no
// second line. Measured — "Christopher" drew as an empty label.
assert.equal(/\.seatPuckName::before/.test(CSS), false,
"a float and one line of name cannot both be true");
});
@@ -154,9 +146,28 @@ describe("the button seat's name", () => {
// This is what every puck being the same height rests on. A name is a label, and
// the ring is sized against the tallest puck on it, so one name wrapping is one
// seat setting how far apart all of them have to sit.
const clamps = CSS.match(/(?<![\w-])line-clamp:\s*(\d+)/g) || [];
assert.ok(clamps.length > 0, "the name should still be clamped to something");
clamps.forEach(clamp => assert.match(clamp, /:\s*1$/, clamp));
assert.ok(name, "the name should have a base rule");
assert.match(name[1], /white-space:\s*nowrap/);
});
test("and it is one line by not wrapping, not by hiding what wrapped", () => {
// A `-webkit-box` clamped to one line hides the lines past the first; it does not
// hide the part of the second one that the row's own min-height leaves uncovered.
// The row is 15px and .puckNameTight's line box is 12.24, so 2.76px of the next
// line showed — "Christ…" with the ascenders of "opher" underneath it.
assert.equal(/(?<![\w-])line-clamp:/.test(CSS), false,
"a clamp and a fixed row height only agree while the type never moves");
assert.match(name[1], /text-overflow:\s*ellipsis/);
});
test("the box a name is measured in is the box it is drawn in", () => {
// What the clamp cost besides the stray line: a name that wrapped reported
// scrollWidth == clientWidth, so fitSeatNames() could only ever see the overflow
// as height. `nowrap` puts the whole name on one line whether it fits or not,
// which is what makes the width worth reading.
assert.match(name[1], /overflow:\s*hidden/);
assert.equal(/overflow-wrap:\s*break-word/.test(name[1]), false,
"nothing to break a word onto");
});
});