Commit Graph
7 Commits
Author SHA1 Message Date
jheapsandClaude Opus 5 37abc72de1 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
2026-09-10 16:20:37 -06:00
jheapsandClaude Opus 5 8136025544 Say what the undo would take back, and let the empty chairs be empty
Two things on the felt and the bar that were spending weight on the wrong
half of what they had to say.

The undo pill names the last action, and on a phone it had stopped doing
it. It is a fixed width — its text changes on every tap, and a control
that resizes under a thumb reads as the interface twitching — and that
width leaves the label 73px. Measured across one hand, four of the five
labels a hand produces wanted more:

  ↶ dealing the hand        104     ↶ Christopher's fold      109
  ↶ Mike from work's fold   128     ↶ Alexandra's fold        100
  ↶ Dee's fold               69  (the only one that fitted)

so the safety net spent most of a night reading "↶ dealing …". Cut as one
string it keeps the wrong half: "↶ Mike from wor…" names everybody except
the action. So an action's name is handed to commit() in two halves now —
who it was, and what they did — and the actor comes off whole when the
act cannot fit beside it. The whole sentence stays on the button's title
and aria-label, which is what the guidance asks of a control that has had
to shorten its own name.

Two things fell out of it. The label cannot overflow — the act inside it
is the item with `overflow: hidden`, so it swallows the excess and the row
measures as though everything fitted — which is why the fit asks the act
and not the label; asked the label, "↶ dealing the hand" kept its actor
and quietly cut "the hand" off inside it. And the pill was still printing
a keycap on phones: every other control on the bar has dropped its cap
under `hover: none` since that rule was written, but this one is a
different class and had been left out of it, so the narrowest label on the
bar was the only one spending 22px on a shortcut nothing could press.
That 22px is exactly what "raise to $1,200" needed to keep its digits.

Nothing is cut now at any viewport in the sweep: a phone reads "↶ the
hand", "↶ Dee's fold", "↶ raise to $20", "↶ call"; a desktop and the
landscape rail keep the actor wherever it fits.

And the empty chairs. An open seat was drawn as a whole puck of near-black
on the green — the same box a seated player gets, at the same weight,
saying nothing. Five players nine-handed is four of them, and on the felt
they read as holes punched in the table: the eye had to sort the loud
empty boxes from the loud full ones before it could find who was in the
hand. The wash was there for one reason, which is that the chair number's
contrast cannot depend on whether its chair sits over the rim, the dark
green or the light — about Lc 5 across one ring. So the wash moved off the
chair and under the number, which is the only thing an open chair has to
say. The dashes still say "open", the box a thumb lands on has not moved a
pixel, and the felt gets four boxes' worth of quiet back.

1039 unit tests; 418 layout audits across 13 viewports, clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-10 14:13:25 -06:00
jheapsandClaude Opus 5 42b36a1464 Stop the bar dropping the one word it exists to say
The acting line names whoever the table is waiting on, and on a phone it
had stopped doing it. The name was set up as the half that gives way —
right rule, no floor under it. Flex shrinks the only item that can shrink
until the line fits, and with the clock button on the line there was no
fitting to be had: measured with five players nine-handed, mid-street,

  360x640   "Christopher" wanted 62px, drawn 0.  "Alexandra" 54 -> 0.
  390x844   "Christopher" wanted 62px, drawn 0.  "Alexandra" 54 -> 0.
  430x932   "Christopher" 62 -> 31.

What the bar read on the first two was "· $1,200 behind" — a dangling
separator and a figure, at the moment the dealer is looking at it to say
whose turn it is out loud. The ellipsis the rule was written around never
appeared; there were no characters left to put one after.

The rule stays: a number read wrong is worse than a name read short, so
the stack is never truncated. It comes off the line whole instead, and
it is not lost — the same figure is on that player's own seat puck.

The stopwatch pays for the rest. It was on the line from the first second
of every decision, reading "0:07" in grey: 50px of the narrowest row in
the tool spent on a number nobody acts on. It is a prompt, not a readout,
and it now arrives when the button that acts on it arrives — the same
thirty-second threshold, asked once.

  360x640   "Christopher" 62 -> 44 ("Christo…"), "Alexandra" 54 -> 44
  390x844   both whole, where both were nothing
  430x932   both whole
  and "Mike from work" whole at 360 where it was cut to 68

with the bar 151px and the felt 309x463 either side of the thirty-second
mark, and 0px of seat movement across it.

