Merge pull request 'Tapping a seat gives you that seat' (#60) from feature/casino-holdem-seat-editor into fix/casino-holdem-less-guessing

Reviewed-on: #60
This commit was merged in pull request #60.
This commit is contained in:
2026-09-01 10:57:10 -06:00
2 changed files with 89 additions and 9 deletions
+41 -8
View File
@@ -568,8 +568,19 @@ button,
.dealerChip { .dealerChip {
position: absolute; position: absolute;
top: -10px; /* Inside its own seat, not on the rim.
right: -10px;
It used to sit at -10px/-10px, hanging over the seat's corner — which on an oval
of overlapping seats means hanging over the seat *next to it*. Measured on a
nine-seat table: tapping the centre of seat 1 gave the button to seat 9. That
was always slightly true and the enlarged hit area made it much worse; three of
nine seats were losing taps, one of them losing its own middle.
Tucked inside, the overlap is zero at every size. It reads as a puck in the
corner of the seat rather than on its edge, which is a small price for tapping
a player and getting that player. */
top: 3px;
right: 3px;
width: 22px; width: 22px;
height: 22px; height: 22px;
border-radius: 50%; border-radius: 50%;
@@ -595,10 +606,20 @@ button,
.dealerChip::after { .dealerChip::after {
content: ""; content: "";
position: absolute; position: absolute;
top: -9px; /* Grows inward, over its own seat, not outward onto the neighbours.
right: -7px;
bottom: -15px; The chip already sits outside its seat by design — top:-10px, right:-10px — so
left: -14px; a pad that also reaches up and right lands on whatever seat is next around the
oval. It did: three of nine seats had taps taken by a neighbour's chip, seat 1
losing 10 of 49 sampled points including its own centre, which meant tapping
the middle of seat 1 gave the button to seat 9.
Down and left is its own seat, which is a large target that can spare the
corner. The two pixels of outward reach are just to cover the chip's border. */
top: -3px;
right: -3px;
bottom: -18px;
left: -18px;
} }
.dealerChip:focus-visible { .dealerChip:focus-visible {
@@ -2787,9 +2808,20 @@ body.hasActionBar #toastHost {
against the containing block's width, not the element's own, so narrowing the against the containing block's width, not the element's own, so narrowing the
wrap on its own left it full height regardless. */ wrap on its own left it full height regardless. */
#pokerTableWrap { #pokerTableWrap {
--feltRoom: calc(100vh - var(--actionBarHeight, 0px) - 8.5rem); /* A wide ellipse, which is what a real poker table is.
width: min(100%, calc(var(--feltRoom) / 0.72));
Capping the width by the height to hold the portrait aspect ratio was
throwing away most of the screen: at 640x360 the felt came out 135px wide
inside a 601px column — 22% of the width — leaving 366px of rim for nine
seats that need 662px. Adjacent seat centres ended up 13px apart, which is
why a seat's centre belonged to its neighbour.
Landscape has width and no height, so the table takes the width it has and
is only as tall as the room left over. */
--feltRoom: calc(100vh - var(--actionBarHeight, 0px) - 7rem);
width: 100%;
height: var(--feltRoom); height: var(--feltRoom);
min-height: 7rem;
padding-bottom: 0; padding-bottom: 0;
} }
@@ -2826,6 +2858,7 @@ body.hasActionBar #toastHost {
width: 62px; width: 62px;
min-height: 40px; min-height: 40px;
} }
} }
/* ---------- Short phones, upright ---------- /* ---------- Short phones, upright ----------
+48 -1
View File
@@ -133,7 +133,18 @@ const LayoutAudit = {
if (el.contains(top) || top === el || top.contains(el)) return; if (el.contains(top) || top === el || top.contains(el)) return;
// Seats ring a round table and overlap each other at the rim by // Seats ring a round table and overlap each other at the rim by
// design; that is crowding, not an overlay hiding a control. // design; that is crowding, not an overlay hiding a control.
if (top.closest && top.closest(".seat")) return; //
// But only the seat's *body* is harmless. A neighbouring seat's own
// controls sitting on top of this one is a control being stolen, and
// exempting the whole seat subtree hid exactly that: an enlarged
// dealer-chip hit area was taking the centre of the seat three
// places round the table, and this check called it crowding and
// moved on.
const neighbourSeat = top.closest && top.closest(".seat");
if (neighbourSeat) {
const isControl = top.matches && top.matches("button, a, input, select, textarea, [role='button']");
if (!isControl) return;
}
blocked++; blocked++;
if (!blockedBy) blockedBy = this.describe(top); if (!blockedBy) blockedBy = this.describe(top);
}); });
@@ -468,6 +479,41 @@ const LayoutAudit = {
return { name: `touch targets at least ${minimum}px`, offenders }; return { name: `touch targets at least ${minimum}px`, offenders };
}, },
// Tapping the middle of a seat must give you that seat.
//
// Added after a dealer-chip hit area, meant to make a 22px puck thumb-sized, grew
// outward from a chip that already hung over its seat's corner — and therefore
// over the seat next to it. On a nine-seat table, tapping the centre of seat 1
// handed the button to seat 9. The occlusion check missed it because it treated
// anything inside a `.seat` as harmless crowding.
//
// Reported separately from occlusion because the cause is different: not an
// overlay on top of the table, but two pieces of the table fighting over a tap.
seatOwnership() {
const seats = [...document.querySelectorAll(".seat")];
const offenders = [];
seats.forEach((seat, index) => {
const rect = seat.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) return;
const x = rect.left + rect.width / 2;
const y = rect.top + rect.height / 2;
if (x < 0 || y < 0 || x >= innerWidth || y >= innerHeight) return;
const hit = document.elementFromPoint(x, y);
if (!hit || seat.contains(hit) || hit === seat) return;
const other = hit.closest(".seat");
const who = other ? `seat ${seats.indexOf(other) + 1}` : this.describe(hit);
const control = hit.matches && hit.matches("button, a, input, select, [role='button']");
offenders.push(
`seat ${index + 1}'s centre goes to ${who}${control ? "'s " + this.describe(hit) : ""}`
);
});
return { name: "a seat's centre belongs to that seat", offenders };
},
// ---- Runner ---- // ---- Runner ----
runAll(options = {}) { runAll(options = {}) {
@@ -480,6 +526,7 @@ const LayoutAudit = {
this.textSpill(), this.textSpill(),
this.horizontalOverflow(), this.horizontalOverflow(),
this.fieldFontSize(), this.fieldFontSize(),
this.seatOwnership(),
]; ];
if (options.includeTouchTargets) checks.push(this.touchTargets(options.minimumTarget)); if (options.includeTouchTargets) checks.push(this.touchTargets(options.minimumTarget));