Add dedicated-dealer mode and redesign the table's visuals

Add a "Dedicated dealer (not a seat)" toggle for tables run by someone
outside the game: it hides every seat's dealer button while blinds keep
rotating automatically off the same internal position each hand.

Also gave the table a real casino look, based on reference felt/rail and
seat-pod patterns found via web search (huggingface.co/spaces/Estazz/
PokerGameDesigner, github.com/Tehes/poker): a richer radial felt gradient,
a gold piping ring inside a wood-toned rail, a subtle center watermark, a
pill-shaped chip-well pot badge, softer seat pucks with a pill-badge total
and red/black suit coloring on cards, and casino-chip-styled dealer/blind
badges (the dealer chip now dims to a faint disc on other seats and only
lights up on hover, decluttering the ring of ten clickable buttons down to
one visible button on the actual dealer).

Verified via Playwright MCP: toggle hides/reveals dealer chips correctly,
SB/BB still track it, no console errors.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_018CmAti1baCjidbJM2HeJfk
This commit is contained in:
2026-08-31 15:26:32 -06:00
co-authored by Claude Sonnet 5
parent 25bf67062a
commit 3851786b90
5 changed files with 165 additions and 56 deletions
@@ -23,6 +23,7 @@
<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>
@@ -30,7 +31,7 @@
</div>
<div id="seatsLayer"></div>
</div>
<p class="tableHint">Drag a seat onto another seat to swap players. Click the dealer chip on a seat to make it the button.</p>
<p id="tableHint" class="tableHint"></p>
</section>
<aside id="holdemSidebar">
@@ -55,6 +56,11 @@
<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>
<label class="fieldRow">
<span>Base pot amount ($)</span>
<input type="number" id="basePotInput" min="0" step="1" placeholder="e.g. 500" />
+125 -53
View File
@@ -1,5 +1,5 @@
body {
background: #141414;
background: radial-gradient(ellipse at 50% -10%, #1c1c1c 0%, #101010 55%, #0a0a0a 100%);
min-height: 100vh;
margin: 0;
color: #e6e6e6;
@@ -29,6 +29,20 @@ body {
font-weight: 300;
letter-spacing: 0.04em;
color: #f5f5f5;
position: relative;
display: inline-block;
padding-bottom: 0.4rem;
}
#holdemHeader h1::after {
content: "";
position: absolute;
left: 0;
bottom: 0;
width: 42px;
height: 3px;
border-radius: 2px;
background: linear-gradient(90deg, #d8b46a, #a9782f);
}
.holdemSubtitle {
@@ -44,43 +58,45 @@ body {
}
.holdemBtn {
background: #2a2a2a;
background: linear-gradient(180deg, #2e2e2e, #232323);
color: #eee;
border: 1px solid #3a3a3a;
border-radius: 6px;
border-radius: 7px;
padding: 0.6rem 1rem;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
transition: all 0.15s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.3rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
.holdemBtn:hover {
background: #333;
background: linear-gradient(180deg, #383838, #2a2a2a);
border-color: #4a4a4a;
transform: translateY(-1px);
}
.holdemBtnAccent {
background: #0f5c1c;
background: linear-gradient(180deg, #16783a, #0d5225);
border-color: #1e8449;
color: #d6ffe0;
color: #ecffef;
}
.holdemBtnAccent:hover {
background: #146623;
background: linear-gradient(180deg, #1a8a44, #0f602c);
}
.holdemBtnDanger {
background: #3a1717;
background: linear-gradient(180deg, #4a1f1f, #351515);
border-color: #6b2626;
color: #ffd6d6;
}
.holdemBtnDanger:hover {
background: #4a1c1c;
background: linear-gradient(180deg, #572525, #401818);
}
.holdemBtnGhost {
@@ -124,35 +140,57 @@ body {
position: absolute;
inset: 6%;
border-radius: 50%;
background: radial-gradient(ellipse at center, #1f6b3a 0%, #124a27 70%, #0d3a1e 100%);
border: 10px solid #4a2e1a;
box-shadow: inset 0 0 60px rgba(0, 0, 0, 0.5), 0 8px 24px rgba(0, 0, 0, 0.5);
background: radial-gradient(ellipse at 50% 42%, #227a45 0%, #175f37 45%, #0e4527 75%, #082c19 100%);
border: 14px solid #6b4225;
box-shadow:
inset 0 0 0 3px rgba(216, 180, 106, 0.55),
inset 0 0 80px rgba(0, 0, 0, 0.55),
0 0 0 4px #2c1a0d,
0 20px 50px rgba(0, 0, 0, 0.55);
display: flex;
align-items: center;
justify-content: center;
}
#tableWatermark {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.06);
font-size: 2.4rem;
font-weight: 700;
letter-spacing: 0.15em;
text-transform: uppercase;
text-align: center;
pointer-events: none;
user-select: none;
}
#potBadge {
background: rgba(0, 0, 0, 0.45);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
padding: 0.6rem 1.2rem;
position: relative;
background: radial-gradient(circle at 32% 28%, rgba(255, 255, 255, 0.08), rgba(0, 0, 0, 0.55));
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 999px;
padding: 0.7rem 1.7rem;
text-align: center;
display: flex;
flex-direction: column;
gap: 0.15rem;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.6), 0 4px 14px rgba(0, 0, 0, 0.45);
}
#potBadgeLabel {
font-size: 0.7rem;
letter-spacing: 0.1em;
font-size: 0.68rem;
letter-spacing: 0.14em;
text-transform: uppercase;
color: #bde9c9;
color: #d8b46a;
}
#potBadgeAmount {
font-size: 1.4rem;
font-weight: 600;
font-size: 1.5rem;
font-weight: 700;
color: #fff;
}
@@ -168,14 +206,15 @@ body {
transform: translate(-50%, -50%);
width: 96px;
min-height: 54px;
background: #232323;
border: 2px solid #3a3a3a;
border-radius: 10px;
background: linear-gradient(160deg, #2c2c2c, #1b1b1b);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 0.3rem 0.4rem;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.05);
cursor: grab;
z-index: 1;
text-align: center;
transition: box-shadow 0.15s ease, border-color 0.15s ease;
}
.seat.dragging {
@@ -183,36 +222,36 @@ body {
}
.seat.dropTarget {
outline: 2px dashed #09ff00;
outline: 2px dashed #d8b46a;
outline-offset: 2px;
}
.seat.seatInactive {
opacity: 0.45;
opacity: 0.4;
}
.seat.seatFolded {
border-style: dashed;
opacity: 0.6;
opacity: 0.55;
}
.seat.seatSelected {
border-color: #09ff00;
box-shadow: 0 0 0 2px rgba(9, 255, 0, 0.35), 0 4px 10px rgba(0, 0, 0, 0.4);
border-color: rgba(9, 255, 0, 0.6);
box-shadow: 0 0 0 2px rgba(9, 255, 0, 0.4), 0 4px 14px rgba(0, 0, 0, 0.5);
}
.seatChairNumber {
position: absolute;
top: 2px;
left: 5px;
font-size: 0.6rem;
left: 6px;
font-size: 0.58rem;
color: #777;
}
.seatPuckName {
font-size: 0.72rem;
font-weight: 600;
color: #eee;
color: #f0f0f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -220,17 +259,29 @@ body {
}
.seatPuckCards {
font-size: 0.65rem;
font-size: 0.68rem;
font-weight: 700;
font-family: var(--font-mono, monospace);
color: #bbb;
min-height: 0.9em;
color: #ddd;
min-height: 0.95em;
letter-spacing: 0.05em;
}
.seatPuckCards .cardRed {
color: #ff6b6b;
}
.seatPuckTotal {
font-size: 0.7rem;
display: inline-block;
margin-top: 0.15rem;
font-size: 0.64rem;
font-weight: 700;
color: #9be6a8;
min-height: 1em;
color: #a9f5b8;
background: rgba(9, 255, 0, 0.1);
border-radius: 999px;
padding: 0 0.35rem;
min-height: 1.1em;
line-height: 1.4;
}
.dealerChip {
@@ -240,19 +291,31 @@ body {
width: 22px;
height: 22px;
border-radius: 50%;
background: #444;
color: #ccc;
border: 2px solid #666;
background: radial-gradient(circle at 35% 30%, #fff, #d8d8d8 55%, #b8b8b8 100%);
color: #222;
border: 2px solid #999;
font-weight: 700;
font-size: 0.68rem;
cursor: pointer;
z-index: 2;
opacity: 0.32;
transition: opacity 0.15s ease;
}
.seat:hover .dealerChip,
.dealerChip:focus-visible {
opacity: 1;
}
.dealerChip.active {
background: #f5d547;
border-color: #cba428;
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
border-color: #a9782f;
color: #3a2e00;
opacity: 1;
}
.dealerChip.hidden {
display: none;
}
.blindBadge {
@@ -261,9 +324,10 @@ body {
top: -9px;
font-size: 0.58rem;
font-weight: 700;
padding: 0.05rem 0.3rem;
border-radius: 8px;
padding: 0.05rem 0.34rem;
border-radius: 999px;
z-index: 2;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.4);
}
.blindBadge.visible {
@@ -272,13 +336,13 @@ body {
.sbBadge {
left: -6px;
background: #2b6cb0;
color: #dbeafe;
background: linear-gradient(180deg, #3f82d1, #2b5fa0);
color: #eaf3ff;
}
.bbBadge {
left: 22px;
background: #b7791f;
background: linear-gradient(180deg, #dba13f, #b7791f);
color: #fff7e6;
}
@@ -290,9 +354,11 @@ body {
.seatEditorPanel h2 {
margin: 0 0 0.6rem;
padding-bottom: 0.5rem;
font-size: 0.95rem;
font-weight: 500;
color: #f5f5f5;
border-bottom: 1px solid rgba(216, 180, 106, 0.35);
}
.seatEditorForm {
@@ -340,10 +406,15 @@ body {
background: #fdfdfd;
color: #111;
border: 1px solid #3a3a3a;
border-radius: 5px;
border-radius: 6px;
padding: 0.35rem 0;
font-weight: 700;
font-family: var(--font-mono, monospace);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
}
.cardInput.cardRed {
color: #c0392b;
}
.seatContribution {
@@ -444,10 +515,11 @@ body {
}
.sidebarPanel {
background: #1e1e1e;
background: linear-gradient(160deg, #212121, #1a1a1a);
border: 1px solid #333;
border-radius: 8px;
border-radius: 10px;
padding: 0.2rem 0.9rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.sidebarPanel summary {
@@ -26,6 +26,13 @@ document.addEventListener("DOMContentLoaded", () => {
Render.renderAll();
});
document.getElementById("dedicatedDealerToggle").addEventListener("change", e => {
state.dedicatedDealer = e.target.checked;
hooks.save(state);
Render.renderSeats();
Render.renderTableHint();
});
document.getElementById("tableNotes").addEventListener("input", e => {
state.notes = e.target.value;
hooks.save(state);
@@ -29,12 +29,21 @@ const Render = {
this.renderNotes();
this.updatePotBadge();
this.renderSetupFields();
this.renderTableHint();
},
renderSetupFields() {
document.getElementById("seatCountInput").value = this.state.seatCount;
document.getElementById("directionSelect").value = this.state.direction;
document.getElementById("basePotInput").value = this.state.basePotAmount ?? "";
document.getElementById("dedicatedDealerToggle").checked = !!this.state.dedicatedDealer;
},
renderTableHint() {
const hint = document.getElementById("tableHint");
hint.textContent = this.state.dedicatedDealer
? "Drag a seat onto another seat to swap players."
: "Drag a seat onto another seat to swap players. Click the dealer chip on a seat to make it the button.";
},
renderNotes() {
@@ -80,6 +89,7 @@ const Render = {
const dealerChip = node.querySelector(".dealerChip");
dealerChip.classList.toggle("active", index === dealerIndex);
dealerChip.classList.toggle("hidden", !!this.state.dedicatedDealer);
dealerChip.addEventListener("click", e => {
e.stopPropagation();
this.state.dealerSeatIndex = index;
@@ -105,10 +115,21 @@ const Render = {
});
},
isRedSuit(card) {
return /[HD]$/i.test(card || "");
},
updatePuckContent(node, player, index) {
node.querySelector(".seatPuckName").textContent = player.name && player.name.trim() ? player.name.trim() : "";
const cards = (player.cards || []).filter(Boolean).join(" ");
node.querySelector(".seatPuckCards").textContent = cards;
const cardsEl = node.querySelector(".seatPuckCards");
cardsEl.innerHTML = "";
(player.cards || []).filter(Boolean).forEach((card, i) => {
if (i > 0) cardsEl.appendChild(document.createTextNode(" "));
const span = document.createElement("span");
if (this.isRedSuit(card)) span.className = "cardRed";
span.textContent = card;
cardsEl.appendChild(span);
});
const total = HoldemState.computeContribution(player, this.state.chipLegend, this.state.basePotAmount);
node.querySelector(".seatPuckTotal").textContent = total > 0 ? Money.format(total) : "";
node.classList.toggle("seatInactive", !player.inHand);
@@ -222,8 +243,10 @@ const Render = {
card1.value = player.cards[0] || "";
card2.value = player.cards[1] || "";
[card1, card2].forEach((input, cardIdx) => {
input.classList.toggle("cardRed", this.isRedSuit(input.value));
input.addEventListener("input", () => {
input.value = input.value.toUpperCase();
input.classList.toggle("cardRed", this.isRedSuit(input.value));
player.cards[cardIdx] = input.value;
this.commitSeatChange(form, player, index);
});
@@ -36,6 +36,7 @@ const HoldemState = {
direction: "clockwise",
basePotAmount: null,
dealerSeatIndex: 0,
dedicatedDealer: false,
chipLegend: this.DEFAULT_CHIP_LEGEND.map(c => ({ ...c })),
seats: Array.from({ length: seatCount }, () => this.makePlayer()),
notes: "",