f841ccf42526b51e33b563f6bcf112497e81bd36
32
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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 |
||
|
|
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. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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
|
||
|
|
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 |
||
|
|
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
|
||
|
|
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
|
||
|
|
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
|
||
|
|
d297c1a2c9 |
Close two blind spots in the contrast check
Last change found a folded player's stack sitting at 2.22:1, hidden for twenty rounds
behind an exemption. So I went looking for the other exemptions rather than waiting for
the next one to surface. There were two, and neither is hiding a live fault today --
which is worth saying plainly, because it means this is a fix to the instrument and not
to the tool.
Leaf elements only
if (el.children.length > 0) return; // leaf text only
An element's own text stopped being measured the moment a child was added to it. The
undo control went that way two changes ago, the day a <kbd> was appended to it: its
label became invisible to this check with no sign that anything had changed. Now it
measures an element's own text nodes and leaves the descendants to their own turn,
against their own colours.
aria-hidden as a contrast exemption
The bets layer is aria-hidden because the amounts on the chips are already announced by
the seat and the action bar, and repeating them would be noise. That is a good reason
to hide them from a screen reader and no reason at all to stop checking whether a
sighted dealer can read them -- and the chips are exactly what the felt is read for.
Two different questions were being answered by one attribute.
Nothing is exempt now. If something genuinely decorative turns up, it should say so in
its own words rather than borrowing an attribute that means something else. The
watermark that the exemption was written for is gone as of the change before last.
What the widened check finds
Nothing. Zero offenders at six viewports across touch and pointer contexts, and with
the sidebar panels and the rulings sheet open. Measured from rendered pixels, the two
things that had been invisible are comfortable: the bet chip amounts read 12.65 to
13.74:1 and the undo label 8.06:1.
So the fix is proved by injection instead. Colouring the bet chip amounts #3a3a3a now
reports three offenders at 1.80:1, and colouring the undo control #2f2f2f reports it at
1.13:1. Neither was reported before.
269 tests still passing, no console errors.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
f0cfd24647 |
Make the seats that are out of the hand readable again
A folded player's stack measured 2.22:1.
The contrast audit has been reporting clean for twenty rounds, and it never looked at
these seats: the check skipped anything with opacity below 0.99 as "faded on purpose",
and folded and sat-out seats faded their whole box -- 0.5 and 0.58. Opacity on a box
fades the text you still have to read along with the box you wanted to push back.
Measured off rendered pixels, 11.5px text, where AA asks 4.5:1:
name stack
live 13.28 4.75
folded 4.75 2.22
sitting out 7.00 3.22
A folded player's stack is not decoration. It is what they will have next hand and the
answer to "how much has Ben got?", and at 2.22 it was barely there.
So the box recedes and the text does not: a flatter, darker background and a muted
border, with name and stack colours that measure.
name stack
live 13.28 4.75 (unchanged)
folded 7.60 4.55
sitting out 7.60 4.71
Taking the fade away took the signal with it, which the screenshot showed and the
numbers did not: a sat-out seat became almost indistinguishable from a live one. A
folded seat still says so twice, with a red border and a line through the name, but
sitting out had only faintness. It wears a dashed edge now -- the same thing an empty
chair wears, which is right, because neither is in this hand, and the name and stack
tell the two apart.
The same fault, found once the audit stopped hiding it
The key hints on the action bar are 9.6px and were measuring 2.97 to 3.33 against
their buttons -- a fade chosen for looks pushing text a dealer is meant to read under
the threshold, exactly like the seats. They are 4.93 to 7.63 now, still quieter than
the labels they sit beside.
And the audit no longer passes over faded text in silence. Something invisible is still
exempt, since there is nothing to read at zero opacity, but anything in between is
reported as unmeasured -- a number nobody checked is not the same as a number that is
fine. It now lists five: the four key caps and the disabled Export CSV button, which is
disabled and therefore genuinely exempt.
269 tests still passing. Contrast offenders zero, layout audit clean, no console
errors; the folded seat's red line-through and the sat-out seat's dashed edge both
confirmed in cropped screenshots rather than assumed.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
cfb83a2ba6 |
Don't quietly write over a save that couldn't be read
Last change made a failing save say so. This is the other direction, and it turned out to be worse. load() is well defended -- try/catch, a shape check, sensible fallbacks -- and every one of those paths ends in `return this.createDefault()`. So an unreadable save becomes an empty table. Driven with a save truncated to 70% of its length, the way an interrupted write leaves one: 1762 bytes stored, 1233 after truncating loaded: 0 named seats, hand #1 instead of #34 toast: none. warning: none. anything on the page: no. A dealer mid-session sees an empty felt and cannot tell whether they mis-tapped Reset, whether the browser lost the night, or what. And then the first edit destroys the evidence. I typed a name into a seat and storage went from the 1233 unreadable bytes to 2549 bytes of fresh default state. Whatever a person could have picked out of that JSON -- the names, the buy-ins, the standings -- was gone one tap after the tool decided it could not read it. Now the unreadable value is moved aside to its own key before the fallback, and the table says what happened: toast: "The saved table couldn't be read, so this one started empty." panel: "The saved table couldn't be read, so this one started empty. The unreadable copy has been kept rather than written over." It is deliberately not offered as a restore. Half a JSON document is not a table, and pretending otherwise would be worse than an empty one. It just stops being destroyed. The two warnings share the Session panel line and both can show at once -- a browser having trouble with storage tends to have it in both directions -- with the load message first, because "why is my table empty" is the question the dealer is standing there asking. 8 tests (263 total): nothing stored is a new table rather than a failure, a good save loads clean, a truncated one is reported, valid JSON of the wrong shape counts too, the copy is kept, a later save does not touch it, the listener fires once across three reads, and storage with no room to keep the copy is not a crash -- which is exactly the storage most likely to hand back half a value in the first place. Driven through the UI: warning and toast on a corrupted save, the set-aside copy still 1233 bytes intact after editing a seat, and nothing shown at all on a healthy table. One audit correction rides along. The collapsed-column check measured inside hidden elements, so the standings header -- hidden with its table until somebody buys in -- was reported as three collapsed columns on every empty table. It skips hidden subtrees now, and still catches a real one: squeezing a visible column reports the header and all three rows. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
3fe16fa7f2 |
Let a phone move a player to another seat
The hint under the table has said this on every screen since the gestures went in:
Tap a seat to edit - double-tap to fold - press and hold for quick actions -
drag to swap seats.
Three of those work on a phone. The fourth never has. Swapping seats is HTML5
drag-and-drop -- draggable="true", dragstart, drop -- and a touch screen does not
generate drag events at all. So the one clause written in the same breath as "tap"
and "press and hold" was the one thing a thumb could never do.
I found it by dragging seat 1 onto seat 4 with real touch pointer events and watching
nothing happen: no swap, no menu, no error.
Moving a seat is in the long-press menu now, which is the route a thumb already takes
for everything else. "Move to..." replaces the action list with the other chairs, by
number and name, and picking one makes the move:
Move Ben to...
Their chips, cards and bet go with them. The button and the blinds stay on the chair.
Seat 1 - Ann Seat 3 - Cal Seat 4 - Dee Seat 5 - empty ...
That subtitle is the rule, and it is the one a card room uses: the whole seat travels,
while everything naming a position stays put. Somebody who sits down in the big
blind's chair is the big blind.
Driven through on a phone: Ben, small blind, $109 behind with $1 posted, moves to
seat 5 with both numbers; seat 2 goes empty; the button stays on seat 1; the toast
reads "Ben moved to seat 5" and undo puts it all back. Reopening the menu on another
seat shows actions again rather than the chair list it closed on.
The hint tells the truth per device now -- dragging is only mentioned where a mouse
exists. Dragging still works there, checked.
8 tests (232 total): the whole seat moves, the bet moves with it, chips are conserved,
the button and the blinds stay on their chairs, swapping twice is a no-op, and a chair
that is not there is refused rather than swallowed.
One audit fix rides along. The "one dealer button" check counted the empty slot that a
hovered seat deliberately fades in, so it reported two buttons whenever a mouse
happened to rest on the table -- which is most of the time a mouse exists. It ignores
the hovered seat now, and still catches the real fault: with the pre-#62 behaviour put
back it reports six, or five while hovering.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
784315f2ea |
Put one dealer button on the table
A poker table has one dealer button. This one drew nine.
Every seat carried a puck: the real one in gold, and eight ghosts at 0.32 opacity
that existed to be pressed. Measured on a nine-seat table -- eight pale discs, all
eight showing the letter D, one of them the button. Finding it meant comparing nine
identical shapes for the one that's gold, when it should be the only thing of its
kind on the felt.
The ghosts also sat on their neighbours. Every remaining "something important is
covered" offender on a crowded table was a live seat covered by the seat-next-door's
ghost puck: 844x390, 915x412, 360x640 and 640x360 each reported one, and they were
all the same thing.
And a screen reader heard nine buttons named "D".
So a seat without the button doesn't draw one. Hovering a seat fades its slot in --
a preview of where the button would go, one click as before -- and on touch, where
there is no hover, "Give the button" has been in the seat's long-press menu all
along. Keyboard reaches it by focus, where the slot appears too.
pointer-events matters as much as the opacity: an invisible puck that still took
taps would be worse than a visible one.
visible pucks 9 -> 1 at every size
layout offenders 915x412 1->0, 360x640 1->0, 844x390 3->2, 640x360 3->2
clean viewports 4 of 9 -> 6 of 9
labels nine "D" -> "Ann has the dealer button" and
"Give the dealer button to Ben"
The audit now checks it, because this is a rule about the table rather than about
CSS: one puck drawn, and no two announcing themselves the same way. Verified against
the old behaviour put back -- it reports 9 and 9.
Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
|
||
|
|
e01d8e7c49 |
Tapping a seat gives you that seat
Went to audit the seat editor and couldn't open it: Playwright refused to click the middle of seat 1 because seat 9's dealer chip was sitting there. On a nine-seat table, tapping the centre of one player handed the button to another. I shipped that. The enlarged chip hit area grew outward from a chip already hung over its seat's corner at -10px/-10px -- and on an oval, a seat's corner is the seat next to it. Measured on the nine-seat table: seat 1 10 of 49 sampled points taken by seat 9's chip, including its centre seat 4 9 of 49 taken by seat 5's chip seat 5 3 of 49 taken by seat 6's chip I claimed at the time that no seat tap was stolen, and I had checked -- with an audit that exempted anything inside a `.seat` as harmless crowding. The thief was inside a seat. The check walked straight past it. Two fixes, and the second matters more The chip sits inside its seat now rather than on the rim, and its hit area grows inward over its own seat, which is a big target that can spare a corner. Padding inward alone wasn't enough -- I tried it and the numbers didn't move, because the chip's own box was the problem, not just the pad. Isolating that took disabling the pad and then the overhang separately: pad off gave 2 seats affected, overhang off gave zero. And the audit now asks whether a seat's centre belongs to that seat, as its own check. The occlusion check couldn't answer it: it was built for overlays on top of the table, and this was two pieces of the table fighting over a tap. It exempts a neighbour's body -- seats do overlap by design -- but not a neighbour's controls, which was the exact hole. Seven of eight sizes now have no seat whose centre goes elsewhere, and all four seats I tap open their own editor. What's left, stated rather than buried At 360x640 and 375x667 one seat still loses a single sample point of its rim to a neighbour's chip. Centres are correct; this is the rim of a crowded oval. At 844x390 two seats have centres belonging to the seat next door -- and those are seat bodies, not chips. Nine seats on a 163px-tall landscape table overlap almost completely. That's the landscape felt sizing, it predates all of this, and it wants fixing on its own terms rather than being folded in here. Both are now reported by the audit instead of hidden by it, which is the part I actually trust. 152 tests still passing; contrast clean at every size. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
d66abda05a |
Make the dealer puck hittable without making it bigger
Ran the touch-target check that had been sitting in the audit unused. The dealer puck is 22x22 — half what a fingertip needs — in ten places on the table, and tapping it hands the button to that seat and re-posts the blinds. Not a cosmetic control. It stays 22x22. A puck should look like a puck sitting on the rim of its seat, and growing it would take area from the seat itself, which a dealer taps far more often. Instead an invisible pad around it catches the tap: 43x44 hittable on every phone size, close to four times the area, with the pad pushed down and left away from the seat the chip juts towards. Verified that no seat tap is stolen — the occlusion audit is clean at every size. Three things the check itself got wrong, all found before acting on them It read element boxes, and boxes lie in both directions. A control padded out with a transparent ::after responds well past its painted size, and a checkbox wrapped in a label is hit by tapping anywhere on the label — so it reported the dedicated dealer toggle as a 24px fault when it sits inside a 320x44 label that already forwards the tap. It probes outward with elementFromPoint now, which is what a finger actually experiences. The probe walked a fixed 40px each way, so everything wider than 80px reported exactly 80. Every header button came back a suspiciously round "80x42". And it enforced 44px on desktop, where there is no fingertip. Those same header buttons are 64px tall on a phone and 42px with a mouse, which is not a defect. Keyed off hover:none now, the same signal used for the tablet field sizing. Also removed a ::after pad on the dedicated-dealer checkbox that was both inert -- pseudo-elements don't render on replaced elements -- and unnecessary, since the label already does the job. The puck is the one documented exemption: it is deliberately small and "Give the button" is in the seat menu at full size, so it's a shortcut rather than the only way through. The reason sits in the code next to the exemption. Phone sizes: dealer chip 22x22 drawn / 43x44 hittable, zero undersized controls even with the exemption and the pointer rule forced off. Layout and contrast audits clean at all seven sizes. 137 tests passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
6fa4532781 |
Fix the contrast the audit could finally measure
Contrast is the one check I've got wrong twice, both times by being clever about backgrounds. The first version read only background-color, so text on any of the gradient-painted sheets was measured against transparency and the dealer puck's dark "D" was reported at 1.2:1 against nothing. The second over-corrected and treated low-alpha gradient stops as opaque, scoring white-on-dark at 1.0:1. So this one is built to be incapable of false positives rather than exact. Backgrounds composite through alpha; where a gradient is involved every colour stop is tried and the most flattering one used. A finding means the text fails even under the reading most generous to it. It's checked against known answers before being believed: black on white at 21:1, white on white at 1:1, #767676 on white at the 4.54:1 AA boundary, white over a dark gradient, dark text on that same gradient, white over a 6% white wash on a near-black page. Nine cases, all correct — and building them found a real bug in the check, which walked past an opaque gradient and offered the page background as a candidate. That made it too lenient, which is as bad as the old false positives. What it found: Both blind badges failed against their own gradients, in 9px bold text a dealer reads at a glance across a table. At the worst stop: small blind 3.52:1, big blind 2.15:1. The small blind keeps its blue and darkens to 4.77:1. The big blind flips to dark text on brighter gold, 5.84:1 — how a gold chip is marked anyway, and it makes the two badges far easier to tell apart at speed. Quick Split's footnote names a control the dealer is told to reach for, and that name inherited the muted grey of the surrounding aside: 3.39:1. Emphasised text carrying an instruction gets its own colour now. The felt watermark came back at 1.19:1 and is meant to. It's decoration; holding it to a reading threshold would mean making it not a watermark. It's aria-hidden now, which says the same thing where it also does some good, and the audit skips aria-hidden subtrees. 32 contrast passes over the table, all five overlays and Quick Split at five viewports: zero findings, zero unmeasured. Layout audit still clean. 137 tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |
||
|
|
2f6d63400a |
Stop re-typing the layout audit into a console every round
Last round I found a regression I'd shipped two rounds earlier: the standings table's player-name column was zero pixels wide, at every viewport, and nobody noticed because the audit that would have caught it only ever existed in a previous session's scrollback. These checks have been re-written by hand at least six times, and each time they covered only whatever had just changed. So they're a file now. uiAudit.js runs in the page — no build step, no package.json, no dependency — and checks touch-action and tap highlights, that fields stay selectable and are big enough not to trigger iOS zoom, that nothing important is covered or stranded off-screen, that no grid column has collapsed to nothing, and that no text is painted outside its box. runUiAudit.js drives it over the table, all five overlays and Quick Split at thirteen viewports. Deliberately outside CI: the unit suite is dependency-free and runs in a second, this one needs a browser and a running site. Three real faults it found on its first run Fields were 13.3px on every iPad. The 16px minimum that stops iOS zooming the page on focus went in inside a max-width:700px block, and every iPad is wider than that — so a dealer on a tablet, which is the device this is most likely to be propped up on, got the table zoomed every time they touched the blind amount. The reason for that rule was never "narrow screen", it was "iOS". Font size applies at all widths now, and the 44px touch targets key off hover:none, which is what actually distinguishes a finger from a mouse. The seat menu couldn't be scrolled. It had no height limit and no overflow, so on a phone held sideways its first two actions — Fold and All-in — sat below the bottom of the screen with no way to reach them. A dealer in landscape could long-press a seat and then not fold it. The pot wizard sat under the action bar. Every other overlay is z-index 90-98; the wizard alone was at 50, below both the action bar at 55 and the toast at 80. In landscape one of its inputs was completely covered by the Fold button, and a toast could sit over "Confirm Winner(s)". It was the odd one out rather than a decision. Three faults in the audit itself, found by checking before acting Worth recording, because each would have had me "fix" something that wasn't broken: - an ellipsised name was reported as painting outside its seat. A Range measures laid-out text, which runs past the element while overflow:hidden quietly clips it. Only overflow that is actually visible can paint over a neighbour. - every link below the fold was reported as off-screen. Something under the fold on a page that scrolls is reachable; the check now asks whether scrolling can bring it into view, which is the thing that actually matters. - a button in the newly-scrollable seat menu came back "covered by null". elementFromPoint returns null for a point outside a scrollable ancestor's clip — scrolled out of view, not covered by anything. A toast partly over a control is a warning rather than a failure now: it dismisses itself and carries a close button, and on a desktop it necessarily lands over a table that fills the content column. Fully covered by a toast is still a failure, because that was a real bug once — a seat sat exactly under the Undo button. Final sweep: 13 viewports, 65 overlay checks, 13 full-page audits, 4 Quick Split audits, zero failures, one warning. 137 unit tests still passing. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb |