Follow the browser's text size instead of sliding out from under it

Every length in this stylesheet that isn't a structural clearance is in
`rem`. That is the right way round — it means the page follows the
browser's font size, which is what a phone's text-size slider changes —
and it means there was a whole axis of behaviour that 320 audits had
never once touched, because every one of them ran at the default 16px.

The sweep gets a third pass: the same surfaces on the eight phone-shaped
viewports at 20px of root text, which is 125% on an Android slider.
Everything it found on its first run was caught by a check that had been
sitting there for months with nothing to say.

The page slid sideways. Three header buttons that will not break and will
not shrink — `white-space: nowrap` and a `min-width: auto` floor apiece —
were told `flex-wrap: nowrap`, so when they stopped fitting they had
nowhere to go: "Resolve Pot" ended 23px past the right edge of a 360px
window and put a horizontal scrollbar under the whole document; at 24px,
60px past the edge of a 390px one. They take a second row now on the day
they need one, and still fit on the first and still grow into it at any
ordinary size.

Figures painted over the seat next door. Which of the two type steps an
amount got was decided by counting characters — eight for one, ten for
the other — which is a guess at how wide a figure is that happens to hold
at one text size and one puck width. At 20px on a 44px puck, "$1,200" is
six characters, took no step at all, and wanted 38px of the 34 it had.
Render.fitAmounts measures now, in the batched passes fitSeatNames
already uses, and takes the separators out before it takes a step of
type: "$1200" at full size beats "$1,200" at three-quarters of it.

And the pucks grew, which the ring cannot answer. The row heights are px
tokens so an amount stepping its type down shortens its digits and not
its row — but a token is a floor, not a ceiling. At 125% the puck went
from 55px tall to 65 and twelve of them came down on each other, because
at the 44px floor a thumb needs there is nowhere narrower to search. The
two sizes a puck is written at are capped at exactly the sizes they have
always been drawn at, so nothing changes for anybody at an ordinary text
size — and the felt is a diagram, so the rule that already governed a
puck's width now governs its height. Roomy pucks are exempt; they have
the room. The same figure is in Standings in full, at whatever size its
owner asked for.

Two smaller things on the way. The landscape rail opened at its top, so
its overflow came off the bottom — where Cancel and Raise to are; it
opens at the end now, which is the same reason it is anchored low at all.
And the check that called those buttons unreachable was wrong: the rail
scrolls on purpose, and a sweep that cries wolf is a sweep nobody reads.

418 audits, clean. One shape it is not held to, with the arithmetic in
runUiAudit.js: twelve players all-in on a 360x640 phone leaves the felt
290x163 once the bar has taken 283px of the 640, and twelve 44x62 pucks
do not fit around an ellipse 163px deep. Reported as a note every run
rather than a failure every run. 1026 unit tests.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-10 13:40:44 -06:00
co-authored by Claude Opus 5
parent 42b36a1464
commit d4c39f209a
8 changed files with 431 additions and 73 deletions
+115
View File
@@ -240,3 +240,118 @@ describe("and the ring checks its own measurement afterwards", () => {
assert.equal(withBox(null, () => Render.seatBoxMoved({ width: 72, height: 56 })), false);
});
});
// ---------------------------------------------------------------------------
describe("how much room a figure has on the puck it is written on", () => {
// The two steps of type an amount comes down used to be chosen by counting
// characters: eight for the first, ten for the second. That is a guess at how wide a
// figure is, and it happens to hold at one text size and one puck width.
//
// Turn a phone's text up — 20px of root text is what an Android slider gives you at
// 125%, and every size in the stylesheet is in `rem` on purpose — and it stops
// holding. Measured at 360x640 twelve-handed: the puck is at its 44px floor, which
// is 34px of usable width, and "$1,200" is six characters, took no step at all, and
// wanted 38. The sweep caught it as a stack figure painted over the seat next door
// at every phone in the list, and as "ALL IN" doing the same on the all-in table.
//
// So it is measured. A cell is a stand-in for the real one: what it wants against
// what it has, and a class list to write the answer into.
const cell = (figure, wants, has) => ({
dataset: { figure },
textContent: figure,
classes: new Set(),
classList: {
add(name) { this.owner.classes.add(name); },
remove(name) { this.owner.classes.delete(name); },
},
// What the text wants is proportional to how many characters are in it and how
// far down the ladder it has come — which is what a browser would tell us.
get scrollWidth() {
const step = this.classes.has("puckAmountHuge") ? 0.7
: this.classes.has("puckAmountLong") ? 0.85 : 1;
return Math.round((wants * this.textContent.length / figure.length) * step);
},
clientWidth: has,
});
const make = (...args) => {
const c = cell(...args);
c.classList.owner = c;
return c;
};
const fit = cells => {
const before = Render.overflowing;
Render.overflowing = el => el.scrollWidth > el.clientWidth + 1;
try { Render.fitAmounts(cells); } finally { Render.overflowing = before; }
};
test("a figure that fits is left at the size it is meant to be read at", () => {
const c = make("$600", 30, 34);
fit([c]);
assert.equal(c.textContent, "$600");
assert.deepEqual([...c.classes], []);
});
test("one that doesn't gives up its separators first", () => {
// Grouping is a reading aid for a column of figures. This is one number on a
// 44px puck, and a step of type costs more than the commas do.
const c = make("$1,200", 38, 34);
fit([c]);
assert.equal(c.textContent, "$1200");
assert.deepEqual([...c.classes], [], "no type step needed once the commas are out");
});
test("then a step of type, and then another", () => {
const c = make("$1,234,567", 90, 34);
fit([c]);
assert.equal(c.textContent, "$1234567");
assert.deepEqual([...c.classes], ["puckAmountHuge"]);
});
test("and only ever one step at a time is worn", () => {
// The two rules have the same specificity, so a puck wearing both would be sized
// by whichever the stylesheet happens to mention last.
const c = make("$1,234,567", 45, 34);
fit([c]);
assert.equal(c.classes.has("puckAmountHuge") && c.classes.has("puckAmountLong"), false);
});
test("a figure with no separators to give goes straight to the type", () => {
// "ALL IN" is the one thing on this row that isn't a number, and it is the widest
// — six characters and a space, at 700 weight with its letters spread apart.
const c = make("ALL IN", 39, 34);
fit([c]);
assert.equal(c.textContent, "ALL IN");
assert.deepEqual([...c.classes], ["puckAmountLong"]);
});
test("a puck that went small can come back", () => {
// Everything is measured from the figure and the size it is meant to be read at,
// not from whatever the last render left behind. Otherwise this is a ratchet: one
// crowded frame and the seat is small for the rest of the night.
const c = make("$600", 30, 34);
c.classes.add("puckAmountHuge");
c.textContent = "$600";
fit([c]);
assert.deepEqual([...c.classes], []);
});
test("and the grouped figure is what it comes back to", () => {
const c = make("$1,200", 38, 34);
fit([c]);
assert.equal(c.textContent, "$1200");
c.clientWidth = 96;
fit([c]);
assert.equal(c.textContent, "$1,200", "a wider puck should get its separators back");
});
test("an empty row is not an amount", () => {
// The pill keeps its place on a seat with nothing in the pot — that is what makes
// every puck on the ring the same puck — and there is nothing there to fit.
const c = make("", 0, 34);
assert.doesNotThrow(() => fit([c]));
assert.deepEqual([...c.classes], []);
});
});