Give the sidebar eight names instead of five and a cupboard
"Session" was a container word rather than a name. It held the standings, a waiting list, the hand log and the save/load/reset buttons — a topic, a task, a log and three utilities under one label, which is a navigation layer with more than one logic in it. It measured 1206px and 103 visible elements, against 555 and 29 for the next biggest, and "The table itself" sat 1052px down inside it. Reaching Reset table meant opening a panel called Session and scrolling past the money and the log to get to it. It is now Standings, Waiting list, Hands and Save & reset, each named for the thing a dealer came looking for, in the order of a night: set the table up, watch the money, fill the seats, look back at a hand — and, last and apart, the two buttons that end it. Reset table goes from 1052px inside a panel called Session to 62px inside one whose label says what it does. The biggest panel drops from 1206px to 607px. The four subheads that used to carry those names are gone, because the summary says it now. Every panel name is also a heading. A <summary> is announced as a button, so the sidebar's names — its whole navigation — were unreachable by heading, and splitting Session would have removed the only four headings down there. The outline is now h1 page, h2 per panel, h3 for the two subheads that remain. Visually identical: the h2 gives up everything a heading brings with it. The cost is 216px more collapsed summaries at rest, three more one-line labels. Audit unchanged at 2 failing configurations and 3 offenders, both pre-existing. 795 unit tests pass. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
@@ -11,8 +11,9 @@
|
||||
phone the felt did not start until 225px down — a quarter of the screen spent
|
||||
before the dealer saw the table. Only two of the six are per-hand controls.
|
||||
The rest have gone where they belong rather than into a "more" menu, which
|
||||
would have relocated the crowding rather than removed it: Reset Table to the
|
||||
Session panel beside Clear history and the save/load pair, Quick Split down
|
||||
would have relocated the crowding rather than removed it: Reset Table down to
|
||||
the sidebar with the save/load pair — in a panel called Save & reset, so the
|
||||
danger is on the label rather than at the end of a scroll — Quick Split down
|
||||
with Common rulings where the other "open something else" link already lives,
|
||||
and the way back to the site up top where a back link goes. *@
|
||||
<div id="holdemHeaderLeft">
|
||||
@@ -67,7 +68,7 @@
|
||||
loaded: open on a table nobody has set up yet, shut on one already
|
||||
half-way through a night. *@
|
||||
<details class="sidebarPanel" id="tableSetupPanel">
|
||||
<summary>Table Setup</summary>
|
||||
<summary><h2>Table Setup</h2></summary>
|
||||
<div class="panelBody">
|
||||
<label class="fieldRow">
|
||||
<span>Seats at table</span>
|
||||
@@ -145,11 +146,23 @@
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@* These four were one panel called "Session", which is a container word rather
|
||||
than a name: it held the standings, a waiting list, the hand log and the
|
||||
save/load/reset buttons — a topic, a task, a log and three utilities, which
|
||||
is four kinds of thing under one label. It measured at 1206px and 103
|
||||
elements, and "The table itself" was 1052px down inside it, so reaching
|
||||
Reset table meant opening a panel called Session and scrolling past the
|
||||
money and the log to get there.
|
||||
|
||||
Each panel is named for the thing a dealer came looking for, and the
|
||||
subheads that used to do that job are gone, because the summary says it
|
||||
now. They are in the order of a night: set the table up, watch the money,
|
||||
fill the seats, look back at a hand — and, last and apart from the rest,
|
||||
the two buttons that end it. *@
|
||||
<details class="sidebarPanel">
|
||||
<summary>Session</summary>
|
||||
<summary><h2>Standings</h2></summary>
|
||||
<div class="panelBody">
|
||||
<p id="storageWarning" class="storageWarning" hidden></p>
|
||||
<h3 class="panelSubhead">Standings</h3>
|
||||
<div class="standingsTableWrap">
|
||||
<div id="standingsTable">
|
||||
<div class="standingsRow standingsHeader">
|
||||
@@ -165,14 +178,6 @@
|
||||
</div>
|
||||
<p id="standingsEmptyHint" class="fieldHint">Everyone who has bought in shows up here, with what they'd be owed if the night ended now.</p>
|
||||
|
||||
<h3 class="panelSubhead">Waiting list</h3>
|
||||
<form id="waitingListForm" class="waitingAdd">
|
||||
<input type="text" id="waitingNameInput" maxlength="24" placeholder="Who's waiting?" autocomplete="off" />
|
||||
<button type="submit" id="waitingAddBtn" class="holdemBtn holdemBtnSmall">Add</button>
|
||||
</form>
|
||||
<ol id="waitingList" class="waitingList"></ol>
|
||||
<p id="waitingEmptyHint" class="fieldHint">Nobody waiting. Add the people standing behind the chairs, and the tool will offer them the next seat that opens.</p>
|
||||
|
||||
<div id="bankReconcile" class="bankReconcile" hidden>
|
||||
<div class="bankReconcileRow">
|
||||
<span>Cash taken</span>
|
||||
@@ -203,8 +208,24 @@
|
||||
<ul id="settleUpList" class="settleUpList"></ul>
|
||||
<p id="settleUpNote" class="fieldHint"></p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<h3 class="panelSubhead">Hands</h3>
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Waiting list</h2></summary>
|
||||
<div class="panelBody">
|
||||
<form id="waitingListForm" class="waitingAdd">
|
||||
<input type="text" id="waitingNameInput" maxlength="24" placeholder="Who's waiting?" autocomplete="off" />
|
||||
<button type="submit" id="waitingAddBtn" class="holdemBtn holdemBtnSmall">Add</button>
|
||||
</form>
|
||||
<ol id="waitingList" class="waitingList"></ol>
|
||||
<p id="waitingEmptyHint" class="fieldHint">Nobody waiting. Add the people standing behind the chairs, and the tool will offer them the next seat that opens.</p>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Hands</h2></summary>
|
||||
<div class="panelBody">
|
||||
<div class="handHistoryFilterRow">
|
||||
<input id="handHistoryFilter" type="search" class="handHistoryFilter"
|
||||
placeholder="Find a hand — name, amount or #" aria-label="Filter the hand log by player, amount or hand number" />
|
||||
@@ -218,27 +239,11 @@
|
||||
<button id="exportHistoryBtn" class="holdemBtn holdemBtnSmall">Export CSV</button>
|
||||
<button id="clearHistoryBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Clear history</button>
|
||||
</div>
|
||||
|
||||
<h3 class="panelSubhead">The table itself</h3>
|
||||
<p class="fieldHint">
|
||||
The night lives in this browser and nowhere else. Save it to a file to carry it
|
||||
to another device, or to have something to come back to if this one dies.
|
||||
</p>
|
||||
<div class="handHistoryActions">
|
||||
<button id="saveTableBtn" class="holdemBtn holdemBtnSmall">Save table file</button>
|
||||
<button id="loadTableBtn" class="holdemBtn holdemBtnSmall">Load table file</button>
|
||||
<input id="loadTableInput" type="file" accept="application/json,.json" hidden />
|
||||
@* Down from the header, where it sat in the same weight as Resolve Pot
|
||||
— the most-pressed control in a session next to the one that throws
|
||||
the night away. Here it is beside Clear history, which is the other
|
||||
thing that destroys records, and behind a panel you have to open. *@
|
||||
<button id="resetTableBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Reset table</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Chip Legend</summary>
|
||||
<summary><h2>Chip Legend</h2></summary>
|
||||
<div class="panelBody">
|
||||
<div id="chipLegendList"></div>
|
||||
<button id="addChipColorBtn" class="holdemBtn holdemBtnSmall">+ Add chip color</button>
|
||||
@@ -253,7 +258,7 @@
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel" id="keyboardPanel">
|
||||
<summary>Keyboard</summary>
|
||||
<summary><h2>Keyboard</h2></summary>
|
||||
<div class="panelBody">
|
||||
<dl id="shortcutLegend" class="shortcutLegend"></dl>
|
||||
<p class="fieldHint">Nothing fires while you're typing in a field, or while a sheet is open over the table.</p>
|
||||
@@ -261,11 +266,37 @@
|
||||
</details>
|
||||
|
||||
<details class="sidebarPanel">
|
||||
<summary>Table Notes</summary>
|
||||
<summary><h2>Table Notes</h2></summary>
|
||||
<div class="panelBody">
|
||||
<textarea id="tableNotes" rows="4" aria-label="Table notes" placeholder="Anything else worth tracking — side bets, IOUs, running totals..."></textarea>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
@* Last, and on its own, because everything in it is about the night as a whole
|
||||
rather than the hand in front of you — and because two of the three buttons
|
||||
are the ones you cannot take back.
|
||||
|
||||
Reset table came down from the header a while ago, where it sat in the same
|
||||
weight as Resolve Pot: the most-pressed control in a session beside the one
|
||||
that throws the night away. It used to be buried at the bottom of Session,
|
||||
1052px down, which hid it but also meant nobody could find it on purpose.
|
||||
Naming the panel for both jobs puts the danger on the label, where it is
|
||||
read before the panel is opened rather than met at the end of a scroll. *@
|
||||
<details class="sidebarPanel">
|
||||
<summary><h2>Save & reset</h2></summary>
|
||||
<div class="panelBody">
|
||||
<p class="fieldHint">
|
||||
The night lives in this browser and nowhere else. Save it to a file to carry it
|
||||
to another device, or to have something to come back to if this one dies.
|
||||
</p>
|
||||
<div class="handHistoryActions">
|
||||
<button id="saveTableBtn" class="holdemBtn holdemBtnSmall">Save table file</button>
|
||||
<button id="loadTableBtn" class="holdemBtn holdemBtnSmall">Load table file</button>
|
||||
<input id="loadTableInput" type="file" accept="application/json,.json" hidden />
|
||||
<button id="resetTableBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Reset table</button>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -2217,6 +2217,26 @@ button,
|
||||
letter-spacing: 0.03em;
|
||||
}
|
||||
|
||||
/* The panel's name is a heading as well as a control.
|
||||
|
||||
A <summary> is announced as a button and not as a heading, so the sidebar's names —
|
||||
which are the whole of its navigation — were invisible to anyone moving through the
|
||||
page by heading. The four that were headings were the subheads buried inside the old
|
||||
Session panel, and splitting it took those away, so without this the page would have
|
||||
lost the outline rather than gained one. The spec allows heading content in a summary
|
||||
and both VoiceOver and NVDA expose it.
|
||||
|
||||
It has to look like nothing happened, so the heading gives up everything a heading
|
||||
brings with it. Inline, because a block child of a summary pushes the disclosure
|
||||
triangle onto its own line. */
|
||||
.sidebarPanel > summary > h2 {
|
||||
display: inline;
|
||||
margin: 0;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
letter-spacing: inherit;
|
||||
}
|
||||
|
||||
.panelBody {
|
||||
padding-bottom: var(--sp-4);
|
||||
display: flex;
|
||||
|
||||
@@ -25,6 +25,7 @@ other in the page, in the load order from `TexasHoldem.cshtml`.
|
||||
| `theRail.test.js` | A phone on its side: which shape the bar is in, what it leaves under the felt, how a name is shortened, how the ring is pulled back onto the table |
|
||||
| `feltShape.test.js` | What shape the felt is at each phone size, resolved through the media queries in source order (see below) |
|
||||
| `potBreakdown.test.js` | How each pot says who can win it, and the nesting property that wording rests on (see below) |
|
||||
| `sidebarPanels.test.js` | What the sidebar's panels are called and what lives in each (see below) |
|
||||
| `invariants.test.js` | 1000 randomised hands per property (see below) |
|
||||
|
||||
## The layout audit
|
||||
@@ -211,6 +212,39 @@ everything, which is how it survived long enough to be written down. Both readin
|
||||
silent. Prefer asserting the count you expect over comparing two things whose shapes you
|
||||
have not checked.
|
||||
|
||||
## Testing a fault that broke nothing
|
||||
|
||||
`sidebarPanels.test.js` reads `TexasHoldem.cshtml` rather than any JavaScript, because
|
||||
the fault it covers was not a bug. Every control worked and every id resolved. What was
|
||||
wrong was which panel a thing was in and what that panel was called.
|
||||
|
||||
The sidebar was Table Setup, **Session**, Chip Legend, Keyboard and Table Notes, and
|
||||
Session held the standings, a waiting list, the hand log and the save/load/reset buttons
|
||||
— a topic, a task, a log and three utilities under one word. It measured 1206px and 103
|
||||
visible elements against 555 and 29 for the next biggest, and "The table itself" sat
|
||||
1052px down inside it, so reaching Reset table meant opening a panel called Session and
|
||||
scrolling past the money and the log to find it.
|
||||
|
||||
Three things are asserted, and each of them is a claim a future edit could quietly undo:
|
||||
|
||||
- **The names say what is inside them.** The list is checked literally, because the
|
||||
failure mode is a word like "Session" that could hold anything.
|
||||
- **Each name is a heading.** A `<summary>` is announced as a button, so the sidebar's
|
||||
names — its whole navigation — were not reachable by heading. Splitting Session also
|
||||
removed the four subheads that had been the only headings down there, so without a
|
||||
heading in each summary the page would have come out of this with a worse outline than
|
||||
it went in with.
|
||||
- **No panel goes back to being a grab-bag.** Not a size limit — Table Setup is
|
||||
legitimately long. It counts *subjects*: a panel that needs its own headings to list
|
||||
its parts is holding more than one thing. Two subheads survive, and the test names
|
||||
them, because Settle up is the standings at the end of the night and Stacks in chips
|
||||
is the chip legend counted out — the same subject at a different moment, which is what
|
||||
a subhead is for.
|
||||
|
||||
The panels are split on the opening tag rather than parsed, which is sound only because a
|
||||
sidebar panel never contains another one; Table Setup's sub-panels carry a different
|
||||
class. That assumption is worth knowing about if one ever does.
|
||||
|
||||
## Why the hand evaluator is tested the way it is
|
||||
|
||||
`handRank.js` is the only module here that can pay the wrong person without anybody
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
// What the sidebar is a list of.
|
||||
//
|
||||
// The panels were Table Setup, Session, Chip Legend, Keyboard and Table Notes, and
|
||||
// "Session" was a container word rather than a name. It held the standings, a waiting
|
||||
// list, the hand log and the save/load/reset buttons — a topic, a task, a log and three
|
||||
// utilities under one label, which is the "don't mix apples and oranges" failure: a
|
||||
// navigation layer with more than one logic in it. Measured at 1206px and 103 visible
|
||||
// elements, against 555 and 29 for the next biggest, and "The table itself" sat 1052px
|
||||
// down inside it, so reaching Reset table meant opening a panel called Session and
|
||||
// scrolling past the money and the log.
|
||||
//
|
||||
// This is a markup test because that is where the fault lived. Nothing was broken —
|
||||
// every control worked and every id resolved — so nothing failed. What was wrong was
|
||||
// which panel a thing was in and what that panel was called.
|
||||
const test = require("node:test");
|
||||
const { describe } = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const PAGE = fs.readFileSync(
|
||||
path.join(__dirname, "..", "..", "JoshHeaps.Net", "Pages", "Casino", "TexasHoldem.cshtml"), "utf8");
|
||||
|
||||
// A sidebar panel never contains another one — Table Setup's sub-panels carry a
|
||||
// different class — so splitting on the opening tag is enough to say which panel any
|
||||
// piece of markup is in, without parsing the document.
|
||||
const panels = () => PAGE
|
||||
.split(/<details class="sidebarPanel"/)
|
||||
.slice(1)
|
||||
.map(chunk => {
|
||||
const name = chunk.match(/<summary>\s*(?:<h2>)?([^<]+)/);
|
||||
return { name: name ? name[1].trim() : "(no summary)", markup: chunk };
|
||||
});
|
||||
|
||||
const panelHolding = id => {
|
||||
const found = panels().filter(p => p.markup.includes(`id="${id}"`));
|
||||
assert.equal(found.length, 1, `${id} is in ${found.length} panels, expected exactly 1`);
|
||||
return found[0].name;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe("the sidebar's names", () => {
|
||||
test("say what is inside them", () => {
|
||||
// Every one of these is a thing a dealer goes looking for. None of them is a
|
||||
// word that could hold anything — which is the test "Session" fails.
|
||||
assert.deepEqual(panels().map(p => p.name), [
|
||||
"Table Setup",
|
||||
"Standings",
|
||||
"Waiting list",
|
||||
"Hands",
|
||||
"Chip Legend",
|
||||
"Keyboard",
|
||||
"Table Notes",
|
||||
"Save & reset",
|
||||
]);
|
||||
});
|
||||
|
||||
test("and each one is a heading, not only a button", () => {
|
||||
// A <summary> is announced as a button. The sidebar's names are the whole of its
|
||||
// navigation, so without a heading inside, none of them is reachable by anyone
|
||||
// moving through the page by heading — and splitting Session removed the four
|
||||
// subheads that used to be the only headings down here.
|
||||
panels().forEach(p => assert.match(p.markup, /<summary><h2>/,
|
||||
`the ${p.name} panel's name is not a heading`));
|
||||
});
|
||||
});
|
||||
|
||||
describe("where things live", () => {
|
||||
test("the money is together, and the warning about losing it is with it", () => {
|
||||
assert.equal(panelHolding("standingsList"), "Standings");
|
||||
assert.equal(panelHolding("settleUp"), "Standings");
|
||||
assert.equal(panelHolding("bankReconcile"), "Standings");
|
||||
// renderStorageWarning() says this "sits at the top of the panel a dealer opens
|
||||
// to check the money". That panel is now called Standings.
|
||||
assert.equal(panelHolding("storageWarning"), "Standings");
|
||||
});
|
||||
|
||||
test("the queue for a seat is its own panel", () => {
|
||||
assert.equal(panelHolding("waitingList"), "Waiting list");
|
||||
assert.equal(panelHolding("waitingListForm"), "Waiting list");
|
||||
});
|
||||
|
||||
test("the log and the two ways out of it are together", () => {
|
||||
assert.equal(panelHolding("handHistoryList"), "Hands");
|
||||
assert.equal(panelHolding("handHistoryFilter"), "Hands");
|
||||
assert.equal(panelHolding("exportHistoryBtn"), "Hands");
|
||||
assert.equal(panelHolding("clearHistoryBtn"), "Hands");
|
||||
});
|
||||
|
||||
test("and the file is last, with the danger on the label", () => {
|
||||
assert.equal(panelHolding("saveTableBtn"), "Save & reset");
|
||||
assert.equal(panelHolding("loadTableBtn"), "Save & reset");
|
||||
assert.equal(panelHolding("resetTableBtn"), "Save & reset");
|
||||
assert.equal(panels()[panels().length - 1].name, "Save & reset",
|
||||
"the panel that ends the night belongs at the end, away from the working ones");
|
||||
});
|
||||
});
|
||||
|
||||
describe("no panel goes back to being a grab-bag", () => {
|
||||
// Not a size limit — Table Setup is legitimately long and Chip Legend legitimately
|
||||
// repeats a row per chip. What this counts is *subjects*: a panel that needs its own
|
||||
// headings to say what its parts are is a panel holding more than one thing, which
|
||||
// is what Session was and how it got that way.
|
||||
test("at most one subject needs a heading inside a panel", () => {
|
||||
panels().forEach(p => {
|
||||
const subheads = [...p.markup.matchAll(/<h3 class="panelSubhead">([^<]+)</g)]
|
||||
.map(m => m[1].trim());
|
||||
assert.ok(subheads.length <= 1,
|
||||
`${p.name} has ${subheads.length} sections inside it (${subheads.join(", ")}) — ` +
|
||||
`if a panel needs headings to list its parts, its name is not doing the work`);
|
||||
});
|
||||
});
|
||||
|
||||
test("and the ones that remain are a detail of their panel, not a second subject", () => {
|
||||
// Settle up is the standings once the night ends; Stacks in chips is the chip
|
||||
// legend counted out. Both are the same subject at a different moment, which is
|
||||
// the case a subhead is for.
|
||||
const kept = panels().flatMap(p =>
|
||||
[...p.markup.matchAll(/<h3 class="panelSubhead">([^<]+)</g)]
|
||||
.map(m => `${p.name}: ${m[1].trim()}`));
|
||||
assert.deepEqual(kept, ["Standings: Settle up", "Chip Legend: Stacks in chips"]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user