6a59b0d5ba65e37badfaab03dc4d07572625c9a3
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6a59b0d5ba |
Stop the table being drawn from a measurement of a bar that isn't there
The felt is sized from --feltRoomPx, which actionBar.js publishes from the bar's real height, because the bar is fixed to the bottom and grows with the hand so nothing else can know how much window is left. That measurement is right. What it was measuring was not. ActionBar.mount() called watchHeight() before render(), and the bar ships `<div id="actionBar" hidden>`. So the first publish measured a bar that was not there yet and published 0px — not "I don't know" but "the bar takes none of the window", which is the one answer that makes the felt draw itself too big. Traced on a cold load, nine-handed: t=181ms --actionBarHeight 0px bar hidden, 0 tall t=186ms --feltRoomPx 721px t=1056ms --actionBarHeight 151px bar visible, 151 tall — ResizeObserver t=1059ms --feltRoomPx 570px The observer does come round on its own once `hidden` comes off, but a frame later at best, and with nine seats to lay out it was 875ms behind. What a dealer saw at 360x640 was a table drawn 336x504 and collapsing to 336x374 after it was on screen and readable: 130px of felt with every seat on it. 1280x900 lost 13px the same way. Two rules, separate on purpose. publish() now declines rather than guessing while the bar is hidden and not a rail — a rail still publishes its honest zero, because it is a real bar that has moved out of the way. And mount() publishes once render() has given it something to measure. That leaves the frame before any of it, which is real: the scripts are at the end of the body and the table is laid out inside DOMContentLoaded, so the browser has painted by the time --feltRoomPx exists. The fallback there was `100vh` — the worst guess available, since it says the whole window is free. It is now the same guess two other rules in this file already make, 9rem for the bar plus 7rem for what sits above and below, which lands 10px over the truth at 360x640; and it is a ceiling, so over is a small shrink rather than a seat behind the bar. Measured after, on both phones: one draw, no jump, and a layout-shift score of 0 where the felt used to move 130px. 1362 tests pass, 8 of them new — 5 fail against the old code. One existing test pinned the literal fallback string; its claim is about the cap being the measured room, and it now says that. The sweep is clean across 428 audits; contrast unchanged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e36e172670 |
Let the undo pill finish its own sentence
The bar's undo is a fixed-width pill because its text changes on every tap and a control that resizes under a thumb reads as the interface twitching. On a phone that leaves the label 78px, so the label is three elements and fitUndoLabel drops the actor whole rather than letting an ellipsis land wherever it lands. That machinery was right. The labels fed to it were not. Two rules follow from "the actor is the half that goes", and six of the fifteen labels broke one. The act has to name the action on its own, because it is what survives. Four labels put a verb in the actor's slot — "dealing" + "the hand", "dealing" + "everybody in", "calling" + "the straddle off" — so dropping the lead left the pill reading "↶ the hand" after every deal of the night, and "↶ the straddle off". Neither names anything. Those are re-split: "the deal", "everybody dealt in", "the straddle called off". The act has to fit, because whatever is left is ellipsised. Measured at 390x844 and 360x640 with a thumb: "hand killed on the clock" wanted 125px of 78, "check on the clock" 99, "waiting for the blind" 103, "posting to come in" 97 — all four came out cut mid-phrase, "↶ waiting for th…". The qualifier goes into a new optional `long`, which is the whole sentence, and the verb stays on the pill. Every short form is a substring of its long form, which is what Label in Name asks of a control that has shortened its own name — and it is what lets `long` be the accessible name rather than only a title a phone has no way to show. Before this the name was whatever was visible: "↶ hand killed U", arrow and keycap included. Both are aria-hidden now. Measured after, on both phone sizes: nothing ellipsised, and every visible form contained in the name. One exception, stated in the code: a five-figure raise. "raise to $12,500" is 86px of 78 and the number is the whole of what it says, so there is no qualifier to move — the leading digits survive, the amount is on that player's seat puck, and the whole of it is on the name. Three comments carrying the old 73px figure are re-measured while I was here. 1353 tests pass, 11 of them new — 3 fail against the old labels. The sweep is clean across 428 audits; contrast unchanged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6992833821 |
Make the way back reachable, and stop it leaving while it is being reached for
This tool prefers undo to confirmation, deliberately — runUndoable says so in as many words — which makes the toast's Undo the safety net under the biggest things it does. It was not one. Every whole-step undo clears the per-action history on its way past, so the bar has no pill left and the toast's button is the only way back. Measured on a desktop it is the 67th of the page's 68 tab stops, and it is gone in eight seconds. And U — the key the sidebar legend advertises as taking back the last action — found nothing and did nothing: it only ever looked in the bar. So U looks at the toast first, by pressing the button that is on the screen rather than running a second copy of what it does, and only at a button that named the shortcut: "Turn off" on the wake lock and "Reload" on the other-tab warning are not things a stray keypress should reach. The eight seconds also ran whatever the dealer was doing. A cursor resting on the toast did not stop it; nor did tabbing onto the button. Measured before: focus the Undo, wait 8.6 seconds, and the toast is gone and focus has been handed to <body> — the offer withdrawn and the dealer's place in the page with it. Touching it or tabbing into it now stops the clock, and letting go starts it again with at least the time to act rather than whatever was left of the time to notice: let go at 7.9 seconds of 8 and 100ms is not an offer. Dismissing with focus inside puts focus back where the dealer had got to, which is read off where focus crossed into the toast rather than off where it was when the message arrived. And each toast carried its own role="status" inside #toastHost, which is already a live region in the markup. A region nested in a region, announced twice; the one in the markup is the one that works, because it is the one anything has been watching. 1342 tests pass, 18 of them new — and 8 of those fail against the old code. The sweep is clean across 428 audits; contrast unchanged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
023228efc0 |
Give the rulings the window a sideways phone was already holding
Twelve rulings in a 460px sheet in the middle of an 844px window: 754px of content in a 334px scrollport, 111 of it spent on a title and a caveat that stand still, 384px of window width unused either side. Four of the twelve names on screen, and the dealer is looking one up with an argument going on at the table. Material's rule for a bottom sheet in landscape is to stop being one, and this tool already does exactly that under this query for the action bar. So the sheet takes the window and the width pays for the height: the twelve go into two columns of six, all of them on screen at 844x390 with nothing to scroll at all, and the title, the caveat and Done share one line instead of standing on three. Two wrappers in the markup rather than one grid of twelve, because a grid pairs its children into shared rows: opening the third ruling on the left would push the ninth down the right-hand side to keep the row square. Two flows means a column only ever moves itself — measured, opening one leaves the other six names exactly where they were. Everywhere but landscape the wrappers are `display: contents` and the twelve are the single column they have always been. And the same sheet upright: the header is stuck to the top of its own scrollport now, the way the seat menu's has been. Measured before, at the bottom of that scroll Done was 221px above the sheet's top edge on a 360x640 phone, and 262 on a 390x844 one with a ruling open — which is the only reason anybody is in here. A dealer who has read the answer had to scroll back past it to leave. The layout sweep's scroll-into-view guard learns the new box, or it finds a sheet whose scrollHeight equals its clientHeight and decides nothing is out of view. 1324 tests pass. The sweep is clean across 428 audits; contrast unchanged. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f86e50268e |
Stop calling it the next hand when it deals no hand
"Next hand" named three things: the bar's green button, the pot wizard's last button, and the header shortcut — the only one of the three that never starts a hand. Between hands it moves the button a second time, so the seat about to pay the big blind is skipped, and the toast opened with the words the wizard had just used, which read as a confirmation of what was already done. Measured: settle through the wizard and the table is on hand 6 with the button on seat 2; press the header's Next Hand and it is still hand 6, button on seat 3, no confirmation asked. The mechanism stays — a dealer does pass the button along on a hand nobody played. The name changes. The label and its title are now read off whether a hand is out: "Next Hand ⟳" while one is, "Move Button ⟳" when none is, written past the dimmed-reason cache each render so they follow the table instead of freezing on the state the page loaded in. The toast says which of the two happened, on one line: the second line told the dealer to press the green button the bar is already holding. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
4778852436 |
Ask a seat what it is doing, and put the answer where it can be seen
Two findings in the seat editor, and the second is why the first was
hard to notice.
A seat is sitting out, in the hand, or folded, and "folded" means
nothing while the seat is out. The form asked that as two checkboxes,
which offers a fourth corner that has no meaning, and the code that
stopped a dealer reaching it disabled the Fold box whenever In hand was
clear. Measured: that left the label at opacity 1 and colour
rgb(170,170,170) — identical to a live one — with a title of "Folded"
on it. Pressing Fold on a seat that was sitting out gave no movement and
no reason, which is the exact failure round 16 went through the tool to
remove; it hid from the sweep rule that finds those because the title
sat on the <label> rather than on the control the rule inspects.
Three named states can't contradict each other and none of them is ever
unavailable, so there is nothing left to dim. Sitting out · In the hand ·
Folded, as a radiogroup — the same shape the card picker's suits use —
with the chosen Folded segment in the red the seat menu's own Fold
already wears.
And the form itself. The phone block's comment has always said why the
editor becomes a bottom sheet — "the seat editor lives far enough down
the page that tapping a seat looks like it did nothing" — but the sheet
was scoped to max-width 700px while #holdemLayout goes to one column at
900. The two tiers in between had exactly the fault the sheet was
written to fix. Measured, tapping a seat:
844x390 the editor drew in the flow at y=369 in a 390px window —
21px of 401 on screen, 5% of it, at scrollY 0 with nothing
scrolling it into view. The seat lit up green and the form
it opened was under the bottom edge.
768x1024 31% visible, the rest below the fold, again no scroll.
An iPhone held sideways and an iPad held upright are not obscure. The
query is the layout's own breakpoint now, and the seat-sheet rules moved
out of the phone block into their own so it could be asked separately.
Afterwards every viewport from 360x640 to 900x500 shows 97-99% of the
sheet, and above 900 the editor is a static block at the top of a
sidebar beside the table — 100% visible there too.
1307 tests pass; the sweep is clean at 428 audits across 13 viewports.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3458a6d144 |
Put the two numbers the panel glances at above the prose
Table Setup is the one panel the tool opens by itself, on a table nobody has set up yet, so it is most of what a dealer sees the first time they arrive. Measured on a fresh table: the panel stood 886px on a 667px phone, and the whole page 2076. The order was the problem more than the length. The two figures the panel's own summary glances at — "10 seats · $1/$2" — were the seat count at the top and the blinds 218px further down, under three switches and the three paragraphs explaining them: a dedicated dealer, a card sheet at every street, and whether the phone is allowed to sleep. A dealer setting up a $1/$2 game read all of that on the way to the two boxes they came for. The three have gone into "While you deal", with the paragraphs. That is not a new idea in this panel — House take and Without live betting were already nested disclosures — it is the same idea applied to the things that were not about money. Afterwards the panel is 529px, the page 1719 on the same phone and 1275 on a 1280x900 desktop, and the first two rows are the seats and the blinds. A switch nobody can see is a switch nobody remembers turning on, so the disclosure carries a glance of its own: "Asking for cards", "House dealer", or "Screen may sleep" when one of the three is away from where it ships, and silence when none of them is. One of them, not a list: joined they run to 49 characters into a row 44px tall with a label already on it, and "House dealer · Asking for c…" has told nobody anything. 1291 tests pass; the sweep is clean at 428 audits across 13 viewports. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e9ec4afafe |
Ask a card the two questions it is, not one out of fifty-two
The picker put the whole deck on the screen at once — four suits across,
thirteen ranks down — and that grid is 646px tall. Measured with the
board tracked and seven cards already out:
640x360 816px of content in a 317px sheet, 40 of the 52 below the fold
667x375 816 in 330, 40 below
844x390 774 in 343, 36 below
375x667 816 in 587, 20 below
1280x900 774 in 775, none
So on every phone the tool is actually used on, entering the flop began
with a scroll, and a dealer looking for the nine of diamonds was reading
down a column to find it. No arrangement fixes that: fifty-two targets at
the 44px this sweep holds everything else to need 572px of width before
gaps if they are thirteen across, which is wider than any phone in
portrait, and thirteen rows deep if they are four.
A card is not one choice out of fifty-two. It is two — a suit and a rank
— and that is how a dealer says it out loud. So the sheet asks them in
that order: four suits on a row, then the thirteen ranks of the one
chosen. Measured afterwards at eight viewports from 360x640 to 1280x900:
nothing below the fold, nothing scrolling, every button still at least
44x44 and still hit-testing as itself. Portrait content went 816 -> 372,
landscape 816 -> 322.
The second tap is not a second cost in the usual case. The suit stays
where it was put, so a second card of the same suit is one tap — and one
tap is what picking a card used to cost *plus* a scroll through thirteen
rows. Opening on a slot that already holds a card shows that card's suit
with it under the cursor, which is the case where the dealer came to
change something rather than add it.
Three things that had to move with it:
- the cursor is a rank now, 0-12, and changing suit deliberately leaves
it alone: the same rank in another suit is the card beside the one
you were on.
- the arrows read the column count off the layout instead of assuming
SUITS.length. The rank row is auto-fill — five across on a 360px
phone, seven at 460 — and the last time this number disagreed with
the grid it slid the cursor into a column nobody asked for.
- the suits are a radiogroup, not a tablist: the grid below is not
another panel of content, it is the same thirteen controls filtered,
and the unchosen red suits stay red, because reading four shapes to
find the hearts is the search this sheet exists to remove.
deck() still returns all fifty-two and the duplicate check still reads
all of it, so splitting the grid by suit cannot let a card be dealt
twice.
1276 tests pass; the layout sweep reports clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
53c99b2d91 |
Tell the sheets the phone is on its side
The action bar learned two rounds ago that a landscape phone is short of
height and swimming in width: it stops being a bar across the bottom and
becomes a rail down the right. The seat menu was never told.
Measured on the fullest menu the tool can build, twelve-handed with
somebody on the waiting list:
640x360 545px of content in a 306px sheet. Cash out…, Move to…,
Edit seat…, Small blind, Big blind and Cancel below the
fold — Cancel by 224px.
667x375 545 in 319, Cancel 211px under.
844x390 529 in 332, Cancel 183px under.
And on all three the sheet was 420px wide inside a window 640 to 844
wide. Two hundred and forty-seven pixels of width held empty on the one
screen shape whose whole problem is height, while the dealer scrolled a
menu to find the way out of it.
The two tiers this menu has been sorted into since round 7 — what you
press with cards in the air, and housekeeping between hands — stop being
the top and bottom of a column and become the right and left of a row.
Fold and All-in land under the same thumb the rail puts Fold, Call and
Raise under, with Cancel beneath them. The housekeeping grid takes as
many columns as the width holds instead of the two a 360px phone has
room for.
After: 297px in a 298px sheet at 640x360 and at 667x375, 281 in 282 at
844x390. Nothing below the fold on any of them, every button still at
least 44x44 and still hit-testing as itself. The move picker's twelve
chairs and its Back went from four rows and a scroll to three rows and
none. Upright nothing moved at all — 545px before, 545px after, the same
two columns.
auto-fill rather than auto-fit: auto-fit throws away the tracks nothing
sits in, so a group of two stretched to half the sheet each while the
group above it was a third, and the two grids lined up with nothing.
1253 tests pass; the layout sweep reports clean.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
2e372d4677 |
Take the glass off the middle of the table
The board — the five community cards a dealer taps to enter the flop, the turn and the river — could not be pressed at all. #seatsLayer is an `inset: 0` sibling of the table that holds the seat pucks, and an element with no background still hit-tests, so the empty middle of that layer took every tap aimed at what was underneath it. Measured at 1280x900 with five cards out: all five hit-tested to `div#seatsLayer`, on every screen wide enough to keep the board in the middle rather than on the strip. #betsLayer, the other sheet over the felt, has been pointer-events: none since it was written. Underneath that, the cards were drawing at a little over half their size. placeBoard measures how much middle the felt has and writes it to the centre's max-width — but max-width caps a box and does not claim anything, and the centre is a flex column that sizes to its content. So with 463px of measured room it went on being 136px wide, the row inside it is width: 100% of that, and five cards sharing 136px come to rest on their own min-width: 24px, against the 44px floor every other control on the felt is held to. Neither was visible to anything. A scripted probe calling .click() on the node skips hit-testing entirely, and the sweep's occlusion check had only ever been shown three selectors' worth of the page — the action bar, the header and the seats. It asks about every control now, which took three refinements to be honest: a control that set pointer-events: none has opted out rather than been covered; a shut <details> hands out a real rect for everything in it, and checkVisibility is the browser's own answer; and flow content under a fixed layer is a position rather than a fault, so it is said rather than failed. The five sheets a dealer spends half the night in were getting one rule of twenty-five. They get all of them now. Three rules had to learn what a modal is first: two ask about the fixed bar, which is not a question while a scrim is covering it, and the third called the bottom four rows of a sheet that scrolls unreachable buttons. The helper all three lean on had its own blind spot — it asked whether an element had left its box entirely, which is true of something well below the fold and false of something hanging eight pixels over the edge of it. That turned up two controls that were a glyph and nothing else: the pot wizard's close, on the one dialog that divides money, and the cross that removes a custom field, in a column of identical crosses. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c1d0105d12 |
Answer the press instead of eating it
The card picker greys out a card that is already dealt and hangs the reason off a `title`. Measured on an iPhone with three hands out: seven of the fifty-two were dead, none of them could take focus, and tapping one changed nothing at all — no toast, no text, not a byte of DOM. The tool's answer to "who has the ace of spades?" existed and was reachable only with a mouse. The header's two buttons were the same, on a new dealer's first screen, and Export CSV was greyed out with no explanation written anywhere. `disabled` is the wrong attribute whenever there is something to say: it drops the control out of the tab order, so a screen reader never lands on it to hear the description, and it eats the tap on a touch screen, where the hover that would have shown the `title` does not exist. `aria-disabled` announces the same state and leaves the control reachable, so one capture listener can turn the press into a sentence — in a toast, or inside a sheet that is standing on top of the toast. It paid for itself in the picker's keyboard too. The arrows hunted past dealt cards because a `disabled` button cannot hold focus, and the hunting did the exact thing the grid's no-wrap rule was written to prevent: Right from A-spades over a dealt A-hearts landed on A-diamonds, so the column shifted and the next press of Down walked a different suit. A cell that can hold focus needs no hunting, and the APG asks for that inside a composite widget anyway. The line that answers is one line by measurement, not by taste. Its box is 318px on a Galaxy S8 at 20px of root text; the first wording drew 332px and wrapped, shoving fifty-two targets 21px down mid-tap, and twenty-four capital Ws draw 434px whatever the wording, so the box is what holds. Two things the sweep found on the way. It opened the card picker by un-hiding an overlay whose grid had never been painted, so for its whole life it audited an empty sheet; it now opens it the way a dealer does, with cards out and the answer showing. And the slot buttons above the grid were named to a screen reader by the em dash they draw when empty. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
2d80edd513 |
Say how many ways once, in the one control that owns it
"Split between" on Quick Split was two controls for one value stacked on each other: chips 2 3 4 5, and under them a bar reading "2 ways" with a − and a +. The count was written down three times at once — the lit chip, the bar, and "Each of 2 players gets" in the panel above — and two of the three were controls. 134px with its label, against 74px for "Smallest chip in play" right below it, which offers five choices in one row. The bar was never meant to be a bar. `.waysStepper` asked for a 150px min-width inside a wrapping flex row, so on a phone it always wrapped to a line of its own, and `flex: 1 1 auto` then gave it all 358px of that line: 252px of empty readout with the − and + pushed 304px apart, on a screen 390px wide. Now it is 136px with its ends 121px apart, and it only exists once you are through the door — a fifth chip reading "6+", behind which the stepper is where the count is written. Under six ways, which is every chop a home game will run, the field is one row and 74px. Two more things went with it: Neither stepper ever disabled an end, so − at the floor and + at the ceiling looked live and swallowed the tap. NN/G's stepper guidance asks for the opposite. Both ends of both steppers now grey out. The per-pot chop capped its + with `Math.max(contenderCount, MAX_WAYS)`, which is always 12 — a side pot only P1 and P2 were eligible for would happily chop ten ways at $12 each. It is `chopCeiling()` now, and a two-contender pot is offered no stepper at all, that being the common shape. The sweep went straight to the all-in mode and audited only that, so the mode the screen opens in had never been looked at in fourteen rounds, which is how a 358px bar lasted this long. It audits both now — 428 audits, clean — and the first run of it found the step buttons had been 40px wide rather than 44 since they were written. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
de4331de72 |
Make forty-four pixels mean forty-four pixels
Every input and select a dealer touches has a rule asking for min-height: 44px, and not one of them was 44px tall. Form controls are content-box by default, so 44 was being read as the height of the text inside, and 8px of padding top and bottom plus a 1px border each side went on the outside of it. Measured on an iPhone: all fifteen settings fields in the sidebar stood at 62px, and the raise pad's amount box at 66 with the text turned up. Nothing looked wrong, which is why it lasted. 62px clears a 44px floor, so the sweep — which asks whether a target is big enough, never whether it is the size it was asked to be — had nothing to say, and one field is not obviously too tall. The cost is only visible in the aggregate: Table Setup 941px to 869, the waiting list 212 to 194, every panel in the sidebar open 2,993 to 2,813. The raise pad's field was worth measuring rather than assuming, because it is not only a field: it sets the action bar's height, the bar's height is published into --feltRoomPx, and the felt is laid out from what is left. It takes 10px off the bar and hands them to the table. On a Galaxy S8 twelve-handed at 20px text the bar goes 248px to 238 and seat 7's stack comes out from behind it — with the taller field the sweep reported "$1,100 is 7/7 under div#actionBar", a figure a dealer cannot read at all. Written against the element types rather than the surfaces they sit on. The surface version was written first and had already missed the waiting list's name box, two hundred lines from anything it named. This is the third time this stylesheet has been caught by content-box; the panel headers and the rulings summaries are the other two. The new test holds every field given a 44px floor to being measured the way that floor assumes. 1167 tests, 418 audits clean. |
||
|
|
e0536de14d |
Let the Chip Legend show a legend instead of the form for typing one in
The panel measured 793px on an iPhone and 420 of them were the colour form: five rows of swatch, value box and a filled red delete block, then an add button and a paragraph of instructions. Underneath all of it sat "Stacks in chips" — the fewest chips that make each stack, which is what a dealer opens this panel during a game to read. Setting the colours is something you do once, on the first night, and it was sitting on top of the thing you do at every colour-up. The order is the other way round now. The legend leads: five colours and five amounts on one line, which is what the panel is named for and what it had never once displayed — the values were locked inside number inputs, so answering "what is the green one worth" meant reading it out of a form field. The colour-up list follows. The form goes behind a disclosure, the same shape Table Setup already uses for House take and Without live betting. 793px to 441px, with both the things a dealer reads above the fold. The five delete buttons keep the 44px the touch rules give them and lose the red fill, which made each one as loud as the row it removes and louder than the values beside it. The red comes back on hover, on the way to pressing one. And the colour-up list no longer runs straight into its own footnote. The sweep's twelve-handed "packed" shape joins "all in" as a note rather than a failure at 20px text, with the arithmetic written beside it: on a 360x640 phone the felt is 330x294, the pucks are 44x63, five sit down each side, and 315px of puck does not go into 294px of felt. The pitch between neighbours is 35px and 51px against a 63px puck. They have always overlapped; what kept the run green was the hit-test probes landing between the glyphs, so any change that moved the ring three pixels turned it red for something it hadn't touched — twice this round. Held to the same standard as everything else at 16px. 1162 tests, 418 audits clean. |
||
|
|
b096f054f1 |
Take the chair number off the corner it was never alone in
A seat's number was a grey digit in the top-left corner of the puck, and the top-left corner is also where the blind badge drops off the rim and where a long centred name reaches. Measured across 348 seats on 36 tables: 117 of them had it printed under something else. All 66 seats posting a blind had it covered outright — a 22px badge at z-index 4 over a 6px digit — and 51 more had it sitting on the first letter of the player's own name, the whole glyph in the worst case, on a 54px puck. There is nowhere on the felt to move it to. The button's chip has the other end of the top rim. Bets land on the rim between a seat and the pot, which is the bottom edge for the seats above centre and the top edge for the ones below. The middle of the puck is name, bet and stack, and all three are about this hand. The chair number is the only mark on a puck that isn't, so it is the one that gave way. Nothing is lost that a tap doesn't return. The seat editor's heading reads "Mike from work · Seat 2" now and the seat menu's first line leads with the chair; a seat nobody has named still calls itself "Seat 5"; and an open chair still wears its number large and centred, which is the one moment the number is the thing being looked for rather than a footnote on somebody else's name. The sweep had never mentioned any of this. The check that asks whether a puck covers itself up knew about two of the three things a puck wears, and only ever held them against writing — so the mark sitting on the names was invisible to it, and so was a badge painted straight over another mark. Both gaps are closed: put the number back and it reports five offenders on a Galaxy S8 ten-handed, naming Christopher, Alexandra, Jonah and the SB. 1148 tests, 418 audits clean. |
||
|
|
49c2d745eb |
Let the keyboard into the sheets, and stop dropping it on the floor
Modal answers one question for all six overlays — where focus goes and how Tab is held — and this round it was measured as wrong in three directions. Its list of Tab stops could not see a <summary>. The rulings sheet is twelve of them and one Done button, so the trap decided Done was both ends of the ring: eighty Tab presses, one element ever reached, twelve rulings that could not be opened without a mouse. Adding `details > summary` gives the sheet its thirteen stops back. `:not([tabindex="-1"])` moves onto every line while it is open, because a control taken out of the Tab order still matches `button`. "The first control" was the card picker's Clear: the sheet a dealer opens to write a card down opened on the button that rubs one out. A sheet names its own landing spot now, and on five of six it is the heading — which says which seat, which card, which pot. The confirm dialog keeps a control, because a short question with one irreversible answer should land on the answer that leaves the dealer no worse off. And a sheet that redraws itself was deleting the control that held focus. Choosing a buy-in, picking a card, switching slots, confirming a side pot — all four left focus on the document body. Rebuilt controls carry a name now and are handed back to whoever had them; where there is no coming back, the caller says where to go instead. The card grid was fifty-two Tab stops of the fifty-six in that sheet. It is one now, with the arrows moving inside it, stepping over cards already dealt and stopping at the edges rather than wrapping onto a different rank. 1136 tests, 418 audits clean. |
||
|
|
382238e708 |
Tell the browser the page is dark, and stop painting over it instead
Everything in this stylesheet is a colour it paints. `color-scheme` is the half it cannot: the browser draws the checkboxes, the menu a <select> opens, the scrollbars, the caret, the spellcheck underline and the autofill wash, and with no scheme declared it draws every one of them for a light page. Measured, `color-scheme: normal` on a #141414 ground — three checkboxes in Table Setup rendering as bright white squares, the loudest objects on the screen and brighter than the gold, and two more in the seat sheet doing it over the felt. The comment on the pot wizard's hand-painted checkbox called it "the last raw browser control in the tool". Six others were still the platform's, and three of those were still 13x13 against a 24px floor this file had already written down once — in a comment that explains the floor, attached to a rule naming one checkbox by id. One declaration on :root moves all six, and moves them natively, which is the part worth having: a control the browser draws still answers Windows High Contrast Mode, still matches the platform the dealer already knows, and cannot lose its tick to a stylesheet. `accent-color` makes the checked state the green this tool already uses for yes — the same #1e8449 the painted box fills with, so the one drawn checkbox and the six native ones agree rather than nearly agree. The painted one stays painted, because it is not a setting: it is the box a dealer ticks to move money with players watching, and it wants a fill that reads across a table. But it now pays what painting costs. A tick drawn as a `background` on a pseudo-element is not a colour forced-colors mode knows how to replace, so it was simply gone — no visible checked state at all in Windows High Contrast Mode, on the dialog that pays the table. System keywords are the only colours guaranteed to survive, because they are the palette. Three other things fell out of looking: The id-scoped 24px rule existed three times over — once in the open, once under `max-width: 700px`, once under `pointer: coarse` — and still reached one box of six. Said about checkboxes instead, once. A colour swatch declared 32px wide measured 22 with a mouse: flex-shrink, the exact mechanism the coarse-pointer block had already been bitten by and written a note about. It gets the same `flex: none` the touch case got. Quick Split is the darker of the two pages and had never declared a scheme either. It has no checkboxes to go white, but a dealer opening it from the felt should not find a light scrollbar down the side of it. 1104 tests pass; the 418-audit sweep is clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
8da48e02c8 |
Let the rulings be a list of rulings, not a scroll with a list on top
The sheet a dealer opens mid-argument held twelve rulings with all their prose showing — 2,480px of it on a 390px phone, of which 1,754 was below the fold — and a row of twelve chips across the top to jump between them. The chips were an index of the twelve headings printed directly above the same twelve headings. The same list twice: 172px of navigation before the first word, and the dealer who tapped one landed in the middle of a scroll with eleven rulings they hadn't asked for still wrapped around them. The headings are the index now. NN/g's rule for this shape — a longer list of independent topics somebody opens one of — is an accordion, shut by default, and it suits these unusually well because half of them answer the question without being opened at all. "A short all-in doesn't reopen the betting" is the ruling, not the topic. So is "Chips nobody called come back", and "A straddle is a blind raise", and "A raise has to be a full raise". Measured after: 29px below the fold on a 390px phone with all twelve rows on screen at once, 51px on a desktop, and 244px with the longest of them open. Not exclusive — no `name` attribute. Closing one because another opened moves everything under the finger that is reaching, which is the disorientation the accordion is here to remove rather than add. And the sheet shuts what it finds open when it is opened again: <details> keeps its state, so without that a dealer who read the odd-chip rule an hour ago finds it still hanging open under the one they came back for. Two things that came with it: The gold arrives when a ruling is opened and not before. Twelve gold headings in a column is the accent doing the job of body text, which leaves it nothing to say when one of them is the one being read. The sidebar's eight panel names are --text for the same reason; this is the same component doing the same job. Five of the twelve are two paragraphs, and they ran into each other — the sections were spaced from one another but nothing separated the halves of one ruling. It never showed while there was a gold heading every few lines to break the column up. 1095 tests pass; the 418-audit sweep is clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
71c198cf2e |
Make "up to" true on the screen that pays the table
The pot wizard printed a figure beside every name under the words "up to". It was that player's own remaining contribution, which is not what winning pays them, and the gap was large and in the wrong direction. Three players in for $250 each: all three rows read "up to $250" while the prompt directly above them said "$765 is in play" — and $765 is what the winner was handed. A dealer who trusted the row would have short-paid by two thirds. With a real side pot it was worse in a more confusing way. The breakdown box at the top of the same dialog announced "Main pot $765 · all 3", and Bob's row three lines under it said "up to $250". One dialog, two numbers for the same money, and the wrong one next to the checkbox the dealer actually ticks. resolveRound caps the pool at the winner's own remaining contribution and then takes min(remaining, cap) from every entry, dead money included — so the ceiling is that sum, and PotCalculator.ceilingFor computes it. Bob's row now says $765, which is the main pot, which is what the breakdown box three lines above has been saying all along. It is also only said where it binds. A ceiling equal to everything on the table is the prompt's own figure repeated once per row — redundant data-ink, and on a pot everybody covered that was all it ever was. It now appears on exactly the rows where picking that winner leaves a side pot still to award, which is the one time a dealer needs telling. The flat pot's three rows carry a name and nothing else; the side pot's carries one number, on Bob. The test that matters is not that the ceiling equals a figure written out by hand but that it equals what resolveRound pays, so every case checks the label against the function that moves the chips — across seven shapes of pot, ragged contributions and cent-sized stakes included. 1082 tests pass; the 418-audit sweep is clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
9af65a7b82 |
Sort a seat's eleven actions by when, not draw them all the same
The long-press menu had grown to eleven actions and drew every one of them as the same full-width bar. On the fullest menu the tool can build — a live seat that isn't the dealer, blinds still the only money in, an override already set — that came to 796px and took 85% of the screen on all four viewports measured. It scrolled on all four, and Cancel was off the bottom of the screen on all four: 79px past it on a 390px phone, 252px on a 360px one, 448px in landscape. Apple puts the comfortable number for an action sheet at three to five and says not to let one scroll at all. Nothing is cut. Every one of the eleven is the only route to something, and "Blinds follow the button again" is the only route in the whole tool to clearing a blind override. They are sorted by *when* instead: a seat action is either something you do with cards in the air or housekeeping between hands, and those two want different targets. The hand keeps the full-width bars — Fold, All-in, Un-fold, and seating the next player off the rail, because a chair opens between two hands with people watching it. Everything else goes to a two-column grid of quieter 44px tiles underneath. The captions are not decoration. A half-width tile reading "Small blind" is a label, and the puck already wears one of those; under "Give this seat" it is a verb again. That is what lets three of the longest labels here lose ten characters each and still fit a half tile on one line, which is the height the caption pays back. The override reset spans the grid rather than sitting in it as a fourth position to give, dashed and unfilled — the felt's own word for "nothing set by hand here" — so it doesn't read as the Cancel below it either. Same fullest menu afterwards: 650px, 77%, no scroll on a phone or a desktop. What a dealer actually sees mid-hand is eight actions and 546px, 65%. The move picker, which was eleven more full-width bars, now fits all eleven chairs of a twelve-handed table on a 360px phone without scrolling. Two things the height was hiding, fixed with it: The header is stuck to the top of the sheet's scrollport and carries a real close button. NN/g is explicit that a grab handle is not a close affordance, and this is the sheet that can still outgrow a landscape phone — so the way out, and whose menu this is, are now in the part that cannot scroll away. That close button is also the first control in the markup, which is what Modal focuses. It used to focus Fold. The APG says to keep a dialog's initial focus off the destructive choice; a close button at the top puts it there without a special case. Escape finds that one rather than the button at the foot, so Escape closes the sheet while the foot steps back — and it now has somewhere to step back to: opening the move picker used to replace the sheet and leave only a control that threw the whole menu away. 1068 tests pass; the 418-audit sweep is clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
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 |
||
|
|
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 |
||
|
|
d4c39f209a |
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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 |
||
|
|
3326557043 |
Merge pull request 'Hold'em: give the seat holding the dealer button its own middle back' (#121) from fix/holdem-dealer-chip-eats-its-seat into master
Build and Deploy / build-deploy (push) Successful in 1m12s
Reviewed-on: #121 |
||
|
|
edf159325b |
Give the seat holding the button its own middle back
On a fresh table the button is on seat 1, and seat 1 is where the first player goes. Tapping it did nothing. The dealer chip is a 22px disc, and a 22px disc is half the 44px a fingertip needs, so it carried an invisible pad to catch the tap. The pad was moved twice looking for somewhere to grow that wasn't already spoken for: outward first, where it hung over the seat next door and gave that seat the button; then inward, over "its own seat, which is a large target that can spare the corner". The seat could not spare it. Measured on a 390px phone, a 72x46 puck: the pad ran from the puck's own centre to its right edge and from its top to 3px off its bottom, and the chip's handler calls stopPropagation. The seat holding the button kept 52% of its face — 130 of 276 sampled points went to the chip, its exact middle among them. Every other seat kept 97%. The button moves every hand, so the dead puck went round the table with it. And the chip it went to had nothing to say. Its only answer is "give the button to this seat", and this is the seat that has it. The seat menu already knew that and hides "Give the button" there; mid-hand the same no-op would have taken the blinds off the felt and re-posted the same chips to announce that nothing happened. So the pad is gone rather than moved again — a fixed pad cannot be safe at every puck size, and no finger reaches any chip now: the ghosts are pointer-events: none without hover, and the one live chip is this one, which is now inert to the pointer and still there to read. Giving the button is the long-press menu on touch and the revealed ghost on a mouse, both unchanged. makeDealer returns early on the seat that already has it, so the keyboard can't re-post blinds to move the button nowhere either. The sweep had a check for exactly this — "a seat's centre belongs to that seat", written when the pad was reaching outward — and it ran clean through all of it, because `seat.contains(hit)` waves through anything inside the seat and the chip is inside the seat. The check written for this bug could not see the second half of it. A control inside a seat is a rival for the tap now, and against the old stylesheet it fails seven scenarios. 982 unit tests, 6 of them new; the layout sweep is 320 audits with the one pre-existing failure (a $1,300 chip behind seat 11 on a twelve-handed 360x640 all-in). Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6e7b047d3e |
Merge pull request 'Hold'em dealer: a table that reads its own showdown, and a tool that is there with no signal' (#120) from polish/holdem-game-night-ready into master
Build and Deploy / build-deploy (push) Successful in 1m14s
Reviewed-on: #120 |
||
|
|
171e878262 |
Give the felt back the sixth of the phone the title was holding
The header stood 120px tall on a 390x844 phone and only 46 of that was a control. The other 74 were a 22px page title with a gold rule under it, a back link, and a gold "Hand #1" pill — three things read once, above a screen whose whole subject is the green table underneath. Sixteen percent of the window, spent before the felt started. A phone held sideways had already solved this. The landscape block clips the title and puts everything on one row, because at 390px tall there was no other option. Upright never got the same answer, and upright is how this tool is held. It is 44px now, the table starts at 78px instead of 152, and the felt went from 496px to 570 — about fifteen percent more table on the same screen. Nothing left the page: - The title is still the document's <h1>, clipped rather than removed, so it is still the heading a screen reader lands on and still what the browser tab and the installed app are called. - The back link keeps a 44px box and loses only the word "Casino", which is what every phone does with a back link. The name a screen reader gets is on the <a> either way, and it contains the visible word on the sizes that still show it. - The hand number moved to the action bar's status row, in front of the street. That row already says what hand you are in — the street, the pot, the player count — and this is the fourth fact of the same kind; a counter read after the fact had no business holding a line of the header all night. It costs the bar nothing: the five audit scenarios that report the bar's height report the same 249, 227, 203, 227 and 227 pixels as before. The three buttons take the width the title gave up rather than shrink-wrapping to the right of it, which is 26px of extra target each. `flex: 1 1 0` was wrong here — it divides the row into equal thirds and squeezes "Next Hand ⟳", 105px of text, into 99, and these are nowrap, so what gives is the label. An `auto` basis makes the label the floor and the spare width the bonus. On the narrowest phone in the sweep the arrow and all three buttons come to 323px of 336. The arrow was 32px on the first pass and the layout sweep caught it at 31x43 on every touch viewport there is — 56 scenarios, one control. The word had been carrying the width; with the word gone the arrow is the whole target. 44 square. The sweep is down from five failures to one. Four of the five were seats and bet chips colliding on a 360px Galaxy S8, and 76px more felt was all they needed. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
0f7213ec2d |
Open a seat on what is on it, and put the sheet in front of the table
The seat sheet is the second-most-used surface here — every buy-in, every rename, every correction goes through it — and it was the least finished one. Seven things were stacked flat at one weight and all open at once. Measured on a 390px phone it came to 594px and scrolled, and the three controls a dealer touches on nearly every visit were 132px of that. The chip counter alone was 174px, the tallest single thing in the sheet, for a job most tables do once a night or never; the two hole-card slots were 62px of dashed grey on every table that never asked to be shown a card. Cards, the contribution and the custom fields are behind a fold now, and which of the three stand open is a question about the seat rather than a setting: a seat holding cards opens its cards, a seat with chips counted opens its count, a seat with a note on it opens its notes. A shut section says the answer on its own row — the cards, the total, how many notes — so nothing already entered is further away than it was. An empty chair is 419px and does not scroll; a seat with everything filled in is 625 and opens all three. The fields have the width the sheet always had. `.fieldRow input` caps at 140px, which is right in a 320px sidebar of settings and wrong in a sheet holding a whole phone: "Name" sat against the left edge with 203px of nothing between it and a field a third of the row wide. Same 44px control, 300px instead of 140. The chip counter's values are text. They were a `title` attribute, which is a tooltip, which is nothing on the device this is used on — five identical number boxes told apart by five coloured dots. It also left the wrapping <label> with no text in it, so they had no accessible name either. And the sheet has a scrim. It was the only one of the tool's seven overlays without one, while covering the felt from 250px down and the action bar with it — modal in everything but appearance. What stayed lit above it was the header, with Misdeal, Next Hand and Resolve Pot live under a thumb reaching for a name field. It dismisses on a tap and on Escape now, like the other six; a card picker opened from inside still takes Escape first. In landscape the scrim stops where the sheet stops, so Fold, Call and Raise keep the rail. It is put away with visibility rather than opacity, which is the same lesson the sheet above it already learned about transforms — and the layout sweep caught it the same way. At `opacity: 0` a fixed, full-window box is invisible and still entirely there: bottomFurniture() read it as furniture standing 640px tall at the bottom of a 640px phone, in every state and on every screen, and asked scroll-padding to reserve the whole window for it. Ninety-eight scenarios failed on it; hidden is hidden, and the sweep is back to the five it started with. The toast steps over the sheet the way it already steps over the action bar. Both are fixed to the bottom of a phone, and a "Keeping the screen on" toast was landing squarely on the seat form with its Undo and its × live above the fields. The height is measured and published, because it changes the moment a section is unfolded. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
853fcedea0 |
Measure the text the way an eye reads it, not the way a ratio scores it
The sweep has checked WCAG AA contrast for a long time and the page passed. That result was worth less than it looked. WCAG 2's ratio is a luminance gap, and a luminance gap overstates readability once one colour is near black — which is every reading on a page whose ground is #141414. Tuning a colour until it clears 4.5:1 there lands on text that passes the audit and still reads as a smudge across a table in a dim room. Several rules in this stylesheet said as much in their own comments, one of them "measured 4.5:1 at #a5a5a5 exactly, so it sits above that": the right instinct through the wrong lens. The check also could not see half the backgrounds it scored against. It reads background-color and takes the best contrast of any candidate ancestor, so text on the felt was scored against the dark page behind the felt. Two plain AA failures sat in a clean report for as long as the check has existed — the chair number on the brown rim at 2.65:1, and the pot's own label, gold on green, at 2.66:1. runContrastAudit.js scores with APCA and gets the background by screenshotting the page with its text made transparent and reading the pixels, which no gradient or shadow can fool. The first version walked the DOM and called the gold dealer button black-on-black at Lc 0.4; that false positive is why it works this way. Found: 65 of 92 pieces of text below their level, 54 passing AA at the same moment, worst Lc 28. Now nothing is below Lc 60, nothing fails AA, and the worst is Lc 60.4. Three fixes worth naming, none of them "make the grey lighter": - A player's stack was secondary text at Lc 44, under a name in --text at a heavier weight. That says a dealer needs the name more than the number; during a hand the opposite is true. Hierarchy comes from size and weight now, which is free, rather than from fading, which on a dark ground is the most expensive way to say it. - An empty seat had no background of its own, so the felt decided how readable its number was: Lc 28 on the rim, 30 on the green, 33 further in. It has a ground now, at 0.62 and not the 0.82 first written — contrast is nearly flat across that range and the heavier wash put four dark rectangles on the felt like holes. - The pot's label sat on the one green in the design. The disc is deeper rather than the label bleached, because gold on dark green is the pairing this page is built on. Deepening only its outer stop moved it two points: the highlight is at 32%/28% and that is exactly where the label sits. apca.js is checked against the eight published reference pairs, png.js against round-tripped fixtures for every filter. Four fault injections caught; a fifth turned out to be a no-op — the Paeth predictor is symmetric in its first two arguments over all 16,777,216 byte triples, so that swap is untestable, which is written down rather than papered over with a fixture that appears to catch it. Still below the strict APCA level: 55, all of them 10-12px labels that would need white type or a bigger seat puck. Reported, not chased, and said out loud in the sweep's own notes. Layout sweep unchanged: 5 FAILs, 14 offender lines, no regressions. 950 tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
295d9eff4a |
Stop a scroll being an edit, and a stack changing without being touched
A focused <input type="number"> is a spinbutton, and a browser lets the mouse wheel drive it. Measured on the running page at 1280x900 with the sidebar open: focus the small blind and scroll, $5 becomes $5.50. Focus a seat's stack and scroll, $500 becomes $501. Both go straight through to localStorage, because these fields save on `input` like everything else here. Nothing is announced and nothing looks wrong. The tool has invented a chip, on a table whose whole purpose is that the chip count is right, and the gesture that did it was a dealer reaching for another panel. The rule is narrow on purpose. Every other way of driving a spinbutton is deliberate -- the arrow keys, the stepper arrows, typing -- so scrolling is the only one taken away. Verified after: the value holds, one ArrowUp still moves it, typing still works, and the page still scrolls. Blur rather than preventDefault. Cancelling the event stops the value moving and stops the page moving with it, which trades a silent bug for a sidebar that mysteriously will not scroll -- a problem the dealer can see and cannot explain, instead of one they cannot see at all. The decision lives in Money as a predicate beside selectsOnFocus, so the rule is testable without a browser and main.js only does the blurring. Two things checked rather than assumed. The tagName guard in the predicate is decoration: deleting it passes every test, because nothing but an <input> carries type="number". That is now said in the comment instead of being left to look load-bearing. And the page's one <select> -- which way the blinds move -- is not vulnerable: focused, hovered and scrolled 300px in Chromium it stays where it was, so it is left out of the rule rather than defended against. 916 tests pass, 6 of them new. Four fault injections into the predicate are caught; a fifth was applied, not caught, and turned out not to be a fault. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
fafbd8df32 |
Stop the things that actually move for anyone who asked them not to
The stylesheet had honoured prefers-reduced-motion for a while, in the three
places it mattered least: a 2px press-down on a button, a checkbox tick, and
the acting seat's pulse. What none of them covered was motion that moves
something across the screen, which for anyone with a vestibular disorder is
the part that matters. A button dipping under a thumb is not what makes people
ill.
Four things were doing it, and the three largest were in JavaScript where a
stylesheet cannot reach: the seat editor sheet, which slides the full height
of itself up from off-screen every time a dealer taps a chair; the toast; and
three scrollIntoView({ behavior: "smooth" }) calls, the most provocative of
the lot because what moves is the whole viewport rather than one control.
The scrolls go through Render.bringIntoView now, which reads the setting per
call rather than at page load -- a dealer who turns it on mid-session should
be taken seriously without reloading a table they cannot afford to reload. The
toast keeps its fade and loses its travel, because something that appears
already in place reads as a rendering glitch rather than as news. The sheet's
visibility delay flips at once, or it would leave a sheet that has already
jumped away still painted for a quarter of a second.
The press-down block held nine :active selectors and looked deliberate. It was
written to stop a stuck press state when a tap turns into a scroll, and had
reached for the reduced-motion query as the nearest proxy -- so it covered
.dealerChip and .raiseQuickBtn while missing .actionBtn, the class on Fold,
Call, Raise and Post blinds & deal. Read as its own rule the membership is not
a judgement call: every :hover and :active transform in the file belongs
there. There are sixteen. Nine were listed.
The scan that found the other seven took two goes. A line-by-line version
walked straight past a deliberately broken build, because the injected rule
was written on one line and neither "opens a rule" nor "is a transform"
matched. Parsing blocks caught it -- and then failed on the clean file, which
is how .toastAction, .toastClose and .potWizardChoice came to light: a
multi-line group had been truncated to its last line, so nothing had ever
looked at them.
910 tests pass, 16 of them new. Six script and stylesheet fault injections are
each caught, and the reduced/no-preference split is verified in a real browser
against both context settings.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
0d878a176d |
Say in the sweep's own notes where the last five failures actually live
315 of 320 audits pass. The five that do not are one root cause on the two smallest screens: twelve seats want about 60px of footprint each and the ring has 44px of rim to give them, in the two states that make the bar tallest. Written down rather than left to be rediscovered, because closing it is a design decision -- drop the bet chips at extreme crowding the way the tight tier already drops the hole cards, cap the seat count by viewport, or let the felt scroll -- and each trades something a dealer can see. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
7216f3da4d |
Stop a wrapped label costing twelve seats their room
The sweep had carried the same residual for rounds: on the two smallest phones with twelve players, seats painting over each other's names and stacks. It read like a geometry problem. It was a wrapped label. "ALL IN" is the only thing that ever lands on the stack line with a space in it. A stack figure has no spaces, so it can only overflow, and the puckAmountLong/Huge sizes exist to stop that; nothing had asked what the line does when it can break. Measured at 360x640 with twelve players all-in, it wanted 36px and had 34, broke onto a second line, and an 11px line became 22. That is not a typography complaint, which is why it survived: it looks fine. The puck went from 55px tall to 66, and puck height is what the ring is short of -- at the ends of an ellipse consecutive seats are separated vertically, so a puck 11px taller is a puck 11px further into its neighbour. The layout search then did exactly what it is written to do: tried every width it has, found none that cleared, and fell back to the narrowest, which is also the one that wraps hardest. So the rule every design system gives a badge applies here too: one line, always. Not wrapping is only half of it -- a line that cannot wrap and still does not fit paints over the seat next door instead -- so on the tight tier the mark gives up its letter-spacing, 3.6px of the 36 it wanted, and keeps the warm colour and the bold weight that do the same job. Halving the overlap did not clear it, and the rest of the shortage was not on the felt. The sweep had been saying so in a warning rather than a failure: at 360x640 the bar was 267px of a 640px window, 42%, against its own "persistent chrome under 30%". Two things in it were being shown when they could not be used. The side-pot list is decision support, and during a run-out there is no decision to support -- it collapses to the main pot and one line for the rest. The status row said the player count twice in the same breath, as a chip and again inside the run-out sentence beside it. Measured: bar 267 -> 249 on a Galaxy S8 and 267 -> 227 on an iPhone SE, felt 243 -> 283 there, worst seat overlap 34px -> 4px. Across the sweep, overlap offender lines 33 -> 9, chips behind seats 30 -> 5, failing audits 6 -> 5, and the iPhone SE's seat-text overlap gone entirely. No new failures. 894 tests pass, 9 of them new. Seven fault injections are each caught. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e2bb935b43 |
Be there when the signal, the screen and the storage are not
Three failures that only ever happen at a table, none of which show up on a desk with a good connection and a plugged-in laptop. The offline promise depended on how the URL was spelled. Razor Pages routing is case-insensitive, so /Casino/TexasHoldem serves the identical page; a service worker's scope is matched as a plain string prefix, so a worker scoped to /casino/ does not control it. The registration succeeded, the worker activated, nothing was logged, and the cache stayed completely empty -- because the precache list was handed to navigator.serviceWorker.ready, which resolves with the registration controlling the page and so never resolves at all outside its scope. A dealer who opened that URL to load the tool for the night had loaded nothing. One spelling wins now, via a 301 scoped to /casino rather than applied site-wide: every file under wwwroot/casino and wwwroot/images/casino is already lowercase, so lowercasing there cannot miss a real file on a case-sensitive filesystem. That guarantee does not hold for the rest of the site, and /chessHub would break. The handshake is fixed rather than left to be saved by the redirect -- it takes the worker off the registration itself. The manifest icons were never cached, because the shell list is read off the DOM and the DOM never mentions them. They are read out of the manifest now, cache-first so an offline launch stops logging ERR_INTERNET_DISCONNECTED, and the link sweep picks up apple-touch-icon, which is the only one iOS reads. The screen slept mid-hand. A wake lock is held while betting is open and dropped the moment it is not: through a whole street of renders it takes exactly one, two syncs in a tick take exactly one, a hand that ends inside the request leaves nothing held, and the browser taking it back leaves us able to take it again. Announced once with a Turn off button rather than a permanent header indicator; a setting after that. And localStorage is evictable, which the Save & reset panel now says out loud. persist() is asked for on Post blinds & deal, because every browser wants that request to come from something the dealer did. 885 tests pass, 18 of them new. Five fault injections into the wake lock are each caught. One of the new tests was green for the wrong reason first: globalThis.navigator = fake does nothing, because Node has defined navigator as a getter since v21. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
75ec028c3e |
Ask for the hands at the showdown, the way the streets are already asked for
The card feature had two halves and only one of them ever asked a question. Turn the
setting on and every street opens a sheet for the cards coming off the deck — that
worked, on a phone and on a desk, in a normal hand and in an all-in run-out. The
other half never asked at all. A dealer who entered all five community cards, pressed
"Showdown — resolve pot", and got a wizard saying
Christopher, Mike from work, Alexandra and Bob haven't turned over, so who
won is still yours to say.
with nothing in that sheet to turn anybody over with. The only route in was to close
the wizard, tap a seat puck, find the two card slots in the seat editor, do it four
times and re-open the wizard, with the pot on the table and four people waiting. The
one moment the tool is certain to know the hands is the moment it stopped asking.
So the showdown button asks. The sheet opens on whoever the bar has just named as
showing first, takes their two cards, and moves round the table from there; hands
already entered are skipped, because a hand entered is a question already answered.
Four sheets later the wizard opens reading "Chris wins $40 with three of a kind,
nines" and its button says "Pay Chris" instead of "Confirm Winner(s)".
Leaving the sheet stops the asking rather than skipping a player: closing it says the
tool has the wrong end of the stick, and the answer to that is to get out of the way.
It isn't a dead end — the wizard still names whoever didn't turn over, the bar still
says "Showdown — resolve pot", and pressing it again resumes at the player the dealer
stopped on, because the list is read off the cards on the table rather than remembered.
CardPicker.close() takes a reason so the chain can tell "that sheet filled its last
slot" from "the dealer left". Two callers pass nothing and don't care.
The setting kept its behaviour and lost its jargon: "Track the board and read the
showdown" was the feature's name rather than what turning it on does to your evening.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
e90bcccada |
Spend the accent on the one thing to do next, not on two at once
The green is a promise: this is the step. Measured over a whole hand — seventeen steps — it was on two controls at thirteen of them, the bar's Call and the header's Resolve Pot painting the identical gradient at opposite ends of a phone, and on the header alone at the other four while the bar held the only button there was to press. At showdown the most consequential tap in the hand, "Showdown — resolve pot", was blue-grey while the corner shortcut was green. So the accent moves to .actionAdvance, which is always the next step by definition, and comes off the header shortcut, which never is. Twenty-two measured steps of a real hand now show exactly one. The sweep checks it: oneNextStep() counts the accent-coloured controls that are actually on screen, scoped to an open sheet when there is one. Six broken versions were run at it, and the two that took three goes to catch were both the same mistake in the injection rather than a hole in the check — a button painted green with the panel shut isn't rendered, and one painted green below the fold is deliberately ignored. Scrolled into view it is named alongside the bar's Call, and goes clean again when the green comes off. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
14fb68dc04 |
Merge pull request 'Hold'em dealer: making the table and the bar fit the screen they are on' (#119) from polish/holdem-round-next into master
Build and Deploy / build-deploy (push) Successful in 1m40s
Reviewed-on: #119 |
||
|
|
4a1cb842cb |
Say on each shut sidebar panel the one thing you'd open it for
The sidebar is eight <details>, and every one measured exactly 60px shut with nothing on it but its own label — 480px of a phone's screen, and 480px of a 660px desktop column, spent saying "Standings", "Hands", "Waiting list". Whether the box balances, whether anyone is waiting for a seat, how many hands have been played: the page had all three worked out already, and charged a tap for each of them, on rows two thirds of which were empty. That is the disclosure pattern's named failure — a header you have to open to find out whether it was worth opening — and the answer the design systems give is the same one: put the status on the header. The panel keeps the sentence. "Off by $300" is the alarm; which seat is holding chips nobody wrote down is a paragraph, and that is the half that belongs behind the tap. Two rules, and panelGlance.test.js checks both. A glance appears only when it has something a dealer would act on — eight rows that always say something are eight rows that say nothing again, and a row reading "Nobody waiting" all night is the clutter this went in to remove. And a glance never says something the panel under it would contradict: it is read off the same state, never off the panel's DOM, which for a shut <details> isn't rendered at all. Absolutely positioned rather than floated. A <summary> only draws its disclosure triangle while it is display: list-item, so it cannot be a flex row; the float that replaced it kept the row at 60px but aligned to the label's line box, and a 12px string in a box sized for 15px text rode 2.5px low on all six panels. Out of flow is also the smaller promise — an absolutely positioned box cannot make its row taller at all. Three paths deliberately skip renderAll, because it would take the field out from under whoever is typing in it, and all three move something a glance reports. The notes textarea, the chip legend's value fields and commitSeatChange each call renderPanelGlances on their own instead; without that the note you had just typed was summarised by the note before it until the next tap landed. A space between the label and the glance, too: the summary is announced as one button, and without it a screen reader was given "Table Setup9 seats". Costs 0.08ms of a 4.79ms renderAll on a twelve-handed phone. Four injected faults in the wording are caught one test each. glancesFitTheirRows took three goes. The first asked only whether a row had grown taller than a bare one, and a glance floated back into flow and allowed to wrap sailed straight through it, because a float does not expand the box it sits in. The second was caught by the sweep on its first clean run — 94 failing configs, 91 of the offenders this check's own: it takes the shortest row as its idea of a bare one, and the Keyboard panel is display: none under pointer: coarse, so on every viewport visited with a thumb the bar was a row that wasn't drawn, and all seven real rows read as grown. Rows that aren't rendered aren't rows. The four broken layouts are re-run with that panel hidden and each is still caught. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
e7ddff2fed |
Let a large felt have larger seats, and the type that goes with them
The felt grows with the window and the pucks did not: 96px was the only ceiling there was. Nine-handed, the seats covered 19% of the felt on a phone, 11% on a 1280px desktop and 8% at 1440px and beyond — and the stack figure a dealer reads sat at 10px on a 2560px monitor, the same 10px as on a 360px phone, well under every published floor for text meant to be read. Two things stop this costing more than it buys. Every candidate width is a forced layout. Simply raising the ceiling took the layout pass from 24ms to 73ms twelve-handed on a desktop, so a screen that cannot use the wider widths must not measure them: the extra pass is offered only when the arithmetic already says the seats have room to spare, and a phone measures exactly the ten widths, in the same order, that it measured before. Timed with the order alternated so neither carries the warm-up: 10.7ms against 10.4ms on a phone, and +3 to +6ms where the widths are actually used. And a wide puck on a narrow felt is worse than a small one. The first version of this let phones through, and a 144px puck on an iPhone's 278px ring is half the width of the table: six-handed, all six seats sat over the rail by 15px against 5px before, reading as a stack of cards rather than a table with people around it. So the ceiling is a fifth of the felt's shorter span, capped at 144. Five is measured rather than chosen — the shorter span is 233-303px on every phone in the sweep including sideways and 617-714px on tablets and desktops, so any divisor between 3.2 and 6.4 separates them — and the test pins both ends. Above the ordinary ceiling the puck takes the larger type as well, which is the point of the extra width. Nine-handed: an iPad goes 96px/12px to 134px/15px, a 1440px desktop 96px/12px to 140px/15px, and the seats go from 8% of the felt to 12%. Every phone, at every seat count, is untouched. The sweep is byte-identical to before this change: same six configurations, same 63 offenders, empty diff. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f8ed4abc85 |
Stop two empty rows in every seat puck setting the size of the table
A seat puck carries the name, the hole cards, the amount that player has put in, and their stack. Two of those are empty most of the time and both held their space anyway. The amount pill was worse than empty space. It has a background, a pill radius and side padding, so with nothing in it, it painted an 8x11px green lozenge — measured on seven of nine seats at an ordinary table. One of those is a component holding its place; nine is a rash, and a dealer has to learn to ignore a colour that on the other two seats means "this player has money out". The space was not free either. seatRingRadii sizes the ring from the tallest puck on it, so two rows nobody had filled were setting how far apart the whole table had to sit. Across 21 tables — seven viewports, six, nine and twelve handed — every single puck comes back shorter and six come back wider: an iPhone 14 nine-handed 78x88 -> 78x63, twelve-handed 44x65 -> 66x55, which is the one name in the set that was clipped and now is not. The collapse is only safe because the layout cache moved with it. resolveSeatLayout measures ten candidate widths, each a forced layout, against seatShapeSignature() — which listed the names and the dealer seat, everything that could wrap a name onto a second line. It did not mention these two rows, because while they always held their space they could never change a puck's height. Now they can, so the signature names them: booleans over the whole table, because measuredSeatBox takes the tallest puck on the ring, and because a per-seat key would be correct and would throw the cache away on every tap. The ring settles by 24-26px when the blinds go in — and it settled by 23-25px there before this round too, since the action bar changes height at that moment and the felt is measured against it. The one place it moves is a place it already moved. uiAudit gains emptyDecoration(): nothing on the felt may paint a background or a border with no content in it. Its one exemption is on the list because the check reported it rather than because it was predicted — .betChipDisc is a circle filled with the denomination colour of the chip beside it, so the colour is the content. Three others were written from memory first and matched no element on the page at all. The sweep is unchanged at 6 failing configurations and 63 offenders. All four table failures are the all-in shape, where every pill is filled and the collapse buys nothing; the ordinary tables this helps were already passing, so what moved is room a name has rather than whether anything was covered. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
942919f3f7 |
Stop the side-pot list growing the action bar over the felt
The bar is fixed over the table, so its height is the felt's budget — and it carried a pot breakdown that drew one row per all-in with no ceiling. On a 360x640 phone: five players idle put the bar at 151px, six all-in at 303px, twelve all-in at 411px. That is 64% of the screen, leaving 96px of felt to draw twelve seats in, and at that size the seats stop being a table: five pairs collapse onto each other's centres, ten hang past the rail and a dozen labels sit completely covered. Past four layers the bar now draws the main pot, one line for everything between, and the newest side pot. The two ends are the live ones for the same reason — side pots are paid from the newest back down to the main pot, so the top layer is the next thing that happens and the main pot is the last, and those are the two a dealer says out loud while the chips are stacked. The middle is the rest of that count-down, wanted at showdown, which is the pot wizard: same builder, no cap, in a dialog that scrolls. The elided line carries the count and the money, so the visible rows still add to the announced pot. Nothing in the sweep could see any of this, and the reason is worth keeping. Every check asks whether an element is covered or clipped; none asked how much room the covering element had taken. But the deeper miss was the sweep itself — all 294 audits drove their table with Call and Check, so no table it ever measured had a player all-in, and the one thing on the bar whose height depends on the table was empty in every measurement. There is now a fourth table shape, twelve players shoved in for twelve different amounts, and furnitureBudget() measures the fixed band against the window: failing over half of it, warning over the 30% that persistent chrome is usually held to. Galaxy S8, twelve all-in: bar 411px -> 267px, felt 96px -> 192px, 91 layout offenders -> 25. iPhone SE: 73 -> 10. The rest is the seat ring on a short phone, which the sweep can now see. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
02c2a034cb |
Stop the fixed bar and the toast sitting on the keyboard's focus
WCAG 2.2 SC 2.4.11 asks that a focused control not be completely hidden
by something the page has put on top of it. The bar is fixed over the
bottom and is not in the flow, so the browser does not know it is there:
Tab to a control near the bottom and it is scrolled flush against the
edge of the window, under the bar, and the browser considers it done.
Tabbing the whole page measured 16 stops resting completely covered on a
390x844 phone and 27 more partly, plus four under a toast on the desktop.
It is now 0 and 0 on desktop, phone and a phone on its side — clean
against 2.4.12 (AAA) as well, which allows no overlap at all.
Three things were wrong and they needed different answers.
scroll-padding-bottom on the scroll container, sized from the bar's own
published height and the toast's, handles the case where the browser has
to scroll to reach the control. That is half of it, and the half that
looks like all of it: a control the browser already considers visible is
one it will not scroll to, and a fixed overlay on top does not enter into
that decision. Measured — tabbing to Common rulings with a toast up left
it 100% covered and scrollIntoView({block:"nearest"}) moved nothing. So a
focusin handler measures the overlap and scrolls by it, and when the page
has no room left (26px against 94px wanted) the toast gives way instead.
Sizing the page padding for the toast would have fixed that by making the
page grow and shrink 64px whenever anything was announced.
The page's room to scroll past the bar was 9rem on the sidebar alone,
which left the table's column — most of a desktop — with none. It is now
on the whole app and taken from the bar's real height, so it is also 0 in
the layout where the bar is a rail down the side.
And the seat editor's bottom sheet was put away with a transform, which
moves a thing without putting it away: its Done button sat at y=850 on an
844px screen, focusable, announced, and impossible to scroll to, because
a fixed element does not move when the page does. visibility:hidden puts
it away, transitioned so the slide-out still runs.
uiAudit.js gained focusHasRoom() and focusablesReachable() for the two
static halves. Audit unchanged at 2 failing configurations and 3
offenders, both pre-existing. 805 unit tests pass.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
cd2db6346e |
Stop a chip colour taking the table's money with it, quietly
Deleting a chip colour was one click of an unlabelled 24px button, and it was never a change to a legend. computeContribution() prices a seat by running the legend over that seat's chip counts, so a colour that leaves takes every chip counted in it off the table. Measured in a browser on a three-handed table: deleting the $500 row took $22,500 of $26,874 with it. No confirmation, nothing said afterwards, and no way back — a colour added again is a new colour with a new id, while the counts stay keyed to the one that left. The counts were left behind too, so the saved file carried money nothing could price and nothing could show. Now: a colour nobody has counted still goes on one click, and one that is holding money asks first, naming the amount, the chips and the seats, and saying that Undo will put it back while adding the colour again will not. Conditional on purpose — a confirmation that fires every time is one nobody reads by the third night, and four others already compete for that attention here. Removal goes through runUndoable, and the counts go with the colour so the file matches the screen. The number in that warning is the difference between pricing every seat with the colour and without it, not chips times value. The obvious arithmetic is wrong both ways: a seat with a typed amount is never priced from its counts, and a seat whose chips all leave falls through to the table's standing ante rather than to zero. A randomised check over 2,000 tables asserts the warning equals what the table then does, and it found a third case — fourteen $1 chips against a $200 ante, where removing the colour puts the table UP $186. The wording says so in that direction too. The × also had no accessible name, so uiAudit.js gained controlNames(): every control needs a name containing a letter or a digit, resolved the way the accessibility tree resolves it. A symbol is not a name. Taking the new aria-label off again makes it report all five buttons. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 805 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6eb97c8a35 |
Give the sidebar eight names instead of five and a cupboard
"Session" was a container word rather than a name. It held the standings, a waiting list, the hand log and the save/load/reset buttons — a topic, a task, a log and three utilities under one label, which is a navigation layer with more than one logic in it. It measured 1206px and 103 visible elements, against 555 and 29 for the next biggest, and "The table itself" sat 1052px down inside it. Reaching Reset table meant opening a panel called Session and scrolling past the money and the log to get to it. It is now Standings, Waiting list, Hands and Save & reset, each named for the thing a dealer came looking for, in the order of a night: set the table up, watch the money, fill the seats, look back at a hand — and, last and apart, the two buttons that end it. Reset table goes from 1052px inside a panel called Session to 62px inside one whose label says what it does. The biggest panel drops from 1206px to 607px. The four subheads that used to carry those names are gone, because the summary says it now. Every panel name is also a heading. A <summary> is announced as a button, so the sidebar's names — its whole navigation — were unreachable by heading, and splitting Session would have removed the only four headings down there. The outline is now h1 page, h2 per panel, h3 for the two subheads that remain. Visually identical: the h2 gives up everything a heading brings with it. The cost is 216px more collapsed summaries at rest, three more one-line labels. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 795 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
bd8ee11f77 |
Say who can win each pot in the fewest names that answer it
The pot breakdown listed every eligible player for every pot. Pot eligibility is nested — the main pot is everyone still live and each side pot above it drops whoever was all-in below — so the main pot's line was every name at the table and each side pot repeated it minus one. The lines too long to fit were exactly the ones nobody needed spelled out, and the short ones at the bottom were the side pots a dealer actually announces by name. Measured on an iPhone 14: a single all-in five-handed already clipped the main pot, and twelve-handed the first row wanted 521px of a 254px slot. There was no count and no title, so a dealer reading "Alexandra…" could not tell whether three people or eleven were eligible — the number the showdown turns on. Each line now names whichever of "who is in" and "who is out" is shorter, which nesting guarantees is never more than half the table, and leads with the count because the line is right-aligned and it is the tail that gets cut off. "all 9", "8 in, not Mike from work". On a phone this takes one, two and three all-ins from every main-pot line clipped to nothing clipped at all; twelve-handed goes 6 clipped rows to 5, and even a cut row still opens with "9 in". The wizard's copy of the same announcement wraps instead — it is a dialog that scrolls, and it is what the dealer reads out before anybody shows a hand — so nothing there is ever cut off. The full list is also on the title for anyone with a pointer, which is the extra rather than the answer, since a tooltip is not a mechanism a keyboard reaches. potCalculator.test.js now proves the nesting across 20,000 random tables and asserts on its own generator, so the loop cannot go vacuous. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 787 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
13d704301d |
Stand the felt up on a phone, and check which rule actually wins
A portrait phone got a square felt, so the vertical room the layout had already measured was published and then dropped: 522px of room under a 366px table on an iPhone 14, 638 under 406 on a Pro Max. The cost was seat names — 44px pucks with five of twelve clipped. --feltAspect: 1.5 spends it. Twelve seats at 390x844 go from a 44px puck with five names clipped to 66px with one; five seats go from three clipped to none; the Pixel 7's twelve-seat ring goes to zero. 1.6 and 1.7 measure byte-identical, so 1.5 is where the sweep stops paying. The three smaller phones are room-bound at any cap and measured identical from 1.15 to 1.7. The first version of this was a no-op. The rule went into @media (max-width: 700px), and @media (max-width: 900px) below it set the same property back to 1 — both match a phone, same specificity, later one wins. It measured as 1 on every phone while reading perfectly. The 900px rule is now a 701-900px range, and feltShape.test.js resolves --feltAspect through the media queries the way the cascade does so the next one of these is caught. Also tried and removed: a floor under the max-height, so a tall bar couldn't press the table flat. Twelve all in with nine side pots takes the bar to 341px, leaving 142px of room at 360x640; floored to 242 the felt keeps its shape by putting five of twelve seats behind the bar, unfloored it is flat with all twelve visible. The slit it defended against came from forcing the custom property by hand. A test asserts its absence with the numbers. Audit: 4 failing configurations and 5 offenders down to 2 and 3, both remaining ones pre-existing. 776 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
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 |
||
|
|
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 |
||
|
|
fbc4bd9a3e |
Merge pull request 'Hold'em dealer: a felt that fits, and a table that reads its own showdown' (#118) from polish/holdem-seat-collisions into master
Build and Deploy / build-deploy (push) Successful in 1m11s
Reviewed-on: #118 |
||
|
|
cd57c16050 |
Let the table tell the dealer who won
An option — off by default, and off means the tool is exactly what it was.
Turn it on and the community cards go on the felt. Each street asks for the cards as
it is dealt: the picker opens on the flop the moment the flop button is pressed, and
again on the turn and the river, because the moment a dealer is holding those cards is
the only moment they know what they are without turning the board round. Three taps,
then one, then one. The picker is the same sheet as the hole cards, which now knows
about the middle of the table too — the seven of clubs cannot be on the flop and in
somebody's hand, and until now it could.
Turning the players' cards over is unchanged. That already worked the way it should.
What is new is what happens next. Resolve Pot stops asking who won and says so:
Alexandra wins $50 with three of a kind, nines.
with every hand at the table read out under its player's name, the winner already
ticked, and the button reading "Pay Alexandra". A chop comes out as "Christopher and
Mike from work split it, $22.50 each, with a pair of aces", both ticked. The dealer
still confirms — the tool reads the cards, a person still says the pot is good, and a
hand entered wrong is corrected by unticking rather than by starting over.
It declines rather than guesses. A board that isn't five cards, a player who hasn't
turned over, the same card entered in two places: each says which of those it is and
hands the question back, and the wizard is the one it always was. Who is *ahead* and
who *won* are different claims and only the second one can pay somebody.
Side pots are read per round, not once. The best hand takes the main pot and is then
out of the question; the side pot goes to the best of who is left, which is a different
answer and the one the tool has to ask for again.
handRank.js is the only module here that can pay the wrong person without anybody
noticing, so it is built to be checkable rather than clever: rank five cards, then take
the best of the twenty-one five-card hands in seven. Beside the written cases — every
category against every other, the wheel and the steel wheel, three kickers deep, the
counterfeited two pair, the board that plays — it is checked against a second evaluator
written a different way, on twenty thousand random seven-card hands and four thousand
random pairs of them. An ace that keeps its value in the wheel, a dropped two-pair
kicker and a flush read only by its top card were each introduced on purpose and each
caught.
The board takes the middle of the felt, sized from the room the seat ring actually
leaves — measured against every seat rather than the ring's widest point, which is only
the answer for a seat sitting exactly on the horizontal axis and had two pucks on top
of the board while claiming 77px of clearance either side. Where there is no middle to
take, it moves to a strip above the table: a phone on its side has a felt 528x83 with
55px pucks, so the ring closes over the centre entirely and a board in it is a board
with players painted on top.
That strip found an older bug. It costs the felt height, and a felt that changes size
had only ever been re-measured, never laid out again: the seats follow it on their own,
being placed in percentages, but a bet chip anchored inside a seat is placed in pixels
off that seat's centre. Measured at 640x360, the chips were positioned for a felt 83px
tall and painted on one of 60 — seat 3's $5 came to rest on seat 4's puck, and two
pairs of seat pucks were overlapping. The bar growing mid-hand does the same thing: a
side pot appearing is 55px off the felt. Both fixed.
The sweep grew a third table — the same five players in the same nine chairs as the
second, with the board on, so the only thing that differs between those two rows is the
board itself. It earned its place on the first run.
728 unit tests. 216 audits: what is left is the seat ring hanging past the rail on a
landscape phone, the known residual, plus five or six pixels more of it on the layouts
where the strip takes the felt's height, and a blind badge over a stack on a packed
twelve-seat portrait table.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
2a080eab6a |
Give every bet on the felt somewhere to be
The chips were the last thing on the table nobody had measured, and the sweep had never seen one: it audited a table that had never had a bet placed on it, so a third of what is on the felt during a hand went unlooked at for the tool's whole life. It posts blinds, deals and calls three times now before it measures anything. What that found, and what this fixes: - Chips accumulated. Seats are kept and updated in place rather than rebuilt, so clearing the bets layer stopped clearing the ones anchored inside a seat and nothing else ever did. An iPhone 14 had one chip per seat after the deal and five by the third render; a dealer's every tap left another disc on the felt. - The ring-or-cling decision asked how wide the window was. A phone held sideways is 844px across and gives the felt 117px: it passed the width test, took the ring, and every chip on it was painted under a puck — the seats carry z-index 1 and the bets layer carries none, so a chip that lands under one is gone, and with it the only number saying what that player has in. Measured now, and only measured. - Getting clear of a box was `edgeReach(puck) + edgeReach(chip)`, which is right only straight up, down or sideways. Two axis-aligned boxes are clear when they are apart on one axis, so the travel is the reach of the box you get by adding their widths and their heights. At 768x1024 a $10 leaving a seat on the diagonal wanted 88px and got 77, and came to rest 11px inside its own player's puck — every seat off the four compass points, which is most of them. - Every chip was laid out at the first chip's size. A chip is as wide as the number on it: "$5" is 21px and "$10" is 27px on a phone, so each $10 was placed as if it were 6px narrower than it is. The widest is no better: at 768x1024 one 47px chip made the whole table give up a ring the other four had room for. - Anchored chips settled where they landed. At 640x360 the felt is 528x83 and a puck is 55 of those pixels tall: clearing it takes 41px, the players facing each other are 70px apart, and both chips set off 41px into a gap that is 70 wide. Three overlapping pairs on a twelve-handed table. Each chip now gives ground only to something it is actually standing on, a pixel at a time — a midline rule or a single scale factor both punish the seats at the ends of a rail, which were never going to hit anything. - Nothing knew the pot total was there. Seat 1's $10 was sitting on "Pot $65". Two things off the felt, from the same sweep: - The toast outlived its news. "Next hand — button on seat 2. [Undo]" stayed up while the hand was dealt and three players called: an offer to move the button, wired to a snapshot that predates the hand, so taking it would have thrown the hand away without saying so. It was also in the way of thirty small screens' seats and controls. The bar drops it as it takes the next action; the bar's own Undo names what it would actually take back. - The pot dialog cut a name off. Three players in the pot on a Galaxy S8, two rows visible, the third below a hairline that reads as the end of the list — a dealer ticking the two they could see would have paid two thirds of a three-way pot. The 85vh budget was still subtracting the action bar's height from a dialog that has sat above the bar since its z-index was fixed, and a player's ceiling moves beside their name on a short screen. All three fit now, and where a list does run on, it runs on under a fade rather than stopping dead on a row boundary. The pot badge goes on a phone held sideways: 86x66 of a 692x113 felt, for a number the action bar prints three inches below it, standing in the only band a bet chip has. The audit stopped calling one thing another. An element clipped away by a panel that scrolls is not covered by whatever is painted at that height — the wizard's third winner came back for four viewports as "fully covered by button Confirm Winner(s)". 190 audits: 25 failing configurations to 7, and every one of those seven is the seat ring hanging past the rail on a landscape phone, which is the known residual from the last round. 663 unit tests, 44 of them new. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
f93e0935ef |
Give every seat its own pixels
Six players at a nine-handed table is the most ordinary table this tool draws, and on every phone in the sweep one of them had their stack painted under the player next to them. Measured across nine viewports and six table sizes: seventeen of fifty-four tables hid a name or a stack behind something belonging to another seat. Two causes, and the layout only knew about a third of one of them. The felt decided how big a puck could be from a single number — the straight-line distance between the closest two *neighbouring* seats, judged against a fraction of a puck's width. Distance has one number in it and a puck is half again as wide as it is tall, so the same 84px was generous side by side and nowhere near enough on the diagonal. And neighbours are the wrong pairs to ask about: on a phone held sideways the felt is 528x87, the ring is very nearly a straight line, and the seat that lands on top of you is the one across the table. Ten of the tables in the sweep had a collision that a neighbours-only check cannot see — seats 1 and 7, overlapping by 68px, while every adjacent pair had room. seatsCollide() asks the real question instead: do any two of these boxes want the same pixels. Two equal axis-aligned boxes overlap exactly when their centres are closer than one box in both axes, so it is an answer rather than a proxy, and it looks at every pair. The second cause was the small blind badge, sitting at `left: -6px` — hanging off its own puck's left corner onto whatever was beside it. On an iPhone Pro Max at six seats, with the pucks not touching at all, "SB" was painted across the next player's $1,137. All three badges now sit inside the puck's width, and in the same place as each other: a seat only ever wears one, so two positions bought nothing except making the dealer's eye find which of two spots this seat was using before it could read which blind it is. Then the layout had to be able to act on the answer, and it couldn't. It chose between two widths — 96px or 58px — and if neither fitted it widened the ring and let the seats overlap, on the reasoning that a dealer can see an overhang but cannot see a tap landing on the wrong player. That trade is real; it was also a false choice, because the tables that overlapped did not want either number. At nine seats a Galaxy S8 had room for a 61px puck, an iPhone 14 for 68, a Pixel 7 for 74, an iPhone Pro Max for 79. Three media queries declared three more of these numbers — 78px, 64px, 62px — at three breakpoints, guessing at something a media query cannot see: the same 390px phone has room for a 90px puck at four seats and 61 at nine. So the width is searched for now, not chosen. render.js walks candidates from the top, applies each one, measures what the browser actually drew, and takes the widest that leaves every seat its own box. There was a width that worked on every table in the sweep; the code was never asking for it. Ten candidates is ten forced layouts, so the result is cached against the felt's size, the seat count and the names — measured 12.5ms cold, 0.8ms warm, and renderSeats runs after every tap. Three things this got wrong on the way, all caught by measuring rather than reading: The badge overhang was folded into the space between two seats. That collapsed the ring on a phone held sideways — 87px of height, and 18px of badge is most of the room the ring has to move in — until a six-handed landscape table was unsatisfiable at any width. It didn't belong there: the badge points *away* from the table at both ends, so two seats facing each other across the felt wear them back to back. box-sizing: border-box, which the puck needed so the searched number means the width it is drawn at, quietly turned every min-height from a content height into a drawn one. An empty chair went from a 52px tap target to a 42px one on every phone in the sweep. The audit found it; the four min-heights have each gained the padding and border they used to be quoted without. The new audit check reported a name under a neighbour on a table whose seats were 9px apart. A puck's name is a `-webkit-box` with `line-clamp`, and getClientRects() hands back a rect for every line the text *would* have taken, including the ones the clamp throws away — so the probe was hit-testing 30px below the puck, where nothing is painted. It clips to what is painted now. uiAudit gains "no seat covers another seat's writing", the complement of the check already there for a puck's own decoration on its own name — which is why nothing caught this: seatLabelsClear() only ever looked inside one seat. Fault-injected against the previous version: eighteen configurations, naming both causes. And the sweep itself now runs two table shapes. It had run one for its whole life — five players at a nine-handed table — and how crowded the felt is depends on how many people are sitting down far more than on anything a viewport can report. A full table was never once looked at. 632 tests, 0 failures. 190 audits across thirteen viewports at two table shapes, with a mouse and with a thumb. Every table from two seats to twelve now keeps its own text on every viewport in the sweep. One configuration still reports: a dozen players on a 640x360 phone, where the felt is 87px tall and two rows of 55px pucks cannot both fit and stay on the rail — the ring grows 8px past the edge to keep the seats off each other, which is the trade the old code named and got right. Left reported rather than tuned away. Names on a crowded phone table truncate harder than they did, because the puck is now sized to fit instead of allowed to overlap: "Christopher" is "Christ…" at nine seats on an iPhone 14. The full name is in the DOM, in the seat's form and in the sidebar. The next lever on that is the seat layer's inset, which is still a fixed 44px of "half a puck" for a puck that is no longer 96px wide — worth 54px to 60px where it was measured. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
190ccebf6c |
Merge pull request 'Hold'em dealer: a reachable Confirm, a table that fits, offline, and a quieter first screen' (#117) from polish/holdem-offline-and-render into master
Build and Deploy / build-deploy (push) Successful in 1m42s
Reviewed-on: #117 |
||
|
|
c052ee32c8 |
Put the Confirm button back on screen
Opened the pot wizard — the one dialog in this tool that divides money — and its Confirm button was below a scroll. Not on a tall table, not on one screen: at every size I measured, with six players and a side pot. iPhone 14 390x844 244px of the dialog below the fold Galaxy S8 360x640 417px 844x390 landscape 583px desktop 1280x900 172px Nothing on screen said there was anything under the list of names, so a dealer with the table waiting had to scroll a list of players to find the only control that finishes the hand. Two goes at this, and the first was wrong. A sticky footer is the obvious answer and it cannot work here: the actions were the last child of the scrolling content, so their containing block ended exactly where they did and `bottom` had no room to lift them. Sticky on the *header* did work, for the mirror-image reason — its container extends below it — which is what made the failure confusing to read. Capping the player list wasn't enough either; the pot breakdown and the log still overflowed on a short screen. So the dialog is a proper column now: a title that doesn't move, a body that scrolls, and a footer outside the scroll. potWizard.js appends its buttons to the footer instead of into the body, and clears both together so a step can't inherit the previous step's buttons. Measured after: Confirm on screen and hit-testable at all six configurations, twelve-handed on a Galaxy S8 included, and the dialog itself no longer scrolls at all. The checkboxes are painted now, too. `accent-color` only ever coloured the tick — the box around it stayed the platform's own bright white square, which on a near-black dialog was the loudest thing on screen and the last raw control in a tool where everything else had been drawn. Same surface ramp, same corner radius, and a tick drawn with clip-path so it can't pick up a substituted font. uiAudit gains "an open dialog's buttons are on screen", which hit-tests rather than trusting geometry — a button can be inside the viewport and still be under something. Fault-injected: putting the footer back inside the scrolling body reports Confirm 105px below the fold. Two things checked and left alone, because they turned out to be right: the standings table hides three columns on narrow screens through a container query keyed to the sidebar rather than the viewport, which is why a desktop shows fewer columns than an iPad — the sidebar is the narrower box there. And In/Won/ Net read $0 for hands settled outside the wizard, which the comment above them already explains. Caught by my own guard on the way past: the token test read `scroll-padding` as `padding` and failed the spacing scale. Correct by its rules, wrong in substance — those values are a sticky bar's measured height, not a step in the rhythm — so the lookbehind now rejects a hyphen as well as a word character. Re-fault- injected to confirm it still catches a real ad-hoc gap. 619 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes and 35 across the five overlays. The whole wizard walked end to end: side pot resolved, hand logged, $1,560 on the table before and after. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
1f3dda180f |
Make the table big enough for the game around it
Looked at the felt at three times scale and the seats were falling off it. Not
sitting at a rail — hanging past the outer edge of the table into the page,
half a puck on green and half on nothing. Measured:
1280x900, nine-handed five of nine seats past the rail, worst 49px
844x390, six-handed all six, worst 20px
430x932, six-handed two, worst 12px
360x640, nine-handed none
The ring sat at a flat 44% by 42% of the seat layer at every size, two numbers
picked against a phone. A fixed pair can only be right for one ratio of puck to
felt, and the felt's ratio changes with every viewport — so it was a different
amount of wrong on each one, which is exactly what makes something look like a
mistake rather than a choice.
The radius is derived now: the widest ring whose outermost seat still finishes
inside the table, worked out from the measured table, seat layer and puck. It
can only ever pull seats in — the old pair is kept as the ceiling — and it falls
back to that pair whenever there is nothing to measure, which is every call from
the unit tests and the first render before the felt is laid out.
Three things this turned up on the way, each caught by the sweep:
* The seat layer is not centred on the table. Its insets differ top from
bottom, so the first version — which assumed symmetry and worked from half
widths — left a phone with 12px still over the rail. It works in absolute
pixels against the nearer edge now.
* The radius and the puck size depend on each other: the ring is limited by
how big a puck is, the puck is sized by the `seatsTight` class, and that
class was decided from the gaps between seats, measured at a radius that
hadn't been worked out yet. Fixing the gap measurement then made a
nine-handed phone overhang that had been fine, because the two were chasing
each other. It is settled by trying the candidates in preference order
instead: the roomy layout if everything fits, the tight one if not, and if
neither fits, tight pucks on a ring widened only as far as it takes to stop
seats colliding — because two seats fighting over a tap is a worse failure
than a puck over the rail. A dealer can see an overhang; they cannot see
that the seat they pressed wasn't the one that answered.
* The spacing threshold was a flat 84px, the figure a 96px puck was judged
against. Asking a 68px tight puck to keep a 96px puck's distance is a test
it can never pass, so every landscape table fell through to the fallback and
took an overhang it didn't need. It is a ratio now, not a number.
Result: worst overhang anywhere goes 49px to 9px, and 59 of 65 configurations
are completely clean. Desktop, tablet and every phone held upright: nothing over
the rail at all.
Residual, reported rather than tuned away: six configurations still overhang by
1-9px, all of them landscape at 640x360 and 667x375, where the felt is about
110-125px tall and no ring puts readable pucks inside that. The audit check is
left strict so they stay visible.
uiAudit gains "seats and chips stay on the table", which runs at every viewport
in the sweep. Fault-injected: putting the flat 44/42 back reproduces the original
failure exactly, including seat 3 at 49px.
Faster, too, because the measuring got hoisted out of the per-seat loops while I
was in there:
renderSeats 7.3ms -> 1.8ms
renderAll 12.4ms -> 6.8ms
619 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes.
Offline re-verified: a whole hand with the network cut, $800 conserved.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3ffce9d7c1 |
Stop the empty table shouting, and point at the next step
The first screen a dealer ever sees was the one nobody had looked at. Opened
fresh, with nothing saved, the tool showed:
* the word EMPTY, in capitals, ten times across the felt
* Resolve Pot as the accent-green primary — on a table with no players and no
pot, so the loudest control on screen was the one that could do least
* Next Hand, with no button to move
* no indication anywhere of what to do first, except a grey caption under the
felt saying "tap a seat to edit"
An empty state is the one screen a new user is guaranteed to meet, and this one
described itself ten times without once saying what to do.
**The felt stops repeating itself.** An open chair is already dashed instead of
solid, faded, and has no stack under it — that reads at a glance, which is how a
dealer looks at a felt, and it reads the same on the one open chair at a full
table, which is the case the label was actually for. The word is gone from all
ten, and the seat numbers underneath it are now the thing you see. What the word
carried for anyone reading by ear moves to an aria-label on the seat, which now
says more than the sighted version ever did: "Seat 3, empty", "Seat 1, Ann".
**The bar says what the table is.** It used to hide itself when nobody was
seated — silent at exactly the moment a dealer has never used this before. It
now says "Nobody's at the table yet." with one full-width button, Seat a player,
which opens the seat editor on the first free chair. That is the same panel
tapping a seat gives them, so the button teaches the gesture rather than
replacing it.
**Two dead controls stop pretending.** Resolve Pot and Next Hand are disabled
until somebody is at the table, which takes the accent green off the button that
cannot work and leaves the only green on screen on the one that can. Disabled
rather than hidden: hiding them would move the header the moment the first
player is named, and a control that vanishes teaches nothing about what it was
for. Next Hand's own explanatory title is kept and handed back when it
re-enables.
One bug of my own, found by walking the flow rather than by reading it: after
typing a name both header buttons stayed greyed out, still captioned "nobody is
at the table yet". commitSeatChange refreshes narrowly on purpose — a full
render on every keystroke takes the name field out from under the dealer
mid-word — and the comment directly above it already warns about this exact trap
for the action bar. The header needed the same line.
609 tests, 0 failures. 65 layout audits across 13 viewports x 5 table sizes,
0 failures — the empty table is now one of those sizes, and had never been
audited before. Re-verified with the network cut: the first-run screen, the CTA
and seating a player all work offline.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
5d5093f3cc |
Make the dealer tool work with no signal, and stop it shouting
Three findings this round, all measured on the running page.
**1. The tool needed the internet to exist.**
The night already survived anything — it lives in localStorage — but the page
did not. Reload in somebody's back room with one bar of signal and a dealer
holding a phone in one hand and a deck in the other got a browser error page
with nine players waiting. Verified: with no worker, an offline reload is
net::ERR_INTERNET_DISCONNECTED.
There is now a service worker (wwwroot/casino/sw.js) and a manifest, so the
tool installs to a home screen and runs with the network switched off. Measured
end to end: 19 assets cached from one ordinary visit, then with the context
offline the page loads, draws six seats, applies its CSS, and plays a whole
hand — blinds posted, four calls, $800 still on the table.
Three decisions worth writing down:
* The precache list is read off the document rather than kept in the worker.
Every stylesheet and script is already in the DOM; a hand-written file list
stays right until somebody adds a script tag, and then the tool is quietly
offline-broken until a dealer finds out at a table.
* No skipWaiting. A new worker taking over a live page can hand it assets
from a different release than the ones it started with, and this page loads
seventeen scripts that expect to agree with each other.
* Stale-while-revalidate for assets, not cache-first — see finding 3.
Scope is /casino/ rather than /casino/texasholdem/, because a scope has to be a
prefix of the page it controls and "/casino/texasholdem" does not start with
"/casino/texasholdem/". Anything in scope the worker wasn't told about goes
straight to the network.
Program.cs needed .webmanifest added to the MIME map: an unknown extension is
not served with the wrong type, it is not served at all, and a 404 manifest is
exactly the silent failure that stops a browser ever offering "Add to Home
Screen".
**2. A screen reader read out the whole action bar after every tap.**
The bar carried aria-live on the element whose entire contents render() rebuilds
each action. Measured, after one tap:
"PRE-FLOP Pot $15 9 players Bob · $250 behind 0:00 ↶ dealing the hand U
Fold Call $10 Raise"
Nothing in it is wrong; it is unusable at the speed a hand is dealt, and it is
the documented anti-pattern — a live region has to be a stable element whose
text changes, not a container that gets replaced. There is now a small
role="status" element that exists from page load and says one sentence:
"Dee to act, $10 to call. Pot $15." Guarded on the sentence rather than on the
state, because render() runs for many reasons that don't change whose turn it
is, and a live region handed the same words twice reads them twice.
**3. Every tap rebuilt ninety DOM nodes.**
renderSeats did `innerHTML = ""` and then cloned a template, attached three sets
of listeners and set a dozen attributes per chair — on every render, and
renderAll runs after every action. Profiled on a nine-handed table:
renderSeats 7.3ms -> 4.0ms
renderAll 12.4ms -> 9.5ms
The pucks are built once and updated in place, which is what the rest of that
function was already doing to their contents. A seat's node belongs to its index
for as long as the table is that size, so the listeners still close over the
index; only a change in the number of chairs rebuilds the row. Verified that
listeners don't stack: one click after six renders fires the hook exactly once,
and resizing 9 -> 4 -> 10 renumbers correctly.
Found along the way and deliberately left alone: ViewData["cssVersion"] renders
empty on every page of the site, so the ?v= cache-busting has never worked. It
is inert — Program.cs sends Cache-Control: no-store on all static files — and
the service worker was the only reason to care, which is why it uses
stale-while-revalidate instead. Noted in TexasHoldem.cshtml.
608 tests, 0 failures, including a new announcing.test.js. 52 layout audits
across 13 viewports x 4 table sizes: 0 failures, and the action bar still
doesn't change height mid-hand. Gestures re-verified after the seat refactor:
double-tap fold, long-press menu, give-the-button, and the active dealer chip.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
aaf96543a8 |
Merge pull request 'Hold'em dealer: blind fix, design tokens, and the decrowd' (#116) from fix/naming-a-blind-deals-the-hand into master
Build and Deploy / build-deploy (push) Successful in 1m36s
Reviewed-on: #116 |
||
|
|
2e9c55af21 |
Give the felt back the screen the chrome was using
Measured on a 390x844 phone, six-handed, same table before and after:
before after
header 193px 120px
felt starts at 225px 152px
controls on screen 27 18
bordered elements 37 22
text elements 61 49
A quarter of the screen went on chrome before the dealer saw the table.
**The header.** Six equally weighted buttons in two rows, of which two are
per-hand controls. The other four have gone where they belong rather than into
a "more" menu — that relocates crowding instead of removing it. Reset Table is
in the Session panel now, beside Clear history and the save/load pair: it is
the control that throws the night away and it was sitting in the same weight as
Resolve Pot, the most-pressed button in a session. Quick Split is down with
Common rulings, where the other "open something else" link already lived. The
way back to the site is a quiet link at the top, where a back link goes.
The subtitle is gone. It was one sentence of orientation on a tool used
nightly, and the landscape and short-phone rules already hid it — the layout
had decided it was expendable on every screen this is actually used on.
**Teaching copy, once.** Table Setup shipped open, so the first screen was the
most crowded one; it now opens only on a table nobody has set up yet, decided
once at load so it cannot reopen itself under a dealer who just shut it. The
gesture hint under the felt goes after the first deal of the session — keyed to
the hand number, not to bettingOpen, which was the obvious test and the wrong
one: it would have appeared and disappeared twice a hand, shunting the panel
stack up and down all night.
**Borders.** Panels had a border, a lighter background and a shadow — three
ways of drawing one edge. The standings had a rule under every row, which is
what makes a list read as ruled paper. Both drop the line and keep the
grouping; the standings header keeps its rule, because that one marks a change
of kind. Fields and buttons keep theirs: those say "interactive", which is not
decoration.
**The felt itself.** Names wrap to two lines instead of ellipsising, so
"Christopher" and "Mike from work" are readable rather than "Chri..." and "Mike
fro..." — the truncation was eating exactly the half a dealer typed to tell two
Mikes apart. And a stack of $1,234,567 rendered at 7px; it now drops the
thousands separators before it drops the type size, so the same number is eight
characters at 10px with every digit intact. The comment above fitAmount was
right that abbreviating loses precision — but precision the dealer cannot read
is not precision.
Two things tried and reverted, both measured:
* Reserving the second name line made every puck the height of the tallest
(69-73px became a uniform 88). Fifteen pixels on all nine seats is felt
spent on chrome, which is what this round is trying to give back.
* Dropping the name a type step to fit more on one line still did not fit
"Christopher" on the button seat, where the dealer chip takes 22px of the
row — so it cost legibility and bought nothing.
Known limit: a single word too long for a puck breaks across the two lines, and
gets a hyphen only where the browser has hyphenation dictionaries. Real phones
do; headless Chromium does not, so audit screenshots show "Christo / pher". The
whole name is visible either way, which it was not before.
599 tests, 0 failures. 52 layout audits across 13 viewports x 4 table sizes
(2, 6, 9, 12 seats) and 20 across the five overlays: 0 failures, and the action
bar still does not change height mid-hand at any of them. The back link was
caught by the audit at 56x13 when it stopped being a button; its hit area is
44px now, paid for with negative margins so the header keeps the 73px.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
8b43614b1c |
Give the stylesheet a vocabulary, and stop the bar moving
The tool read as crowded on screens where nothing overlapped, and the reason turned out to be arithmetic rather than taste: holdem.css used 42 different spacing values, 31 font sizes, 14 border-radii and 139 colours, and defined three custom properties. Every number had been picked by hand at the moment it was needed. A 0.42rem gap beside a 0.45rem gap beside a 0.5rem gap gives the eye no rhythm to lock onto, so it reads the whole surface as noise. So: constrained scales, and everything snapped to them. Seven spacing steps on an 8px rhythm, eight type sizes, four corners, and a four-step surface ramp that stays off pure black — shadows are invisible on #000 and white-on-black smears on the OLED phone this is used on. 661 values rewritten; colour mentions halved, 381 to 174. Nothing here changes what the tool does. Two things were measurably moving under the dealer's thumb, and both are fixed by reserving space rather than by animating anything: * The undo pill named the last action, so it resized on every single tap. Measured across one hand: 148, 110, 109, 108, 109, 110, 120px. It is now a fixed 10rem, with the label in its own element so it can ellipsize without truncating through the keycap beside it. * The action bar is fixed to the bottom, so anything that changes its height moves its top edge. The status row wraps, and it wrapped differently as the street and pot changed. It now holds two lines' worth of room whenever a hand is running. Across 13 viewports x 3 table sizes, the bar no longer changes height mid-hand at all. Numbers that change while the dealer watches — pot, stacks, bets, standings — are now set in tabular figures, so a pot ticking $18 to $21 stops shuffling its digits sideways. Two regressions the sweeps caught on the way, both from the snapping: * The Add Chips field cleared 44px by way of its font size plus its padding, so moving either dropped it to 42. It states its own min-height now; a touch target shouldn't be an accident of two other numbers. * The bar came out 2px shorter in landscape, which grew the felt, which lifted the top seat's dealer button 2px into the header. That clearance was structurally 1-7px, so the top seat inset became a floor rather than a ceiling instead of chasing the 2px. tests/holdem/designTokens.test.js keeps the scales honest — a new value has to be a step or an exception with a reason. Fault-injected four ways. 599 tests, 0 failures. 39 layout audits across 13 viewports and 3 table sizes, plus 35 across the five overlays: 0 failures. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
42b9e83e8f |
Don't deal the hand when the dealer names a blind
The seat menu's "Make small blind" / "Make big blind" / "Blinds follow the button again" all re-posted the blinds whenever `Betting.onlyBlindsIn(state)` was true. That condition means "nothing has happened in this hand except the blinds" — which is true of a hand one second old and equally true of a table sitting in setup with no cards out and nothing committed. So naming a blind before the deal ran repostBlinds -> beginHand: betting opened, two stacks docked, the action bar up and waiting on the seat after the big blind. The dealer had said where the blinds go and the tool heard "deal". Betting.blindsArePosted() is the question these three actually wanted — the blinds are down only once a hand is open — and it's the guard makeDealer was already using by hand. Before the deal, naming a blind now moves a badge and nothing else; the toast says "Dee has the small blind next hand." rather than claiming she posted it. Mid-hand behaviour is unchanged: chips come back and go in again from the new positions, and the chip count stays straight. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
419c1de2a0 |
Merge pull request 'Hold'em dealer: 55 polish rounds, one merge' (#101) from release/casino-holdem-polish into master
Build and Deploy / build-deploy (push) Successful in 1m32s
Reviewed-on: #101 |
||
|
|
470fbc0d26 | Add the seat-name round to the release | ||
|
|
c4e842f3e3 |
Stop the button's chip sitting on top of the name underneath it
A phone screenshot, mid-hand, eight-handed: the seat with the button reads "Christop" and then a white D. The name is centred across the whole puck and the chip sits in its top-right corner on a layer above, so a long name runs underneath and stops — measured, "Christopher" lost 20px of its 64px box. The bad part is the silence. The text believed it had the room it was being painted out of, so no ellipsis appeared: nothing on screen said the name went on. On the one seat at the table a dealer looks at most. The name now keeps out of the chip's corner on the seat that has the button, so a long one ends in an ellipsis before it gets there. Only that seat — every other puck has its full width, because the chip on those is invisible. And the audit has a check for it now. Nothing it had would have caught this: the occlusion check is about controls a thumb has to reach, and this is a label a dealer has to read. The new one measures every seat's name against its own chip and badges, and the fix was undone by hand to confirm it fires — "20px under button.dealerChip.active". 182 audits across 13 viewports, both input kinds, plain and straddled tables, every overlay: clean. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
1d9353c94e | Add the hidden-means-hidden round to the release | ||
|
|
8616636212 |
Make hidden mean hidden, and check that it does
Three rounds inside the money, so this one was a look at the thing. On a desktop
screenshot of a table mid-session there is a small empty box under the bank
check: 21 pixels wide, 44 tall, no text, no purpose. It is the "Take $600 from 3
players" button from three rounds ago, which sets `hidden` when there is nothing
to record — and `hidden` was losing.
The attribute carries its `display: none` from the browser's own stylesheet,
which is the weakest rule in the cascade, so any class that sets a display beats
it. Every button class here sets `inline-flex`. This is not a new discovery in
this file either: seven overlays already carry their own `[hidden] { display:
none }`, each one presumably found the same way. One global rule now covers all
of them, so there is no ninth.
The audit had been telling me. Its touch check reported the box as "could not be
probed (drawn 21x44)" — in the warnings, which my sweeps printed only sometimes
and never failed on. So the check now says the plainer thing: an element that
claims to be hidden and is drawn anyway is a fault, not a note. It is in runAll
with the rest, and the bug was put back by hand to confirm it fires.
The same warning stream had a second thing in it: the waiting list's Add button
was 43px wide. Three letters, one pixel under the rule, and a thumb doesn't know
the word is short.
182 audits across 13 viewports, both input kinds, every overlay and a table with
a rake, a cash-out, a waiting list and six logged hands on it: clean, and the
only note left is the one that says the 44px rule stood itself down on a mouse.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
6174981cdc | Add the assumed-contributions round to the release | ||
|
|
4db6f6e4d3 |
Take the chips the pot was priced with, not just pay them out
Two rounds of asking where the tool disagrees with itself, and here is the last of it — the one that was actually creating money. There are two ways a contribution gets in. Tapping the action moves chips: the bet leaves the stack, sits in front of the player, and is swept into their hand total. The other is the "without live betting" panel — a base pot amount, or chips counted out on a seat — for a dealer recording a hand they are watching rather than driving. Nothing leaves anybody's stack there. The pot wizard prices both the same way, and awarding it credits the winner. So in the second mode the table simply grew. Measured at a three-handed table with a $50 base pot: stacks 200, 200, 200 before, pot priced at $150, and afterwards 350, 200, 200 — six hundred dollars of chips became seven hundred and fifty, and the bank check, newly able to see it, blamed a rebuy nobody had made. So before the pot is pushed, whatever was assumed rather than taken comes off the stacks that assumed it, and is recorded where a swept bet lives. Chips already committed or swept are left alone — they have been paid once, and taking them twice is the same bug the other way round. A base pot on top of a live bet collects only the difference. Nobody is charged more than they are holding, and an empty chair or a seat sitting out is charged nothing at all. The same hand now ends 300, 150, 150: the table has the six hundred it started with, and the bank check is balanced before, during and after. 13 new tests, with the already-paid test disabled on purpose to confirm five of them catch the double charge. Walked through the UI in base-pot mode: pot badge $150, wizard pricing three seats at $50 each, and afterwards the felt holding exactly what it held before. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
0ad5592687 | Add the one-stack-rule round to the release | ||
|
|
1c2e716508 |
Ask the same two questions the same way everywhere
The round before this found the bank check and the standings reading a seat's own
stack where the felt reads the table's starting stack — two parts of the tool
disagreeing about how much money was on the table. That was worth a sweep, and
the sweep found more.
The colour-up panel had the same fault. It is the panel that says "count out
2×$100 for this player", and on a table opened the ordinary way — a starting
stack and some names — it showed nothing at all. The one table where every stack
is a round number made of the largest chips there are was the table it had
nothing to say about.
The other question, "is anybody in this chair", existed in four spellings: in the
seat count, in the seat menu twice, in the waiting list, and twice more in the
state module. Three of them looked at name, chips and cash; one also looked at a
hand being dealt. A chair that counts as occupied for one and empty for another
is how somebody's chips get given away — measured, a seat marked in hand with no
name and no stack yet was on offer to the next person in the waiting queue, on
top of whoever was already playing it.
Both questions now have one implementation and one name:
"What is this seat holding?" HoldemState.chipsHeldBy
"Is anybody in this chair?" HoldemState.seatInPlay
10 new tests, including one that pins the bank check, the standings and the
settle-up plan to the same total, and one that pins the seat count and the open
seats to the same set of empty chairs. The occupancy test was cut back to its old
three-part form on purpose to confirm the waiting-list case fails.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
236c52567e | Add the starting-stack accounting round to the release | ||
|
|
1689ecb4f0 |
Count the chips the table is actually playing with
"Starting stack" is how a night opens. The setup panel offers it and says what it means — "seats without their own stack start here" — and everything a dealer looks at reads it: the puck, the action bar, what a player is allowed to bet. The money side didn't. The bank check and the standings read `seat.stack` directly, which is null for exactly those seats. Measured on a three-handed table with a $200 starting stack and nobody's own stack typed in: the felt played for $600, the bank check reported no chips in play and pronounced itself balanced, and the standings — the list a dealer settles the night from — listed nobody at all. Three players, six hundred dollars, and the tool's own accounting said the table was empty. Both now ask the same question the felt asks. The check reports $600 in play against nothing taken, which is the truth: those chips have not been paid for. Empty chairs are excluded, or the starting stack would make every one of them look like somebody holding two hundred dollars — a ten-seat table would have opened with $2,000 of imaginary chips. Being told is only half of it. The verdict names the seats rather than saying "somebody's rebuy probably wasn't recorded", because the dealer's next move is to go to a particular chair and the tool knows which. And the seats holding the table's starting stack — the dealer's own instruction, said once in the setup panel — can be written down in one tap instead of six trips through Add chips. Only those: what somebody's forgotten rebuy was is a question only the dealer can answer, and guessing it would be the tool inventing money, which is the fault this round is about. 14 new tests, with the stack source and the empty-chair test broken on purpose to confirm they fail. Walked through in the browser from a cleared table: set the starting stack, name three seats, and the panel goes from "balanced, no chips" to "$600 more in chips than cash taken. Ann, Ben and Cal are holding chips with no buy-in written down" — then to balanced, with the buy-ins recorded and undoable. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
b8655be258 | Add the waiting-list round to the release | ||
|
|
55eb569d3b |
Keep the list of who's next, instead of leaving it on a napkin
A full home game always has two or three people standing behind the chairs, and the list of who's next lives on a napkin or in the dealer's head. That is where the argument an hour later comes from. The tool had just learned to open a chair — somebody cashes out and the seat is free — and had nothing to say about who should sit in it. The Session panel keeps the list in order of arrival, which is the only ordering anybody has ever agreed to. Whoever is at the front is offered the first open chair by number, and everybody else waits their turn: a list that isn't kept in order is a pile of names, and jumping the queue is the thing a dealer gets shouted at for. With the table full it says so rather than offering a seat that doesn't exist. The same offer appears on the empty chair's own menu — "Seat Priya" — because tapping the chair somebody is about to sit in is what a dealer's hand does anyway. Sitting somebody down puts their name on the chair and nothing else. No stack, no buy-in, not dealt in: they haven't paid yet, and inventing chips for them would put money on the table nobody has handed over, which is the one thing the bank check exists to catch. Add chips is the next thing the dealer does, and it is one tap away in the same menu. Two people called Mike are two people — the list is keyed by id, not by name. Names are trimmed, blanks are refused, and a saved list from a table that never had one comes back empty rather than undefined. 15 new tests, with the occupied-chair guard removed and a stack handed to the new player on purpose to confirm they fail. Walked through the browser: two names added to a full table, a player cashed out to open seat 2, the front of the queue seated there with nothing in front of them, the bank still balanced, and the second name taken off the list. 156 audits with the list populated, including a name long enough to test the row. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
7e420ce747 | Add the misdeal round to the release | ||
|
|
86886ac126 |
Take a hand back, which the rulings sheet has been asking for all along
This tool has told dealers to redeal on a misdeal since the rulings sheet was written — first card to the wrong seat, a seat entitled to a hand dealt out, one of the first two cards exposed — and it had no way to do it. The only control that ended a live hand was Next Hand, which says out loud that it takes the money off the table without paying anybody. That is the right answer for an abandoned pot and the wrong one for a misdeal, where the blinds and the calls belong to the people who put them in. Undo covers the first second of it: "↶ dealing the hand" is one tap. But the action history lives in memory, so a dealer who is three calls in, or who has refreshed the page mid-hand, has nothing — and a refresh mid-hand is exactly the situation this tool has spent rounds making survivable everywhere else. Misdeal appears in the header while a hand is out and goes away when it isn't: before the deal there is nothing to take back, and afterwards it would be offering to unwind a hand that has already been paid. It says what it will do to the money — "$25 goes back to the players who put it in, cards are cleared, and the same button deals again" — and adds a note when two or more players have already acted, because most rooms let a deal stand at that point. It doesn't refuse: which conditions make a misdeal is the dealer's call and the sheet is right there. What comes back is everything, including chips already swept into hand totals on an earlier street — the first version returned only the current street's and would have quietly kept a flop's betting. What stays is the button, the blinds, the straddle and anyone posting in: a misdeal is the same hand dealt again, not the next one, so the hand number doesn't move and nothing is logged. 13 new tests, with the sweep and the straddle broken on purpose to confirm they catch it. Measured through the UI: pot $25 with three players in, take it back, every stack is exactly $200 again, the bank still balances, and dealing again posts the same blinds on the same players from the same button. 156 audits with the button live, including a landscape header that stays one row at 46px. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
c36d7f60bd | Add the cash-out round to the release | ||
|
|
f966d8c340 |
Pay somebody out and keep the chair, the cash and the record straight
The most ordinary thing that happens at a home game after midnight — somebody counts up and goes home — and the tool had nothing for it. Freeing the chair meant wiping the seat by hand, and the bank check then declared their whole stack missing: "someone has paid for chips they haven't been given", for the rest of the night. Measured on a balanced table, cashing a player out with $350 in front of them left the check $350 short. Wiping their buy-in as well silenced it, at the price of losing their entire night from the standings and from the file the dealer hands over at the end. Cashing out is two facts, and both have to survive the empty chair: the box took their cash, and the box handed their chips back. Both are kept now, so the chair opens and the money still adds up. Cash out… is in the seat menu. It says what to count — "$350 — 3×$100, 2×$25" — what they bought in for, and whether they leave up or down, because that is the sentence the dealer says while their hands are moving. The bank check gains a Paid out line and reads "balanced ... once the $350 paid out to players who have left is counted", alongside the discards and the house's cut. The standings keep them with a stripe and the words "· paid out", so nobody counts money out to them twice at 2am, and the export marks them the same way. It refuses to pay anybody with money still in the middle: taking chips out of a pot other people can win, or handing the same chips over twice, are both worse than waiting for the hand to finish. The chair gets a fresh id, because the person who had it has gone and the next player to sit down is not them. Two things this turned up. The first draft worked out what a seat was holding by itself, read `Number(null)` as a perfectly good zero, and offered to pay a player with no stack set nothing at all — it asks Betting now, which owns that rule. The second was mine and the audit caught it: marking the departed row with opacity took the In and Won cells to 3.83:1, under AA, on the very list a dealer settles from. It is a stripe instead. 19 new tests, with the departed buy-in dropped from the check and the pot guard disabled on purpose to confirm they fail. Walked through in the browser: menu, confirm, payout, bank, standings, undo, and the refusal. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
15cec18256 | Add the clock round to the release | ||
|
|
0e2821257f |
Run the clock on a player, instead of counting in your head
The bar has had a stopwatch on the acting seat for a while: how long this player has been thinking, so the dealer knows when it's worth a nudge. What it never had was the thing a dealer actually does about it. Calling the clock is a procedure — a player asks, the dealer grants it, the player gets a set time and then a countdown said out loud, and if they still haven't acted the hand is decided for them. Running that from this tool meant counting in your head while the table argued about whether it had been thirty seconds yet. Thirty and then ten, which is the common form and what the TDA writes down. The countdown is the part the table hears, so it is marked apart rather than being ten more seconds of the same: the readout switches to a bare number and turns red. At zero the bar says the ruling in the dealer's own words and offers it as one button. The ruling depends on what they were facing, and getting that backwards is the part worth being careful about. Facing a bet, the hand is dead. Facing nothing, it is checked — nobody gets folded for being slow when checking was free. Three things about where it sits. The button appears only once somebody has been thinking long enough for the table to start asking, which is the same threshold the stopwatch already marks, so an ordinary hand costs no room at all: measured at 390x844, the bar is 152px with or without it. It is 44px like everything else a thumb has to find — it was drawn at 36 and the audit called it at every touch viewport the moment the check ran, which is the rule doing its job on a control that kills a hand. And once the count is over the Stop button goes, because there is nothing left to stop: at 640x360 its 94px pushed the undo control onto a second line and the taller bar covered the two bottom seats. The ordinary Fold/Check/Raise stay underneath the ruling. A player who speaks up as the count ends is still acting, and whether it was in time is the dealer's call, not the tool's. 11 new tests, and both halves of the ruling were inverted on purpose to check they catch it. Watched end to end in the browser at shortened intervals: the button appears, the count runs down, the ruling lands, and the hand is killed — or checked — with the undo control naming what happened. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
9c8d981e2d | Add the house-take round to the release | ||
|
|
c19575ab10 |
Count the house's cut instead of calling it missing chips
A game that takes a rake has chips leaving the table every hand, and the bank check had no idea. It added up the buy-ins, added up the chips on the felt, and told the dealer somebody had been paid for chips they were never given — a shortfall that grows all night and points at a miscount that never happened. Every home game that takes a drop for the beer fund hits this on hand one. So the tool takes it, rather than watching it go. Under Table Setup: a percentage, an optional cap, and "no flop, no drop" — the rule almost everyone plays, on by default, where a hand that ends before the flop pays nothing. Zero percent is a game that doesn't rake, which is most of them and the default, and nothing about that game changes. Where it shows up is the point. The pot wizard says "House takes $2 of $40 — 5%, capped at $5" above the payouts, before a chip moves, because the summary is the dealer's last chance to disagree with the arithmetic and money taken quietly is the one thing a tool like this must never do. The bank check gets a House take line and says "balanced ... once the $2 in the house's cut is counted". The hand log keeps the pot as the table saw it with the cut recorded beside it, and the export has a column for it, so the night's total is one sum in a spreadsheet. Two decisions worth stating. The rake rounds *down* to a chip the table actually has: a house that can't make 40c takes nothing and the players keep it, which is the only way to be wrong about this that nobody argues with. And on a split pot it comes off the top, then what is left is shared in proportion — with the rounding leftovers going back to the winners rather than to the house, which asked for a number and gets that number, not that number plus dust. Measured end to end through the UI: 5% capped at $5 on a $40 pot pays $38 and banks $2; four stacks plus the cut come to exactly the $800 bought in; the bank check reads balanced. With no-flop-no-drop on, a hand folded round pre-flop pays the full $15 and takes nothing; with it off the same hand pays $14 and takes $1. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
907e0165db | Add the stadium ring round to the release | ||
|
|
8aa90e90a4 |
Give a phone on its side the table shape it needs
The last two rounds left one thing failing and said so: a landscape phone with six or twelve seats stacked two of them 25px apart at each end of the felt, with pucks 64px tall. It has been there since seats were first spaced by distance rather than by angle. Three things were wrong at once, and it needed all three. The rim is the wrong shape when the table is very flat. An ellipse 616px wide and 96px tall has nowhere to put the seats near its ends: the curve doubles back on itself, so two seats a long walk apart come out one above the other. Past two and a half times wider than tall the rim is now a stadium — two straight rails with a half-round cap at each end, which is how a real poker table is built and exactly why it is built that way. A cap is a half-circle of the felt's own height, so an end takes one seat and the rest is rail. Only past that ratio: measured, the cut-off lands between a landscape phone at 4.4 and every other layout the tool has, 1.45 at 1280x900 and 1.0 to 1.4 upright, so no working table changes shape. The ring had less height than the felt did. The seat layer is inset by half a puck to keep the outermost seats on screen, and 26px off the top and bottom of a 96px felt left 44px to work with — less than half the table, which is why no rim shape could have fixed it alone. The inset is a percentage where the felt is short. The landscape branch never had that rule at all, because it is matched on height while the inset lives in a width query, so an 844px-wide phone on its side drew its ring against the felt's whole box; with a flat-topped stadium rail that put a dealer button over Resolve Pot in the header. And the puck was a line too tall. In the tight state the hole cards give up their row: of the four things on a puck, the cards are the one the dealer is holding in their other hand, and the seat editor still shows them. That took the puck from 64px to 55px, which is what finally cleared the ends. Measured across 36 combinations of nine viewports and 2, 6, 9 and 12 seats: the closest two seats at 640x360 go from 25px apart to 55px, and every combination is clean. Then 216 audits across thirteen viewports, three seat counts and both a mouse and a thumb: no failures, no console errors. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
86e1570618 | Add the phone felt round to the release | ||
|
|
dc5db16ffd |
Let the phone table use the width it has
Last round capped the felt so the ring would clear the action bar, and capped the wrong dimension. The wide layout sizes the felt from its width, so capping the width is how you shorten it there — on a phone the felt is already the full width of the column, and capping that threw away 96px of a 360px screen: a 240x240 table inside a 336px space, with nine pucks crowding it. A max-height instead leaves the width alone and lets the aspect ratio give way, which turns a short screen into a wide oval — the shape a poker table is. At 360x640: 336x240 rather than 240x240, same clearance over the bar, and the twelve-seat ring that was crowded at 240 comes out clean, which closes the one combination left failing last round. Where the room isn't the binding constraint, 390x844 and up, nothing changes and the table stays round. The landscape branch now says max-height: none, because it already spends the same measured room on its own height and applying it twice took the felt to the floor value. The measurement that decides whether pucks shrink was also wrong, in a way that only showed up once it was asked a hard question. Rim length per seat measures the walk between two seats, not the gap between them, and on a long oval the rim doubles back at the ends: twelve seats at 640x360 had 193px of rim each and sat 60px apart. It now measures the closest two neighbours directly, off the ring itself — and off the seat layer rather than the felt, because the layer is inset by half a puck on a phone and the felt's box put the answer out by 30px. That makes the rule geometric rather than about screen size, so it is no longer scoped to a phone media query: a wide landscape felt crowds at its ends while its rim claims there is room, and now it is told so. Pucks still touch at nine and twelve seats on a small screen. Tried 46px against 58px: on a roomy phone 46 cleared every overlap where 58 left two, but in a hand with a side pot, where the bar has grown and the felt has shrunk to suit, 46 overlapped four times and 58 twice. That is the felt being small, not the puck being wide, and paying for it with an ellipsis on every long name buys nothing. What has to hold is that each seat keeps its own centre, and that is measured at every size. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6df400322e | Add the audit round to the release | ||
|
|
b9b2237dbe |
Run the two checks the audit had and never used
runAll() did not include the contrast check — it simply wasn't in the list — and
touch targets sat behind an opt-in flag nothing passed. So "the audit is clean"
has meant nine checks rather than eleven for a long run of rounds, and the two
left out are the two whose faults a dealer meets in a dim room with their thumb.
Both were written, tested, and then never called; the contrast checker was built
across two rounds and every sweep since has been silent about colour.
The standalone runner had a second version of the same hole: the 44px rule
stands itself down where `hover: hover` says there's a pointer, and a headless
desktop browser is a pointer, so a single pass had never measured a touch target
at any viewport. It now runs every viewport twice, once with a mouse and once
with a thumb.
A check that decides it doesn't apply now says so, rather than reporting an empty
offenders list — which reads exactly like a pass.
Faded text is measured instead of filed as unchecked. `opacity` on a box fades
the box and its text together onto whatever is behind it, so a dimmed seat is not
grey text on a dark puck: it is a faded puck and faded text both landing on the
page. Both halves are composited that way now. Two exemptions remain and both say
so out loud: a disabled control, which WCAG itself excuses, and a link inside a
sentence, whose size is set by the line it sits in.
What running them found, and this round fixes:
- the chair number on every seat: #8a8a8a at 9px, 3.39:1 once the empty seat's
fade is composited, on the label that answers "which seat is that?";
- Quick Split's amount field at 37px tall and its delete button at 30px across,
on a page whose targets had never been measured;
- and on a 360x640 phone with the bar grown by a side pot, the bottom seat
sitting 38px inside it.
That last one brings back a cap this project measured and rejected once: capping
the felt to the room left fixed the six-seat ring and broke the nine-seat one.
That trade was real then and isn't now — seats are spaced by distance around the
rim rather than by angle, so a shorter felt no longer piles them up. Re-measured
across 30 combinations of five phone sizes and 2 to 12 seats: every one clean
except twelve seats at 360x640, where a 240px felt and a 64px puck cannot both be
true. Where the rim can't give each seat a puck's width, the pucks come down a
size, measured from the rim itself rather than from a media query.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
ca9ac7b6af | Add the straddle round to the release | ||
|
|
fc866597eb |
Deal the straddle, instead of leaving the dealer to fake it
A straddle is a blind raise put out before the cards by the player left of the
big blind, for twice it. Home games run them constantly, and the tool had no
idea they existed — so a dealer's only way through was to deal the hand and have
the straddler "raise" to $4, which gets the money right and the other two rules
wrong.
There are three at once:
- the amount doubles as it goes left, and re-straddles carry on doubling;
- first to act is left of the *straddler*, not left of the big blind, and the
straddler keeps the option when it comes back round even if nobody raised;
- the minimum raise is twice the straddle. Measured off the big blind
underneath it, a $2/$4 game prices the next raise at $6, and every player at
the table will argue — correctly.
Tap Straddle on the deal prompt and the tool posts it, puts the right player on
the clock and prices the raise. The seat wears an STR badge for the hand, read
off what the deal actually posted rather than off the count, so the badge can't
outlive the chips it stands for. It's a one-hand decision, like posting in, and
it clears when the button moves.
The control sits on the deal row rather than a line of its own: measured at
390x844, its own line cost 44px of felt on every deal prompt, straddle or no
straddle, and this bar is docked over the seats. Three buttons across a 360px
phone want to wrap, which made the bar 64px taller and put the bottom seat three
fifths behind it — this layout deliberately doesn't shrink the felt to suit, so
the deal button is given a 7rem floor instead, which keeps the row to one line
and the cost to nothing.
20 new tests, checked by breaking each of the three rules on purpose first: the
wrong first-to-act, the minimum measured off the big blind, and a flat rather
than doubling chain each fail a different one.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
80dae56f0d | Add the landscape round to the release | ||
|
|
b97453fba1 |
Seat the table properly on a phone held sideways
Landscape is the one shape this layout has never really fitted, and the audit was calling two separate faults there. The felt worked its own height out as `100vh - bar - 7rem`, guessing that everything above it came to 7rem. That is 108px at 640x360, which the guess covers, and 119px at 844x390, which it doesn't — so on an iPhone on its side the bottom row of seats sat up to 26px inside the action bar. The measurement it needed was already being published for the wide layout: the bar's real height, the wrap's real offset and the bottom seat's overhang, all taken off the window. It uses that now. The floor under it drops from 220px to 96px, because 220 was not a size a 390px-tall window has to give — holding out for it was itself how seats ended up behind the Fold button, which is the one thing the floor exists to prevent. That still left the header taking two rows, and the second row is 44px of the 390. The title block and the five buttons came to 829px inside a 765px header, missing one line by 64px. The page title goes — the one thing on this screen a dealer already knows — hidden from the eye rather than removed, so the heading is still there for a screen reader. The buttons tighten across but never down: they stay a 44px thumb target. The seats themselves were spaced by angle, which is only even spacing on a circle. On a 601x126 felt, ten seats at 36° apart put four of them down each steep end, 20px apart, with pucks 72px tall — a seat's centre belonged to its neighbour, so tapping a player got the wrong one. They now walk the rim by distance instead, which puts them along the two long rails the way a real table seats people. Closest neighbours go from 33px apart to 59px. Measured at 640x360, 667x375 and 844x390 with 9-, 10- and 12-seat tables: every combination the audit used to fail is clean, bar a 10-seat table at 640x360, where two seats at the end of a rim that flat still overlap. Portrait, tablet and desktop are unchanged — the floor never bound there, and on a rounder felt walking the rim gives back the same angles it always did. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6ca99fde6b |
Bring master's already-merged rounds into the release branch
Master merged #58-#61 (manual deal, less guessing, seat editor, landscape table) while the stack kept going. Those commits are already in this branch's history; this merge just reconciles the two shapes so the release can go in as one. |
||
|
|
419852a1f4 |
Let a dealer take the table off the device it's stuck on
The night lives in one browser's localStorage and nowhere else. A dealer whose phone dies, or who wants to carry on from the tablet after the break, loses the stacks, the buy-ins, where the button is and the whole hand log. The CSV export can't bring any of it back: it records what happened, not where the table stands. The table can now be written to a file and read back. Round-tripped through the real UI, a hand in progress comes back mid-hand — blinds posted, the same seat on the clock, the button where it was. Loading replaces everything, so it asks first and names both sides rather than saying "are you sure": how many players are in the file and when it was saved, against how many players and how many logged hands are about to go. It goes through the same undoable path as the header buttons, so the toast offers the way back, and the confirm says so instead of overstating it. Reading a file goes through exactly the same migration a save from storage does — load() had that logic inline and it is now a function both call. A second, looser path for imports is how a format grows two meanings of the same field. What it refuses, each with something a dealer can act on and never a half-loaded table: anything that isn't JSON, JSON with no seats in it, a save from some other tool that happens to carry a `kind`, and a file from a newer version — that last one because loading it risks reading a field whose meaning has changed, and the dealer would find out from the money. What it accepts anyway: a bare table with no envelope, for somebody who copied the storage value out by hand. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
8a2fc3e540 |
Give undo and the card picker's buttons a fingertip to land on
The first thing the repaired audit found, run on a real touch context at 44px: three
controls a dealer taps on a phone that are too small to tap.
.actionUndo 145x30 the "I recorded that wrong" pill
#cardPickerClearBtn 50x40 wipes a player's cards
#cardPickerCloseBtn 50x40 closes the picker
Undo is the worst of them. It is the control a dealer reaches for having just entered
the wrong action with five people waiting, and it was fourteen pixels short of a
fingertip. The picker's two are short because the touch rule that fixes every other
small button is scoped to .panelBody, and the picker is a sheet — it never reached
them. One of them is destructive.
All three now sit at 44px on touch. Nothing changes on a pointer device.
I tried an invisible hit-area pad on undo first, to avoid spending bar height, and the
measurement said it did nothing: the pill clips its overflow to keep a long label on
one line, and giving it a positioning context made it clip the pad too. Growing the
box is the honest fix, and it turned out to cost nothing anyway — the bar goes from
138px to 152px and the felt stays at 366px, because the felt had slack against its
220px floor rather than sitting on it as I had assumed.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
b64da7a787 |
Make the layout audit usable inside modals, and stop it passing what it can't measure
Last round turned up that Quick Split had never been audited. Chasing that, the reason
the dealer table's modals had never been audited either turns out to be the harness,
not neglect: it reported the pot wizard covering the seats as ten faults and the
rulings sheet as seven. A modal covering the page is what a modal is for, and an
instrument that calls that a fault is one nobody can use in those states.
It now finds whatever is open on top and scopes the reachability checks to it — the
overlays, plus the phone seat sheet, which is a body class rather than an element. The
seat-ownership check is skipped outright while a sheet is open: seats behind it aren't
fighting each other for taps, nothing is meant to reach them at all.
Two worse faults came out of proving the fix rather than assuming it. Injecting a 20x14
button into the open wizard, the contrast check caught it and the touch-target check
did not:
* effectiveTarget returns null when elementFromPoint can't find the control, and the
check treated that as a pass. Silent skips are how faults hide — the same lesson as
the aria-hidden and faded-text exemptions before this. Unmeasurable controls are
now reported as warnings.
* The probe failed on anything scrolled out of its container. That was hiding a real
control: "Confirm Winner(s)", the button that pays the pot out, had gone unmeasured
for thirty-six rounds. It is scrolled into view before probing, and passes.
A checkbox inside a label is now measured by its label, since tapping the label is what
activates it. Without that, the dedicated-dealer toggle read as an unprobeable 24px
target when it has a 335x44 label around it.
The tool itself came out clean: no failures and no warnings across the table, the pot
wizard, the rulings sheet and the seat editor, on a real touch context at 44px. The
value here is that those states can be checked at all from now on — verified by
injection, which the fixed check catches and the old one did not.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
3be93cdadf |
Chop a side pot on the screen that worked it out, and fix Quick Split's contrast
Quick Split lists the pots when somebody is all-in — main, sides, uncalled — and then
leaves the dealer there. Two players tying for a side pot is ordinary, and working out
each one's share meant switching to the other mode and retyping an amount the screen
had just calculated. Retyping a number with money on it is a transcription error
waiting to happen, and the number was already on screen.
Each pot with two or more people who can win it now carries a Chop, opening underneath
its own row: as many ways as there are contenders by default, a stepper for a chop
between fewer of them, and the same odd-chip and dust wording the main split uses,
because it is the same rule and the dealer shouldn't have to notice they are reading a
different screen. Per-row state, so opening the main pot's chop doesn't disturb what
the side pot below it says. Not offered on the uncalled row, which goes back to one
player, nor on the total.
Separately, this page had never been through the layout audit the dealer table gets
every round, and it was carrying nine contrast failures:
.unevenCurrency 4.11:1 the $ beside every amount typed
.unevenRemoveBtn 3.66:1 the ✕ that deletes a player's entry
.splitFootnote 3.94:1 the instruction for what to do with the pot list
All under the 4.5:1 small-text minimum, the worst of them on a destructive control.
Lifted to pass. The large $ on the even-mode pot input is left alone: at 1.6rem it is
large text, where 3:1 is the bar and 4.11:1 clears it.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|