Measured every overlay in the tool before touching anything. For a dealer running this on a laptop, all six were broken in the same three ways. - focus never moved into the dialog: 0 of 5 measured - Tab walked straight out of the dialog into the page behind the scrim: 5 of 5 - Escape did nothing: only the rulings sheet handled it, 1 of 5 - the pot wizard — the dialog that pays people — had no role and no accessible name at all So opening the pot wizard left focus on the button behind it, tabbing wandered through a page nobody can see or click, and there was no way back out except the mouse. One implementation, not six The overlays are opened and closed by five different modules, which is why they each missed the same things. Rather than patch this into all of them, modal.js watches the `hidden` attribute they already toggle: focus moves to the first real control on open, Tab and Shift-Tab wrap inside the dialog, Escape closes it, and focus returns to whatever opened it. `aria-modal` is set while open and removed on close. An overlay added later gets all of it without anyone remembering to wire it up, which is the failure being fixed. Escape presses the dialog's own cancel button rather than just hiding the overlay, so the confirm dialog still resolves its promise — verified: Escape resolves it to false rather than leaving an await hanging forever. Shortcuts.overlayOpen() had the same shape of bug, listing the six overlay ids by hand. It asks the document now, so a sheet added later can't quietly start letting "f" fold the table behind it. Focus you can see Three focus rules existed for the whole stylesheet, so tabbing relied on the browser default ring, which on these dark custom-painted controls is close to invisible. `:focus-visible` keeps it to keyboard use so a dealer tapping Fold isn't left with a ring on it. That surfaced something small: `.holdemBtn` used `transition: all`, so the new outline faded in over 150ms and the ring lagged a step behind the Tab key. The transition names its properties now. Measured after: 16 of 16 tab stops show a ring immediately, and the press state from before still animates. Also - a mistyped negative stack read back as negative, and flowed into putIn() where Math.min(amount, -50) moves a negative amount and adds chips to the table. Clamped at zero. - accessible names for the chip colour, chip value and notes fields, which had none 11 new tests (99 total), covering negative stacks, negative and unreadable blinds, blinds nobody can cover, and a save that comes back corrupt, truncated, or from before a field existed. Playwright: all five overlays re-measured — focus moves in 5/5, Tab and Shift-Tab trapped 5/5, Escape closes 5/5, focus restored 5/5, role and aria-modal present 5/5. Shortcuts still fire when no dialog is open and are still blocked when one is. Touch guarantees from the earlier round unchanged: 0 of 49 controls on touch-action auto, 0 tap-highlighted. Co-Authored-By: Claude Opus 5 <[email protected]> Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
380 lines
17 KiB
Plaintext
380 lines
17 KiB
Plaintext
@page
|
||
@model JoshHeaps.Net.Pages.Casino.TexasHoldemModel
|
||
@{
|
||
ViewData["Title"] = "Texas Hold'em Dealer";
|
||
Layout = "_Layout";
|
||
}
|
||
|
||
<div id="holdemApp">
|
||
<header id="holdemHeader">
|
||
<div id="holdemHeaderLeft">
|
||
<h1>Texas Hold'em Dealer</h1>
|
||
<p class="holdemSubtitle">Seat your players, track the action, split the pot.</p>
|
||
<span id="handCounterBadge" class="handCounterBadge">Hand #1</span>
|
||
</div>
|
||
<div id="holdemHeaderActions">
|
||
<a href="/casino/texasholdem/quicksplit" class="holdemBtn" title="Chop a pot without setting up a table">Quick Split</a>
|
||
<button id="nextHandBtn" class="holdemBtn" title="Move the button, small blind and big blind to the next occupied seat">Next Hand ⟳</button>
|
||
<button id="resolvePotBtn" class="holdemBtn holdemBtnAccent">Resolve Pot</button>
|
||
<button id="resetTableBtn" class="holdemBtn holdemBtnDanger">Reset Table</button>
|
||
<a href="/casino" class="holdemBtn holdemBtnGhost">← Casino</a>
|
||
</div>
|
||
</header>
|
||
|
||
<div id="holdemLayout">
|
||
<section id="tableSection">
|
||
<div id="pokerTableWrap">
|
||
<div id="pokerTable">
|
||
<div id="tableWatermark">Texas Hold'em</div>
|
||
<div id="potBadge">
|
||
<span id="potBadgeLabel">Pot</span>
|
||
<span id="potBadgeAmount">$0</span>
|
||
</div>
|
||
</div>
|
||
<div id="betsLayer" aria-hidden="true"></div>
|
||
<div id="seatsLayer"></div>
|
||
</div>
|
||
<p id="tableHint" class="tableHint"></p>
|
||
<p class="tableHintActions">
|
||
<button type="button" id="openRulingsBtn" class="linkBtn">Common rulings</button>
|
||
</p>
|
||
</section>
|
||
|
||
<aside id="holdemSidebar">
|
||
<section id="seatEditorPanel" class="sidebarPanel seatEditorPanel">
|
||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||
<div class="seatEditorHeader">
|
||
<h2 id="seatEditorTitle">Select a seat</h2>
|
||
<button id="closeSeatEditorBtn" class="seatSheetClose" aria-label="Done editing this seat">Done</button>
|
||
</div>
|
||
<div id="seatEditorBody">
|
||
<p class="fieldHint">Click a seat at the table to edit their name, cards, chips and anything else.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<details class="sidebarPanel" open>
|
||
<summary>Table Setup</summary>
|
||
<div class="panelBody">
|
||
<label class="fieldRow">
|
||
<span>Seats at table</span>
|
||
<input type="number" id="seatCountInput" min="2" max="24" value="10" />
|
||
</label>
|
||
<label class="fieldRow">
|
||
<span>Blinds move</span>
|
||
<select id="directionSelect">
|
||
<option value="clockwise" selected>Clockwise</option>
|
||
<option value="counterclockwise">Counterclockwise</option>
|
||
</select>
|
||
</label>
|
||
<label class="fieldRow">
|
||
<span>Dedicated dealer (not a seat)</span>
|
||
<input type="checkbox" id="dedicatedDealerToggle" />
|
||
</label>
|
||
<p class="fieldHint">Turn this on if a person outside the game deals — no seat shows the dealer button, but blinds still rotate automatically each hand.</p>
|
||
<div class="blindsRow">
|
||
<label class="fieldRow">
|
||
<span>Small blind</span>
|
||
<input type="number" id="smallBlindInput" min="0" step="0.5" />
|
||
</label>
|
||
<label class="fieldRow">
|
||
<span>Big blind</span>
|
||
<input type="number" id="bigBlindInput" min="0" step="0.5" />
|
||
</label>
|
||
</div>
|
||
<label class="fieldRow">
|
||
<span>Starting stack ($)</span>
|
||
<input type="number" id="defaultStackInput" min="0" step="1" placeholder="e.g. 200" />
|
||
</label>
|
||
<p class="fieldHint">Blinds post themselves each hand. Seats without their own stack start here.</p>
|
||
|
||
<details class="subPanel">
|
||
<summary>Without live betting</summary>
|
||
<div class="subPanelBody">
|
||
<label class="fieldRow">
|
||
<span>Base pot amount ($)</span>
|
||
<input type="number" id="basePotInput" min="0" step="1" placeholder="e.g. 500" />
|
||
</label>
|
||
<p class="fieldHint">For tracking a hand by hand instead of tapping the action: everyone is assumed to have put in this much, unless their seat says otherwise.</p>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="sidebarPanel">
|
||
<summary>Session</summary>
|
||
<div class="panelBody">
|
||
<h3 class="panelSubhead">Standings</h3>
|
||
<div id="standingsTable">
|
||
<div class="standingsRow standingsHeader">
|
||
<span>Player</span>
|
||
<span>In</span>
|
||
<span>Won</span>
|
||
<span>Net</span>
|
||
</div>
|
||
<div id="standingsList"></div>
|
||
</div>
|
||
<p id="standingsEmptyHint" class="fieldHint">Once you resolve a pot, everyone's running total for the night shows up here.</p>
|
||
|
||
<h3 class="panelSubhead">Hands</h3>
|
||
<div id="handHistoryList"></div>
|
||
<p id="handHistoryEmptyHint" class="fieldHint">Resolved pots are logged here, in case a chip count is disputed later.</p>
|
||
|
||
<div class="handHistoryActions">
|
||
<button id="exportHistoryBtn" class="holdemBtn holdemBtnSmall">Export CSV</button>
|
||
<button id="clearHistoryBtn" class="holdemBtn holdemBtnSmall holdemBtnDanger">Clear history</button>
|
||
</div>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="sidebarPanel">
|
||
<summary>Chip Legend</summary>
|
||
<div class="panelBody">
|
||
<div id="chipLegendList"></div>
|
||
<button id="addChipColorBtn" class="holdemBtn holdemBtnSmall">+ Add chip color</button>
|
||
<p class="fieldHint">Give each chip color a value, then count chips on a seat instead of typing a total.</p>
|
||
</div>
|
||
</details>
|
||
|
||
<details class="sidebarPanel">
|
||
<summary>Table Notes</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>
|
||
</aside>
|
||
</div>
|
||
</div>
|
||
|
||
<template id="seatTemplate">
|
||
<div class="seat" draggable="true">
|
||
<div class="seatChairNumber"></div>
|
||
<button class="dealerChip" title="Make this seat the dealer button">D</button>
|
||
<span class="blindBadge sbBadge">SB</span>
|
||
<span class="blindBadge bbBadge">BB</span>
|
||
<div class="seatPuckName"></div>
|
||
<div class="seatPuckCards"></div>
|
||
<div class="seatPuckTotal"></div>
|
||
<div class="seatPuckStack"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="seatEditorTemplate">
|
||
<div class="seatEditorForm">
|
||
<label class="fieldRow">
|
||
<span>Name</span>
|
||
<input type="text" class="seatNameInput" placeholder="Seat" maxlength="24" />
|
||
</label>
|
||
<div class="seatCardToggles">
|
||
<label class="inHandToggle" title="This seat is playing the current hand">
|
||
<input type="checkbox" class="inHandCheckbox" />
|
||
<span>In hand</span>
|
||
</label>
|
||
<label class="foldToggle" title="Folded">
|
||
<input type="checkbox" class="foldCheckbox" />
|
||
<span>Fold</span>
|
||
</label>
|
||
</div>
|
||
<div class="seatCards">
|
||
<button type="button" class="cardSlot card1Slot" aria-label="Choose first card"></button>
|
||
<button type="button" class="cardSlot card2Slot" aria-label="Choose second card"></button>
|
||
</div>
|
||
<label class="fieldRow stackRow">
|
||
<span>Stack</span>
|
||
<input type="number" class="stackInput" min="0" step="1" placeholder="table default" />
|
||
</label>
|
||
<div class="seatBuyIns" hidden></div>
|
||
<div class="seatContribution">
|
||
<div class="chipCounts"></div>
|
||
<label class="fieldRow amountOverrideRow">
|
||
<span>$ amount</span>
|
||
<input type="number" class="amountOverrideInput" min="0" step="1" placeholder="auto" />
|
||
</label>
|
||
<div class="seatTotal">Total: <span class="seatTotalAmount">$0</span></div>
|
||
</div>
|
||
<div class="customFields"></div>
|
||
<button class="holdemBtn holdemBtnSmall addFieldBtn">+ field</button>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="chipLegendRowTemplate">
|
||
<div class="chipLegendRow">
|
||
<input type="color" class="chipColorInput" aria-label="Chip colour" />
|
||
<input type="number" class="chipValueInput" min="0" step="0.01" placeholder="value" aria-label="Chip value" />
|
||
<button class="holdemBtn holdemBtnSmall holdemBtnDanger removeChipColorBtn">×</button>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="chipCountTemplate">
|
||
<label class="chipCountRow">
|
||
<span class="chipSwatch"></span>
|
||
<input type="number" class="chipCountInput" min="0" step="1" value="0" />
|
||
</label>
|
||
</template>
|
||
|
||
<template id="customFieldTemplate">
|
||
<div class="customFieldRow">
|
||
<input type="text" class="customFieldLabel" placeholder="Label" maxlength="20" />
|
||
<input type="text" class="customFieldValue" placeholder="Value" maxlength="40" />
|
||
<button class="holdemBtn holdemBtnSmall holdemBtnDanger removeFieldBtn">×</button>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="standingsRowTemplate">
|
||
<div class="standingsRow">
|
||
<span class="standingsName"></span>
|
||
<span class="standingsIn"></span>
|
||
<span class="standingsWon"></span>
|
||
<span class="standingsNet"></span>
|
||
</div>
|
||
</template>
|
||
|
||
<template id="handHistoryRowTemplate">
|
||
<div class="handHistoryRow">
|
||
<div class="handHistoryRowHeader">
|
||
<span class="handHistoryHandNumber"></span>
|
||
<span class="handHistoryPot"></span>
|
||
<span class="handHistoryTime"></span>
|
||
</div>
|
||
<div class="handHistoryPayouts"></div>
|
||
</div>
|
||
</template>
|
||
|
||
<div id="addChipsOverlay" hidden>
|
||
<div id="addChipsSheet" role="dialog" aria-labelledby="addChipsTitle">
|
||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||
<div class="addChipsHeader">
|
||
<h2 id="addChipsTitle"></h2>
|
||
<span id="addChipsSubtitle"></span>
|
||
</div>
|
||
<div id="addChipsQuick"></div>
|
||
<div class="addChipsEntry">
|
||
<span class="addChipsCurrency">$</span>
|
||
<input type="text" id="addChipsInput" inputmode="decimal" autocomplete="off" aria-label="Amount to add" />
|
||
</div>
|
||
<div class="addChipsActions">
|
||
<button id="addChipsConfirmBtn" class="holdemBtn holdemBtnAccent" type="button">Add to stack</button>
|
||
<button id="addChipsCancelBtn" data-modal-close class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="rulingsOverlay" hidden>
|
||
<div id="rulingsSheet" role="dialog" aria-labelledby="rulingsTitle">
|
||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||
<div class="rulingsHeader">
|
||
<h2 id="rulingsTitle">Common rulings</h2>
|
||
<button id="closeRulingsBtn" data-modal-close class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
|
||
</div>
|
||
|
||
<p class="rulingsIntro">
|
||
The conventions most rooms follow, for the calls that come up mid-hand.
|
||
Your house rules always win — and settle a dispute before the next hand,
|
||
not during it.
|
||
</p>
|
||
|
||
<section class="ruling">
|
||
<h3>Redeal it — misdeal</h3>
|
||
<ul>
|
||
<li>The first card goes to the wrong seat.</li>
|
||
<li>A seat entitled to a hand gets dealt out, or a seat that isn't entitled gets dealt in.</li>
|
||
<li>Two or more boxed cards turn up on the initial deal.</li>
|
||
<li>Either of the first two cards is exposed by dealer error.</li>
|
||
</ul>
|
||
</section>
|
||
|
||
<section class="ruling">
|
||
<h3>A board card turned too early</h3>
|
||
<p>
|
||
If the fourth board card gets turned before the betting round is finished,
|
||
that card is out of play for the round. Finish the betting, then burn and
|
||
turn what would have been the fifth card in the fourth card's place.
|
||
</p>
|
||
</section>
|
||
|
||
<section class="ruling">
|
||
<h3>Acting out of turn</h3>
|
||
<p>
|
||
A one-off is usually just backed up if no one has acted behind them.
|
||
Doing it repeatedly is what earns a penalty.
|
||
</p>
|
||
</section>
|
||
|
||
<p class="rulingsSource">
|
||
Follows the Tournament Directors Association rules, which most rooms and
|
||
home games take their lead from.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="seatMenuOverlay" hidden>
|
||
<div id="seatMenuSheet" role="dialog" aria-labelledby="seatMenuTitle">
|
||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||
<div class="seatMenuHeader">
|
||
<h2 id="seatMenuTitle"></h2>
|
||
<span id="seatMenuSubtitle"></span>
|
||
</div>
|
||
<div id="seatMenuActions"></div>
|
||
<button id="seatMenuCancelBtn" data-modal-close class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="cardPickerOverlay" hidden>
|
||
<div id="cardPickerSheet" role="dialog" aria-labelledby="cardPickerTitle">
|
||
<div class="seatSheetGrip" aria-hidden="true"></div>
|
||
<div class="cardPickerHeader">
|
||
<h2 id="cardPickerTitle"></h2>
|
||
<button id="cardPickerClearBtn" class="holdemBtn holdemBtnSmall" type="button">Clear</button>
|
||
<button id="cardPickerCloseBtn" data-modal-close class="holdemBtn holdemBtnSmall holdemBtnGhost" type="button">Done</button>
|
||
</div>
|
||
<div id="cardPickerSlots"></div>
|
||
<div id="cardPickerGrid"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="actionBar" aria-live="polite" hidden></div>
|
||
|
||
<div id="toastHost" aria-live="polite"></div>
|
||
|
||
<div id="confirmOverlay" hidden>
|
||
<div id="confirmModal" role="alertdialog" aria-labelledby="confirmTitle" aria-describedby="confirmMessage" tabindex="-1">
|
||
<h2 id="confirmTitle"></h2>
|
||
<p id="confirmMessage"></p>
|
||
<div class="confirmActions">
|
||
<button id="confirmCancelBtn" data-modal-close class="holdemBtn holdemBtnGhost" type="button">Cancel</button>
|
||
<button id="confirmAcceptBtn" class="holdemBtn holdemBtnDanger" type="button">Confirm</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="potWizardOverlay" hidden>
|
||
<div id="potWizardModal" role="dialog" aria-labelledby="potWizardTitle">
|
||
<div class="potWizardHeader">
|
||
<h2 id="potWizardTitle">Resolve the Pot</h2>
|
||
<button id="closePotWizardBtn" data-modal-close class="holdemBtn holdemBtnGhost">✕</button>
|
||
</div>
|
||
<div id="potWizardBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
@section Styles {
|
||
<link href="/css/casino/holdem.css?v=@ViewData["cssVersion"]" rel="stylesheet" type="text/css" />
|
||
}
|
||
|
||
@section Scripts {
|
||
<script src="~/js/casino/holdem/money.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/feedback.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/potCalculator.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/state.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/render.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/cardPicker.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/seatMenu.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/addChips.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/betting.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/actionBar.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/shortcuts.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/potWizard.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/handHistoryExport.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/modal.js?v=@ViewData["cssVersion"]"></script>
|
||
<script src="~/js/casino/holdem/main.js?v=@ViewData["cssVersion"]"></script>
|
||
}
|