Merge pull request 'Make every tap in the dealer land instantly on a phone' (#44) from feature/casino-holdem-touch-mobile into master
Build and Deploy / build-deploy (push) Canceled after 10s

This commit was merged in pull request #44.
This commit is contained in:
2026-09-01 09:29:53 -06:00
2 changed files with 284 additions and 47 deletions
+191 -14
View File
@@ -6,6 +6,148 @@ body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
/* ---------- Touch ----------
A dealer runs this one-handed on a phone with players waiting, so a tap has to
register the instant their thumb lands.
`touch-action: manipulation` is the whole game here. Without it a mobile browser
holds every tap for ~300ms to see whether a second one is coming that would mean
"zoom in" — on top of which double-tapping a seat, which this tool uses for fold,
*is* the browser's zoom gesture, so the dealer's shortcut fights the page zooming
under their thumb. `manipulation` keeps panning and pinch-zoom and drops only the
double-tap-to-zoom, which is exactly the one we don't want.
The tap highlight goes with it: every control below gets a real pressed state, and
the browser's translucent grey rectangle on top of a designed one just looks like
a rendering artefact. Text selection goes too — a fast tap-and-hold on a seat
should open the seat menu, not raise a text-selection magnifier over the felt. */
.holdemBtn,
.seat,
.dealerChip,
.cardSlot,
.linkBtn,
.seatMenuBtn,
.seatSheetClose,
.pickerCard,
.actionBtn,
.raiseQuickBtn,
.actionUndo,
.toastAction,
.toastClose,
.potWizardChoice,
.subPanel summary,
button,
[role="button"],
input,
select,
textarea,
a {
touch-action: manipulation;
}
/* The grey rectangle goes from every control, fields included — a tapped input
already shows a caret and a focus ring, which say the same thing more quietly. */
.holdemBtn,
.seat,
.dealerChip,
.cardSlot,
.linkBtn,
.seatMenuBtn,
.seatSheetClose,
.pickerCard,
.actionBtn,
.raiseQuickBtn,
.actionUndo,
.toastAction,
.toastClose,
.potWizardChoice,
.subPanel summary,
button,
[role="button"],
input,
select,
textarea,
a {
-webkit-tap-highlight-color: transparent;
}
/* Text selection is switched off only on the things you press, never on the fields
you type into — a dealer editing a player's name still needs to select the text. */
.holdemBtn,
.seat,
.dealerChip,
.cardSlot,
.linkBtn,
.seatMenuBtn,
.seatSheetClose,
.pickerCard,
.actionBtn,
.raiseQuickBtn,
.actionUndo,
.toastAction,
.toastClose,
.potWizardChoice,
.subPanel summary,
button,
[role="button"] {
-webkit-user-select: none;
user-select: none;
}
/* ---------- Pressed states ----------
Touch has no hover, so the press is the only feedback a dealer gets that the tap
landed — and with the grey highlight suppressed above, these have to carry it.
Everything presses the same way it would on a real table: down and slightly
darker, no travel time. */
.holdemBtn:active:not(:disabled),
.linkBtn:active:not(:disabled),
.seatMenuBtn:active:not(:disabled),
.seatSheetClose:active:not(:disabled),
.toastAction:active,
.toastClose:active,
.potWizardChoice:active,
.raiseQuickBtn:active:not(:disabled),
.actionUndo:active:not(:disabled) {
transform: translateY(1px);
filter: brightness(1.25);
transition-duration: 0.04s;
}
.cardSlot:active,
.pickerCard:active:not(:disabled),
.dealerChip:active {
transform: translateY(1px) scale(0.97);
transition-duration: 0.04s;
}
/* The action bar already presses down; the brightness is what it was missing, and
it's the one place a dealer taps without looking. */
.actionBtn:active:not(:disabled) {
filter: brightness(1.3);
transition-duration: 0.04s;
}
/* Seats deliberately have no `:active` rule. They already dip via `.seatPressing`,
which `pointerdown` adds in render.js — that fires earlier than `:active` resolves,
and, more importantly, a seat's `transform` is what positions it around the felt
(`translate(-50%, -50%)` plus its own left/top). A press state setting `transform`
would drop that translate and throw every pressed seat about 38px off its spot. */
/* Nobody should get a stuck press state from a tap that turned into a scroll. */
@media (prefers-reduced-motion: reduce) {
.holdemBtn:active:not(:disabled),
.linkBtn:active:not(:disabled),
.seatMenuBtn:active:not(:disabled),
.seatSheetClose:active:not(:disabled),
.cardSlot:active,
.pickerCard:active:not(:disabled),
.dealerChip:active,
.raiseQuickBtn:active:not(:disabled),
.actionUndo:active:not(:disabled) {
transform: none;
}
}
#holdemApp { #holdemApp {
max-width: 1400px; max-width: 1400px;
margin: 0 auto; margin: 0 auto;
@@ -85,10 +227,12 @@ body {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
} }
.holdemBtn:hover { @media (hover: hover) {
.holdemBtn:hover {
background: linear-gradient(180deg, #383838, #2a2a2a); background: linear-gradient(180deg, #383838, #2a2a2a);
border-color: #4a4a4a; border-color: #4a4a4a;
transform: translateY(-1px); transform: translateY(-1px);
}
} }
.holdemBtnAccent { .holdemBtnAccent {
@@ -97,8 +241,10 @@ body {
color: #ecffef; color: #ecffef;
} }
.holdemBtnAccent:hover { @media (hover: hover) {
.holdemBtnAccent:hover {
background: linear-gradient(180deg, #1a8a44, #0f602c); background: linear-gradient(180deg, #1a8a44, #0f602c);
}
} }
.holdemBtnDanger { .holdemBtnDanger {
@@ -107,8 +253,10 @@ body {
color: #ffd6d6; color: #ffd6d6;
} }
.holdemBtnDanger:hover { @media (hover: hover) {
.holdemBtnDanger:hover {
background: linear-gradient(180deg, #572525, #401818); background: linear-gradient(180deg, #572525, #401818);
}
} }
.holdemBtnGhost { .holdemBtnGhost {
@@ -430,11 +578,16 @@ body {
transition: opacity 0.15s ease; transition: opacity 0.15s ease;
} }
.seat:hover .dealerChip,
.dealerChip:focus-visible { .dealerChip:focus-visible {
opacity: 1; opacity: 1;
} }
@media (hover: hover) {
.seat:hover .dealerChip {
opacity: 1;
}
}
.dealerChip.active { .dealerChip.active {
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%); background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
border-color: #a9782f; border-color: #a9782f;
@@ -567,9 +720,11 @@ body {
transition: transform 0.12s ease, box-shadow 0.12s ease; transition: transform 0.12s ease, box-shadow 0.12s ease;
} }
.cardSlot:hover { @media (hover: hover) {
.cardSlot:hover {
transform: translateY(-1px); transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4); box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
} }
.cardSlot.cardRed { .cardSlot.cardRed {
@@ -743,8 +898,10 @@ body {
cursor: pointer; cursor: pointer;
} }
.linkBtn:hover { @media (hover: hover) {
.linkBtn:hover {
color: #d8b46a; color: #d8b46a;
}
} }
#rulingsOverlay { #rulingsOverlay {
@@ -909,9 +1066,11 @@ body {
transition: all 0.12s ease; transition: all 0.12s ease;
} }
.seatMenuBtn:hover { @media (hover: hover) {
.seatMenuBtn:hover {
border-color: #4a4a4a; border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a); background: linear-gradient(180deg, #383838, #2a2a2a);
}
} }
.seatMenuBtn.menuDanger { .seatMenuBtn.menuDanger {
@@ -1037,8 +1196,10 @@ body {
transition: transform 0.1s ease; transition: transform 0.1s ease;
} }
.pickerCard:hover:not(:disabled) { @media (hover: hover) {
.pickerCard:hover:not(:disabled) {
transform: translateY(-1px); transform: translateY(-1px);
}
} }
.pickerCard.cardRed { .pickerCard.cardRed {
@@ -1217,8 +1378,10 @@ body {
font-size: 0.78rem; font-size: 0.78rem;
} }
.subPanel summary:hover { @media (hover: hover) {
.subPanel summary:hover {
color: #c5c5c5; color: #c5c5c5;
}
} }
.subPanelBody { .subPanelBody {
@@ -1395,6 +1558,10 @@ body {
padding: 0.5rem; padding: 0.5rem;
resize: vertical; resize: vertical;
font-family: inherit; font-family: inherit;
/* iOS zooms the whole page in when you focus a field under 16px, and the dealer
is then pinching back out to see the table. Every other input here is already
at 16px; this one was the straggler. */
font-size: 16px;
} }
/* ---------- Pot wizard ---------- */ /* ---------- Pot wizard ---------- */
@@ -1456,10 +1623,12 @@ body.hasActionBar #holdemSidebar {
white-space: nowrap; white-space: nowrap;
} }
.actionUndo:hover { @media (hover: hover) {
.actionUndo:hover {
background: #333; background: #333;
border-color: #4f4f4f; border-color: #4f4f4f;
color: #eee; color: #eee;
}
} }
.actionStreet { .actionStreet {
@@ -1600,9 +1769,11 @@ body.hasActionBar #holdemSidebar {
transition: all 0.12s ease; transition: all 0.12s ease;
} }
.actionBtn:hover { @media (hover: hover) {
.actionBtn:hover {
border-color: #4a4a4a; border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a); background: linear-gradient(180deg, #383838, #2a2a2a);
}
} }
.actionBtn:active { .actionBtn:active {
@@ -1831,8 +2002,10 @@ body.hasActionBar #toastHost {
cursor: pointer; cursor: pointer;
} }
.toastAction:hover { @media (hover: hover) {
.toastAction:hover {
background: rgba(216, 180, 106, 0.22); background: rgba(216, 180, 106, 0.22);
}
} }
.toastClose { .toastClose {
@@ -1848,9 +2021,11 @@ body.hasActionBar #toastHost {
cursor: pointer; cursor: pointer;
} }
.toastClose:hover { @media (hover: hover) {
.toastClose:hover {
background: #333; background: #333;
color: #ddd; color: #ddd;
}
} }
/* ---------- Confirmation dialog ---------- */ /* ---------- Confirmation dialog ---------- */
@@ -1985,9 +2160,11 @@ body.hasActionBar #toastHost {
transition: border-color 0.12s ease, background 0.12s ease; transition: border-color 0.12s ease, background 0.12s ease;
} }
.potWizardChoice:hover { @media (hover: hover) {
.potWizardChoice:hover {
border-color: #555; border-color: #555;
background: #191919; background: #191919;
}
} }
.potWizardChoice input { .potWizardChoice input {
@@ -9,6 +9,58 @@ body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
} }
/* Same touch handling as the dealer table — see the note there. This page is the one
a dealer opens mid-hand to chop a pot, so the ~300ms the browser spends deciding
whether a tap was a double-tap-to-zoom is the last thing it needs. */
button,
[role="button"],
input,
select,
textarea,
a {
touch-action: manipulation;
}
button,
[role="button"],
input,
select,
textarea,
a {
-webkit-tap-highlight-color: transparent;
}
/* Not on the fields — a dealer typing a pot amount still needs to select it. */
button,
[role="button"] {
-webkit-user-select: none;
user-select: none;
}
.clearPotBtn:active:not(:disabled),
.choiceChip:active:not(:disabled),
.stepBtn:active:not(:disabled),
.splitBtn:active:not(:disabled),
.splitBtnGhost:active:not(:disabled),
.unevenFoldBtn:active:not(:disabled),
.unevenRemoveBtn:active:not(:disabled) {
transform: translateY(1px);
filter: brightness(1.25);
transition-duration: 0.04s;
}
@media (prefers-reduced-motion: reduce) {
.clearPotBtn:active:not(:disabled),
.choiceChip:active:not(:disabled),
.stepBtn:active:not(:disabled),
.splitBtn:active:not(:disabled),
.splitBtnGhost:active:not(:disabled),
.unevenFoldBtn:active:not(:disabled),
.unevenRemoveBtn:active:not(:disabled) {
transform: none;
}
}
#splitApp { #splitApp {
max-width: 480px; max-width: 480px;
margin: 0 auto; margin: 0 auto;
@@ -336,9 +388,11 @@ body {
cursor: pointer; cursor: pointer;
} }
.clearPotBtn:hover { @media (hover: hover) {
.clearPotBtn:hover {
background: #333; background: #333;
color: #ddd; color: #ddd;
}
} }
/* ---------- Tap targets ---------- */ /* ---------- Tap targets ---------- */
@@ -364,9 +418,11 @@ body {
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.choiceChip:hover { @media (hover: hover) {
.choiceChip:hover {
border-color: #4a4a4a; border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a); background: linear-gradient(180deg, #383838, #2a2a2a);
}
} }
.choiceChip.selected { .choiceChip.selected {
@@ -401,8 +457,10 @@ body {
cursor: pointer; cursor: pointer;
} }
.stepBtn:hover { @media (hover: hover) {
.stepBtn:hover {
background: #333; background: #333;
}
} }
.waysReadout { .waysReadout {
@@ -439,9 +497,11 @@ body {
transition: all 0.15s ease; transition: all 0.15s ease;
} }
.splitBtn:hover { @media (hover: hover) {
.splitBtn:hover {
border-color: #4a4a4a; border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a); background: linear-gradient(180deg, #383838, #2a2a2a);
}
} }
.splitBtnGhost { .splitBtnGhost {