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
+178 -1
View File
@@ -6,6 +6,148 @@ body {
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 {
max-width: 1400px;
margin: 0 auto;
@@ -85,11 +227,13 @@ body {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.05);
}
@media (hover: hover) {
.holdemBtn:hover {
background: linear-gradient(180deg, #383838, #2a2a2a);
border-color: #4a4a4a;
transform: translateY(-1px);
}
}
.holdemBtnAccent {
background: linear-gradient(180deg, #16783a, #0d5225);
@@ -97,9 +241,11 @@ body {
color: #ecffef;
}
@media (hover: hover) {
.holdemBtnAccent:hover {
background: linear-gradient(180deg, #1a8a44, #0f602c);
}
}
.holdemBtnDanger {
background: linear-gradient(180deg, #4a1f1f, #351515);
@@ -107,9 +253,11 @@ body {
color: #ffd6d6;
}
@media (hover: hover) {
.holdemBtnDanger:hover {
background: linear-gradient(180deg, #572525, #401818);
}
}
.holdemBtnGhost {
background: transparent;
@@ -430,11 +578,16 @@ body {
transition: opacity 0.15s ease;
}
.seat:hover .dealerChip,
.dealerChip:focus-visible {
opacity: 1;
}
@media (hover: hover) {
.seat:hover .dealerChip {
opacity: 1;
}
}
.dealerChip.active {
background: radial-gradient(circle at 35% 30%, #ffe9a8, #f5d547 55%, #cba428 100%);
border-color: #a9782f;
@@ -567,10 +720,12 @@ body {
transition: transform 0.12s ease, box-shadow 0.12s ease;
}
@media (hover: hover) {
.cardSlot:hover {
transform: translateY(-1px);
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.4);
}
}
.cardSlot.cardRed {
color: #c0392b;
@@ -743,9 +898,11 @@ body {
cursor: pointer;
}
@media (hover: hover) {
.linkBtn:hover {
color: #d8b46a;
}
}
#rulingsOverlay {
position: fixed;
@@ -909,10 +1066,12 @@ body {
transition: all 0.12s ease;
}
@media (hover: hover) {
.seatMenuBtn:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.seatMenuBtn.menuDanger {
background: linear-gradient(180deg, #3d1f1f, #2c1515);
@@ -1037,9 +1196,11 @@ body {
transition: transform 0.1s ease;
}
@media (hover: hover) {
.pickerCard:hover:not(:disabled) {
transform: translateY(-1px);
}
}
.pickerCard.cardRed {
color: #c0392b;
@@ -1217,9 +1378,11 @@ body {
font-size: 0.78rem;
}
@media (hover: hover) {
.subPanel summary:hover {
color: #c5c5c5;
}
}
.subPanelBody {
display: flex;
@@ -1395,6 +1558,10 @@ body {
padding: 0.5rem;
resize: vertical;
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 ---------- */
@@ -1456,11 +1623,13 @@ body.hasActionBar #holdemSidebar {
white-space: nowrap;
}
@media (hover: hover) {
.actionUndo:hover {
background: #333;
border-color: #4f4f4f;
color: #eee;
}
}
.actionStreet {
color: #d8b46a;
@@ -1600,10 +1769,12 @@ body.hasActionBar #holdemSidebar {
transition: all 0.12s ease;
}
@media (hover: hover) {
.actionBtn:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.actionBtn:active {
transform: translateY(1px);
@@ -1831,9 +2002,11 @@ body.hasActionBar #toastHost {
cursor: pointer;
}
@media (hover: hover) {
.toastAction:hover {
background: rgba(216, 180, 106, 0.22);
}
}
.toastClose {
flex: none;
@@ -1848,10 +2021,12 @@ body.hasActionBar #toastHost {
cursor: pointer;
}
@media (hover: hover) {
.toastClose:hover {
background: #333;
color: #ddd;
}
}
/* ---------- Confirmation dialog ---------- */
@@ -1985,10 +2160,12 @@ body.hasActionBar #toastHost {
transition: border-color 0.12s ease, background 0.12s ease;
}
@media (hover: hover) {
.potWizardChoice:hover {
border-color: #555;
background: #191919;
}
}
.potWizardChoice input {
flex: none;
@@ -9,6 +9,58 @@ body {
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 {
max-width: 480px;
margin: 0 auto;
@@ -336,10 +388,12 @@ body {
cursor: pointer;
}
@media (hover: hover) {
.clearPotBtn:hover {
background: #333;
color: #ddd;
}
}
/* ---------- Tap targets ---------- */
@@ -364,10 +418,12 @@ body {
transition: all 0.15s ease;
}
@media (hover: hover) {
.choiceChip:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.choiceChip.selected {
background: linear-gradient(180deg, #3a2f18, #241d0f);
@@ -401,9 +457,11 @@ body {
cursor: pointer;
}
@media (hover: hover) {
.stepBtn:hover {
background: #333;
}
}
.waysReadout {
flex: 1;
@@ -439,10 +497,12 @@ body {
transition: all 0.15s ease;
}
@media (hover: hover) {
.splitBtn:hover {
border-color: #4a4a4a;
background: linear-gradient(180deg, #383838, #2a2a2a);
}
}
.splitBtnGhost {
background: transparent;