Hold'em dealer: making the table and the bar fit the screen they are on #119
@@ -293,6 +293,13 @@ button,
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:root {
|
||||
/* The sidebar's width, named because three rules need to agree about it: the grid
|
||||
that draws the column, the action bar that has to stay out of it, and the seat
|
||||
sheet's right edge. */
|
||||
--sidebarW: 320px;
|
||||
}
|
||||
|
||||
#holdemApp {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
@@ -468,7 +475,7 @@ button,
|
||||
|
||||
#holdemLayout {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 320px;
|
||||
grid-template-columns: 1fr var(--sidebarW);
|
||||
gap: var(--sp-5);
|
||||
align-items: start;
|
||||
}
|
||||
@@ -962,8 +969,15 @@ button,
|
||||
overflow: hidden;
|
||||
/* A single word longer than the puck has nothing to break on. `anywhere` was the
|
||||
first try and it gave "Christo / pher", which reads as a rendering fault; with
|
||||
`break-word` the browser only breaks a word that cannot fit on a line of its
|
||||
own, and `hyphens` puts a hyphen in when it does. */
|
||||
`break-word` the browser only breaks a word that cannot fit on a line of its own.
|
||||
|
||||
`hyphens` is here in hope rather than in trust. It is meant to put a hyphen in at
|
||||
that break, and where a browser has a hyphenation dictionary it does — but that is
|
||||
not a thing to build a layout on. Measured directly: a 56px box with `hyphens:
|
||||
auto` and nothing else does not hyphenate "Christopher", it overflows, which means
|
||||
the dictionary is simply absent and the property is inert. So nothing above this
|
||||
line depends on it; where it works it is a free improvement to a break that is
|
||||
already legible without it. */
|
||||
overflow-wrap: break-word;
|
||||
hyphens: auto;
|
||||
line-height: 1.2;
|
||||
@@ -975,14 +989,66 @@ button,
|
||||
margin-top: var(--sp-2);
|
||||
}
|
||||
|
||||
/* The button's chip sits in this puck's top-right corner, on a layer above the text.
|
||||
Reserving its width means a long name ends in an ellipsis before it reaches the
|
||||
chip, rather than disappearing underneath one — on the one seat at the table a
|
||||
dealer looks at most. */
|
||||
/* The button's chip sits in this puck's top-right corner, on a layer above the text, so
|
||||
the name has to keep out from under it.
|
||||
|
||||
It used to do that with `padding-right: 22px`, which 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 wide, "Christopher" is 66px and fits, and on the seat holding the button it
|
||||
was cut to 56px and broke as "Christop / her". Mid-word, and no hyphen (see below).
|
||||
The button moves every hand, so that is a player whose name reads properly for four
|
||||
hands and comes apart on the fifth. On a Pixel 7 it went to three lines and the clamp
|
||||
ate the end of it.
|
||||
|
||||
A float is what indents only the lines it sits beside, which is the whole of the
|
||||
problem. 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. A short name still sits up beside the chip exactly as it did.
|
||||
|
||||
The padding stays for the crowded ring, and it has to. A tight puck gives the name one
|
||||
line on purpose — see `.seatsTight .seatPuckName` — and one line has nowhere to move
|
||||
to, so the only way to keep it off the chip there is to make the line shorter. Trying
|
||||
the float on both cost a whole sweep to find out: with two lines granted, the name
|
||||
dropped below the float 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 that has room for two lines, and the padding is for the
|
||||
one that doesn't. Two rules, matching the two regimes the puck already had. */
|
||||
.seat.seatHasButton .seatPuckName {
|
||||
padding-right: 22px;
|
||||
}
|
||||
|
||||
/* Two things have to go with the float. It does nothing inside `display: -webkit-box`,
|
||||
so the clamp is replaced by the same two lines held as a max-height. And
|
||||
`overflow-wrap: break-word` has to stand down, because it is what stops the float
|
||||
working: it breaks a word to fit the short line rather than letting it move past the
|
||||
float to the long one, which is the whole manoeuvre.
|
||||
|
||||
Measured, at the name-box widths a roomy button seat gets:
|
||||
|
||||
78px (desktop, iPad) "Christopher" was 2 lines of 54+12 now 1 line of 66
|
||||
78px (desktop, iPad) "Katarzyna" was 2 lines of 49+6 now 1 line of 56
|
||||
|
||||
The float is one line tall, which is the height it has to be: tall enough to cover the
|
||||
chip's reach into the name — the chip's bottom edge is 26px down and the name's first
|
||||
line ends at 27.4 — and no taller, because a float that spills into the second line
|
||||
indents that one too and puts the whole problem back. */
|
||||
#seatsLayer:not(.seatsTight) .seat.seatHasButton .seatPuckName {
|
||||
padding-right: 0;
|
||||
display: block;
|
||||
max-height: 2.4em;
|
||||
overflow: hidden;
|
||||
overflow-wrap: normal;
|
||||
}
|
||||
|
||||
#seatsLayer:not(.seatsTight) .seat.seatHasButton .seatPuckName::before {
|
||||
content: "";
|
||||
float: right;
|
||||
/* The chip, plus the 3px it is inset by. */
|
||||
width: 25px;
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
.seatPuckCards {
|
||||
font-size: var(--fs-xs);
|
||||
font-weight: 700;
|
||||
@@ -2611,6 +2677,38 @@ body.hasActionBar #holdemSidebar {
|
||||
padding-bottom: 9rem;
|
||||
}
|
||||
|
||||
/* ---------- The bar lines up with the table ----------
|
||||
It 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 were 172px apart, at 960px wide and at 1600px wide and at every width 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.
|
||||
|
||||
Centred between the page's left padding and the table column's right edge instead.
|
||||
Worth doing in offsets rather than a width: the auto margins still centre the bar, so
|
||||
it stays 720px at most and centred inside whatever room this leaves it.
|
||||
|
||||
The arithmetic comes out exact at every width, above and below the 1400px cap, which
|
||||
is worth showing because it looks like it shouldn't:
|
||||
|
||||
bar centre = (--sp-6 + 100vw - (--sidebarW + --sp-5 + --sp-6)) / 2
|
||||
= (100vw - 344) / 2
|
||||
table centre = appLeft + --sp-6 + (contentWidth - (--sidebarW + --sp-5)) / 2
|
||||
|
||||
and under the cap appLeft is 0 with content = 100vw - 64, while over it appLeft is
|
||||
(100vw - 1400)/2 with content fixed at 1336. Both reduce to (100vw - 344) / 2.
|
||||
|
||||
No 100vw anywhere, which is the point — on a page with a scrollbar 100vw is wider
|
||||
than the viewport, and that is exactly the trap the bar's own centring comment
|
||||
warns about. */
|
||||
@media (min-width: 901px) {
|
||||
#actionBar {
|
||||
left: var(--sp-6);
|
||||
right: calc(var(--sidebarW) + var(--sp-5) + var(--sp-6));
|
||||
}
|
||||
}
|
||||
|
||||
.actionStatus {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -62,6 +62,23 @@ at that height, and reporting it that way is a diagnosis of something that isn't
|
||||
happening: the pot wizard's third winner, below the fold in a list, came back for four
|
||||
viewports running as "fully covered by button Confirm Winner(s)".
|
||||
|
||||
Text is measured where it was painted, not where its box is. The two are different
|
||||
things and the difference is not academic: a name is centred, so its box is wider than
|
||||
its ink, and a name kept clear of the dealer's chip by a float is out of the way on the
|
||||
line beside the chip and full width on the line below. The check used to model that as
|
||||
"box minus padding", which is one way of keeping text out of somewhere and not the only
|
||||
one — it 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 rather than 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, and the difference decides whether it is worth fixing. Probing
|
||||
the whole line and counting turned the residual this sweep has carried for three rounds
|
||||
into "1/7 under SB" — one probe of seven, on one configuration, in a thumb context
|
||||
only.
|
||||
|
||||
More than one table shape is also newer than it sounds. The sweep ran on one — five
|
||||
players at a nine-handed table — for its whole life, and how much room the felt has
|
||||
depends on how many people are sitting down far more than on anything a viewport can
|
||||
|
||||
@@ -69,6 +69,104 @@ describe("which shape the bar is in", () => {
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("the bar lines up with the table", () => {
|
||||
// On a desktop the bar was centred on the window and the table on its own column,
|
||||
// and the sidebar takes 320px out of the right of that — so the two things a dealer
|
||||
// is looking at were 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 rule that fixes it is four numbers that have to keep agreeing with four other
|
||||
// numbers, none of them anywhere near each other in the file. That is what this
|
||||
// block is for: the arithmetic is checked on the running page by measurement (bar
|
||||
// centre against table-column centre, which came out 0px off at every width), and
|
||||
// the numbers it depends on are checked here so they cannot drift apart afterwards.
|
||||
const rule = CSS.match(/@media \(min-width: 901px\) \{\s*#actionBar \{([^}]*)\}/);
|
||||
|
||||
test("there is a rule, and it is scoped above the one-column breakpoint", () => {
|
||||
// Below 901px the sidebar is not a column at all — it is stacked under the table
|
||||
// — so there is nothing to lean away from and the bar is centred as it was.
|
||||
assert.ok(rule, "no min-width: 901px rule for #actionBar");
|
||||
assert.match(CSS, /@media \(max-width: 900px\)[\s\S]{0,200}#holdemLayout \{\s*grid-template-columns: 1fr;/,
|
||||
"the one-column layout should start exactly where this rule stops");
|
||||
});
|
||||
|
||||
test("it leans away from the sidebar by the sidebar's own width", () => {
|
||||
assert.match(rule[1], /left:\s*var\(--sp-6\)/);
|
||||
assert.match(rule[1], /right:\s*calc\(var\(--sidebarW\)\s*\+\s*var\(--sp-5\)\s*\+\s*var\(--sp-6\)\)/);
|
||||
});
|
||||
|
||||
test("and the sidebar's width is one number, not two", () => {
|
||||
// The grid draws the column and the bar leans away from it. Written out twice,
|
||||
// the day somebody widens the sidebar is the day the bar goes crooked and no
|
||||
// test says so.
|
||||
assert.match(CSS, /--sidebarW:\s*\d+px/, "the token should be declared");
|
||||
assert.match(CSS, /grid-template-columns:\s*1fr var\(--sidebarW\)/,
|
||||
"the grid should draw the column from the token");
|
||||
});
|
||||
|
||||
test("no 100vw anywhere in it", () => {
|
||||
// The bar's own centring comment warns about this: on a page with a scrollbar
|
||||
// 100vw is wider than the viewport, and the overflow drags the table sideways.
|
||||
// The offsets are exact without it — see the derivation in the stylesheet.
|
||||
assert.equal(/100vw/.test(rule[1]), false, rule[1]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("the button seat's name", () => {
|
||||
// The dealer button's chip sits in the puck's top-right corner. Keeping the name out
|
||||
// from under it with `padding-right` 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". The button moves every hand, so that is a player
|
||||
// whose name reads properly for four hands and comes apart on the fifth.
|
||||
const roomy = CSS.match(/#seatsLayer:not\(\.seatsTight\) \.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
|
||||
const float = CSS.match(/#seatsLayer:not\(\.seatsTight\) \.seat\.seatHasButton \.seatPuckName::before \{([^}]*)\}/);
|
||||
const base = CSS.match(/\n\.seat\.seatHasButton \.seatPuckName \{([^}]*)\}/);
|
||||
|
||||
test("a ring with room for two lines indents only the line beside the chip", () => {
|
||||
assert.ok(roomy && float, "the roomy regime should use a float");
|
||||
assert.match(float[1], /float:\s*right/);
|
||||
assert.match(roomy[1], /padding-right:\s*0/);
|
||||
});
|
||||
|
||||
test("the float is exactly one line tall", () => {
|
||||
// Tall enough to cover the chip's reach into the name, and no taller: a float
|
||||
// that spills into the second line indents that one too, which puts the whole
|
||||
// problem back.
|
||||
assert.match(float[1], /height:\s*1\.2em/);
|
||||
});
|
||||
|
||||
test("break-word stands down where the float is doing the work", () => {
|
||||
// It is what stops the float working — it breaks a word to fit the short line
|
||||
// rather than letting it move past the float to the long one.
|
||||
assert.match(roomy[1], /overflow-wrap:\s*normal/);
|
||||
});
|
||||
|
||||
test("a crowded ring keeps the padding, because it has only one line", () => {
|
||||
// `.seatsTight .seatPuckName` is `line-clamp: 1` on purpose, and one line has
|
||||
// nowhere to move to — the only way to keep it off the chip is to shorten it.
|
||||
// Trying the float on both cost a whole sweep to find out: 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.
|
||||
assert.ok(base, "the base rule should still reserve the chip's width");
|
||||
assert.match(base[1], /padding-right:\s*22px/);
|
||||
assert.match(CSS, /\.seatsTight \.seatPuckName \{[^}]*line-clamp:\s*1/);
|
||||
});
|
||||
|
||||
test("the float is as wide as the chip it stands in for", () => {
|
||||
const chip = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])width:\s*(\d+)px/);
|
||||
const inset = CSS.match(/\n\.dealerChip \{[^}]*?(?<![\w-])right:\s*(\d+)px/);
|
||||
const floatW = float[1].match(/width:\s*(\d+)px/);
|
||||
assert.ok(chip && inset && floatW);
|
||||
assert.equal(Number(floatW[1]), Number(chip[1]) + Number(inset[1]),
|
||||
"the float should cover the chip and the gap it is inset by");
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("how much of the bottom is spoken for", () => {
|
||||
// feltRoomFor asks one question — what is under the felt — and gets two different
|
||||
// answers depending on the layout. Under the bar it is the bar. Under the rail it is
|
||||
|
||||
+45
-14
@@ -778,18 +778,30 @@ const LayoutAudit = {
|
||||
// concerned it had all the room it needed. Nothing else here would have caught it:
|
||||
// the occlusion check is about controls a thumb has to reach, and this is a label
|
||||
// a dealer has to read.
|
||||
// Measured against the lines of text as they were painted, not against the box they
|
||||
// live in. This used to take the box and subtract its padding, on the reasoning that
|
||||
// "padding is room the text has already been kept out of" — which is true, and is
|
||||
// not the only way to keep text out of somewhere. A right float does it too, and it
|
||||
// does it per line, which is the point of using one: the name on the button seat is
|
||||
// now indented past the chip on the line beside it and full width on the line below.
|
||||
// Box-minus-padding cannot see that, and called every seat in the sweep an offender
|
||||
// while the text sat exactly where it was supposed to.
|
||||
//
|
||||
// Range rects are the direct question — where did this text actually end up — so
|
||||
// padding, floats, centring, wrapping and a short label in a wide box all come out
|
||||
// right without any of them being modelled. Line boxes rather than one bounding box,
|
||||
// too: a two-line name that clears the chip on the line beside it is not covered
|
||||
// because its *other* line happens to sit under where the chip is.
|
||||
seatLabelsClear() {
|
||||
const offenders = [];
|
||||
document.querySelectorAll(".seat").forEach(seat => {
|
||||
const name = seat.querySelector(".seatPuckName");
|
||||
if (!name || !name.textContent.trim()) return;
|
||||
const box = name.getBoundingClientRect();
|
||||
if (!box.width || !box.height) return;
|
||||
// Padding is room the text has already been kept out of, so it is not
|
||||
// somewhere the text can be covered.
|
||||
const style = getComputedStyle(name);
|
||||
const textLeft = box.left + parseFloat(style.paddingLeft || 0);
|
||||
const textRight = box.right - parseFloat(style.paddingRight || 0);
|
||||
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(name);
|
||||
const lines = [...range.getClientRects()].filter(line => line.width > 1 && line.height > 1);
|
||||
if (!lines.length) return;
|
||||
|
||||
seat.querySelectorAll(".dealerChip, .blindBadge").forEach(mark => {
|
||||
const over = mark.getBoundingClientRect();
|
||||
@@ -797,10 +809,15 @@ const LayoutAudit = {
|
||||
const marked = getComputedStyle(mark);
|
||||
if (marked.display === "none" || marked.visibility === "hidden" ||
|
||||
parseFloat(marked.opacity) < 0.05) return;
|
||||
const covered = Math.min(textRight, over.right) - Math.max(textLeft, over.left);
|
||||
const vertical = Math.min(box.bottom, over.bottom) - Math.max(box.top, over.top);
|
||||
if (covered > 1 && vertical > 1) {
|
||||
offenders.push(`${this.describe(name)} :: ${Math.round(covered)}px under ${this.describe(mark)}`);
|
||||
|
||||
let worst = 0;
|
||||
lines.forEach(line => {
|
||||
const across = Math.min(line.right, over.right) - Math.max(line.left, over.left);
|
||||
const down = Math.min(line.bottom, over.bottom) - Math.max(line.top, over.top);
|
||||
if (across > 1 && down > 1) worst = Math.max(worst, across);
|
||||
});
|
||||
if (worst > 1) {
|
||||
offenders.push(`${this.describe(name)} :: ${Math.round(worst)}px under ${this.describe(mark)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -862,14 +879,28 @@ const LayoutAudit = {
|
||||
// Along the line rather than at its middle: a badge takes the start
|
||||
// of it and a neighbouring puck the end, and a single centre probe
|
||||
// walks past both.
|
||||
//
|
||||
// Every probe, not the first one that hits. How much of the line is
|
||||
// gone is the difference between a stack a dealer cannot read and a
|
||||
// hairline where a badge's rounded corner grazes the last digit, and
|
||||
// reporting both as "is under" gave no way to tell them apart — the
|
||||
// one residual this sweep has carried for three rounds turns out to
|
||||
// be one probe of seven.
|
||||
let hits = 0;
|
||||
let culprit = null;
|
||||
let probes = 0;
|
||||
for (let across = 0.08; across <= 0.92; across += 0.14) {
|
||||
const x = line.left + (line.right - line.left) * across;
|
||||
const y = (line.top + line.bottom) / 2;
|
||||
probes++;
|
||||
const over = document.elementFromPoint(x, y);
|
||||
if (!over || seat.contains(over) || over.contains(seat)) continue;
|
||||
offenders.push(
|
||||
`seat ${index + 1} "${text}" is under ${this.describe(over)}`);
|
||||
return;
|
||||
hits++;
|
||||
if (!culprit) culprit = over;
|
||||
}
|
||||
if (culprit) {
|
||||
offenders.push(`seat ${index + 1} "${text}" is ${hits}/${probes} `
|
||||
+ `under ${this.describe(culprit)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user