Also: the space before the separator. These are two flex items and a flex
item's own leading whitespace is collapsed at its edge, so the bar has
read "Alexandra· $1,500 behind" since the sentence was split in two. The
gap is in the stylesheet now, where it can survive that.

1018 unit tests; 320 layout audits across 13 viewports, clean.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-10 12:43:12 -06:00
jheapsandClaude Opus 5 ab04c43ffd Stop the table resizing under the thumb that is reaching for it
Two things moved the felt, and neither of them was the felt's business.

The raise pad. It is 57px taller than the row it replaces, and the table was sized
against it — so pressing Raise shrank the whole thing, re-ran the ten-candidate width
search, redrew every seat at a different size, and undid all of it when the dealer
pressed Raise to. Measured, five players nine-handed:

  1280x900   felt 738x532 -> 669x481, pucks 120 -> 109, worst seat moved 73px
  360x640    felt 283x329 -> 283x279, pucks  44 ->  48, worst seat moved 50px
  390x844    felt 309x463 -> 309x451,                   worst seat moved 11px

Every raise, all night, at the one moment the dealer is reading the felt to decide
what to type. A phone held sideways already scored 0px: the rail is a column down the
side, and its height never was the table's business. So the felt is sized against the
bar at rest and the pad floats up over the bottom of it, which is what a transient
surface is supposed to do — the context it is over stays where it was. --actionBarHeight
is untouched and still says what is really covering the window, because that is what the
toast has to clear.

And the status row. It wraps, and flex settles its line breaks before it decides what
to shrink, so a name too long for the room left beside the clock and the undo pill did
not give way — it pushed them onto a third line. That is 22px off a felt underneath a
bar fixed to the bottom. Measured at 360x640 mid-street: "Dee · $600 behind" left the
table 283x329 and "Christopher · $1,200 behind" made it 283x310, with every seat moving
24px between one player and the next. Whose turn it was decided how big the table was.

An earlier round saw this and reserved two lines for the row; three is what it takes.
Reserving three would spend the 22px on every table for the sake of some of them. So
the actor, the clock and the undo pill get a line of their own that cannot wrap, and
the name shrinks into it — with the stack held out of the shrink, because a number read
wrong is a worse failure than a name read short. The row is 66px whoever is acting.

  It claims that line only when there is somebody on it. Claiming it unconditionally
  cost one in the state that can least afford it — a board running out has nobody to
  act, so the row is the undo pill alone, and it used to sit up beside "All-in —
  running out the board".

  The pill gives width back to the name on a phone: 10rem is 160px, and at 360 it took
  half the row, leaving "Christopher" cut to 23px of its 75. A media query, not
  `min(10rem, 34%)` — that was tried, and a percentage against a row sized to its own
  contents chases itself: the sweep caught the pill drawn 38px wide at ten viewports.

Verified by walking a hand at five viewports: one felt size, one puck width, and not
one seat moving, across a raise pad opened and shut and a full street of action.

1003 unit tests. 320 layout audits across 13 viewports with a mouse and a thumb, and
this is the first round in a while with nothing in them — the chip behind seat 11 on
the twelve-handed Galaxy S8, which master has carried for rounds, clears too.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-10 11:48:04 -06:00
jheapsandClaude Opus 5 4a2ec1295b Make every seat on the ring the same seat, and measure it before believing it
Nine chairs, and until this no two of them were the same size. Measured at 1280x900
with five players seated: 64px for Bob, 70 for Christopher because his name wrapped,
71 for Alexandra because she had chips out, 89 for "Mike from work" who had both.

The ragged edge was the smaller half of it. The larger half is that the sizes moved.
A puck grew 17px the moment its player put money in and shrank again when the street
closed, so the felt reflowed twice a hand under the thumb of someone trying to tap it
— and because the ring is sized against the tallest puck on it, the whole table could
resize with it.

Three rows are held open instead: the name, the amount, and the stack. The name gets
one line whatever it says, the amount pill keeps its row when it is empty rather than
collapsing, and each row is a token rather than an em so an amount stepping its type
down to fit shortens its digits and not its row. What the puck says changes all night;
how big it is does not.

It costs nothing. measuredSeatBox already took the tallest puck on the ring, on
purpose, so the room was reserved at every seat and the short ones simply weren't
drawn into it. What pays for the third row is the second line of name going away: an
earlier round measured uniform-with-two-lines at 88px and rightly refused it.
Uniform-with-one is 71.

  A name with no word to drop steps its type down instead of ellipsising. fitSeatNames
  turns "Mike from work" into "Mike", which is what the table calls him, and could do
  nothing at all with "Christopher" — so that one was cut to "Christo…" on a felt that
  may also have a Christine at it. The amounts on the same puck have never taken that
  trade; this is fitAmount's rule applied to the other thing a dealer reads across a
  table. One step, floored at 9px, then the ellipsis.

  The float that kept the button seat's name off the dealer chip goes with the second
  line — three rules of it, indenting the first line so a long name could drop below
  the chip onto a second. There is no second line to drop to, and left in place it drew
  "Christopher" as an empty label. The padding every crowded ring already used is the
  rule at all sizes now, widened from 22px to 25 so it actually covers the chip and the
  gap it is inset by.

Then the part that was quietly wrong. renderSeats builds nine empty pucks and fills
them afterwards, so the layout search ran against blank boxes and cached the answer
under a key naming a table it had never measured. On a 390px phone that is a ring
sized for empty seats — and the comment where it was cached said "let the resize that
follows do this properly". Nothing resizes a phone. Turning the phone sideways and
back was the only thing that ever fixed it, and it fixed it into a different layout:
same viewport, same table, 90px pucks on load and 72px after a rotate.

So the finished pucks are measured against the box the search settled on, and if they
disagree the search is asked again with the real thing in front of it. Once, and the
second answer is cached. Fresh load and rotate-and-back now agree exactly.

That also lets the cache key shrink to almost nothing. It used to name every player's
name, the dealer's seat, whether anybody had cards and whether anybody had chips out —
four things, three of which flip during an ordinary hand, each flip a ten-candidate
re-search. None of them can change a puck's height any more. Measured over three calls
in a row: no re-search, and not one of the nine seats moved a pixel.

The crowded ring gives up the 1.4 leading on its money rows to pay for the row it now
holds open — 10px of figures is 12px of ink and the rest was blank. Without it the
Galaxy S8 twelve-handed, where the pucks are already at the 44px floor, put
neighbouring seats' blind badges down onto each other's "ALL IN". Hanging the badges
further out was tried first and is the wrong direction: the badge landing on a stack
figure belongs to the seat next door, so every pixel further out is a pixel further
onto a neighbour.

992 unit tests. 320 layout audits across 13 viewports with a mouse and a thumb: one
failure, the chip behind seat 11 that master already carries.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-10 11:08:14 -06:00
jheapsandClaude Opus 5 7bd205a91a Line the bar up with the table, and stop the button squeezing a name
Two things that were wrong at every desktop width, both of them the same shape: a
number that was right about one thing being used for another.

The action bar was centred on the window. The table is centred on its own column, and
the sidebar takes 320px out of the right of that — so the two things a dealer is
actually looking at sat 172px apart, at 960px wide and at 1600px and everywhere in
between, because the offset is half the sidebar and the sidebar is a constant. The bar
also finished 80px inside the sidebar's column at 1280, painting over whichever panel
happened to be at that height. It leans away from the sidebar now, by the sidebar's own
width, which comes out exact above and below the 1400px cap — measured on the running
page at four widths, 0px off at all of them. No 100vw in it, which is the point: the
bar's own centring comment warns that on a page with a scrollbar 100vw is wider than
the viewport, and the offsets are exact without it.

The dealer button's chip sits in the puck's top-right corner and the name kept out from
under it with `padding-right: 22px`. That takes the chip's width off *every* line of the
name, and the chip is on one of them. Measured at 1280x900: the name box is 78px,
"Christopher" is 66 and fits, and on the seat holding the button it was cut to 56 and
broke as "Christop / her" — mid-word, no hyphen. The button moves every hand, so that is
a player whose name reads properly for four hands and comes apart on the fifth.

A float is what indents only the lines it sits beside. Line one gets 56px and the rest
get 78, so a name too long to fit beside the chip drops below it whole instead of being
broken to fit next to something that isn't there.

Not on a crowded ring, though, and finding that out cost a sweep. A tight puck gives the
name one line on purpose, and one line has nowhere to move to: with two lines granted
the name dropped onto a second line that the chip's lower half was still sitting on, and
the audit called it at every viewport in the list. So the float is for the ring with room
for two lines and the padding for the one without — two rules, matching two regimes the
puck already had.

The sweep also learned two things from this:

- Text is measured where it was painted, not where its box is. The seat-label check
  modelled "text kept out of somewhere" as padding, which is one way of doing it and not
  the only one; a float does it per line, and the check called every seat in the sweep an
  offender while the text sat exactly where it was meant to. Range rects ask the question
  directly, so padding, floats, centring, wrapping and a short label in a wide box all
  come out right without any of them being modelled.

- Offenders say how much is covered, not that something is. "Is under a badge" reads the
  same for a stack a dealer cannot read and for a badge's rounded corner grazing the last
  digit. Probing the whole line and counting turns the residual this sweep has carried for
  three rounds into "1/7 under SB" — one probe of seven, one configuration, thumb only.

294 audits, 4 failing configurations, 5 offenders: twelve players on a 360px phone with
the raise pad open (3/7 and 2/7, pucks genuinely overlapping — the search reports it
rather than papering over it) and the 1/7 hairline above. 770 unit tests.

Fault-injected, all restored and re-verified: writing the sidebar's width out again
instead of the token, growing the float to cover the chip's whole height, and taking the
padding off the crowded ring are each caught by a test; and with both the float and the
padding removed the rewritten label check still reports the name 6px under the chip,
which is the fault it exists for.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-04 18:17:16 -06:00
jheapsandClaude Opus 5 f07144acd8 Give the table the screen back when the phone is on its side
A phone held sideways is 640x360: short of height and swimming in width. The layout
spent the whole of that shortage on one column — 70px of header, 85px of felt, 205px
of action bar. Two rows of seat pucks 55px tall have to fit inside 85px of felt, which
they cannot, so seven of nine finished past the rail with up to 17px of puck floating
on the page. That single fault was 96 of the 98 offenders the layout audit could find
anywhere.

No amount of shrinking fixes it, because the shortage was never in the table. The bar
is 205px tall and 529 wide to hold three buttons and a status line, and it was taking
that height from the one element that needed height. So in landscape it moves: the bar
becomes a rail down the right-hand side, the table takes the column beside it, and the
felt goes from 529x85 to 409x205 — an actual oval, with room for two rows of pucks and
a gap between them. It also puts Fold, Call and Raise under the right thumb rather than
along the bottom edge, where a landscape grip has the palm.

Which layout the bar is in is declared by the stylesheet in a custom property and read
back by actionBar.js, because the one thing that must not exist is a second copy of
that media query written in JavaScript.

Then the ring stopped predicting itself. seatRingRadii works out the widest ring whose
outermost puck still finishes inside the rail, and it is right about a puck it measured
before the width search settled and before a name wrapped or was shortened — each worth
up to 15px of height arriving after the answer. Four viewports finished with the top
seat 5-6px over. render.js now checks the drawn result the way the audit judges it: if
the worst puck hangs 6px past the rail, the ring comes in by 6px and they are placed
again. A layout that predicts itself has to be right about the future; one that measures
itself only has to be right about the present.

Also in this round:

- Names shorten at a word instead of being chopped mid-one. "Mike from work" on a
  nine-handed phone was "Mike f…"; he is Mike. A name never ends on a joining word,
  because "Mike from" looks cut off rather than shortened, and the whole name is on the
  tooltip either way.

- The blind badge is counted in what a seat needs. It always points away from the table,
  which was taken to mean it costs nothing between two seats — true of a straight rail
  and false of a ring, where neighbours sit at different heights. Three tables had a
  player's stack painted under the next player's SB or BB, and that badge outranks
  everything else on the felt. It also got shorter, 15px to 12: clearing its own name at
  5px of overhang instead of 9 hands every chair in the ring four pixels back, which at
  640x360 is the difference between a 66px puck and a 48px one.

- Border-box, inherited, for the whole app rather than nine elements opting in one at a
  time. Both elements that hadn't went wrong in the same afternoon: the rail overran the
  page's padding by 25px and put Resolve Pot underneath itself, and the raise pad's
  amount field pushed a horizontal scrollbar across a 640px phone.

- The raise amount field is thumb-sized. The rule that sizes fields for a finger names
  the settings panels and the seat form, so the one field on the one surface built for
  thumbs was 36px on every phone in the list. The sweep never saw it because it had no
  step that opened the raise pad; it has one now, and that step also caught the rail
  squeezing its own status line under the pad.

- mobile-web-app-capable alongside Apple's older name, which Chrome has been calling
  deprecated in this page's console on every load.

The audit goes from 19 failing configurations and 98 offenders to 2 and 3, across 294
audits — thirteen viewports, three table shapes, a mouse and a thumb. What is left is
twelve players on a 360px phone with the raise pad open, where twelve pucks at the 44px
minimum genuinely do not fit on the felt; the search reports that rather than papering
over it. Each new mechanism was checked by breaking it on purpose: removing the settle
pass puts 26 offenders back, dropping the badge from the footprint 5, and un-sizing the
raise field is caught by the step that now opens it.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
2026-09-04 17:19:11 -06:00