@stargazeryan/poker-ui 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -22,22 +22,62 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  BB: () => BB,
25
+ COUNT_UP_MS: () => COUNT_UP_MS,
25
26
  CardBack: () => CardBack,
26
27
  CardTable: () => CardTable,
28
+ Chip: () => Chip,
29
+ ChipStack: () => ChipStack,
27
30
  CommunityCards: () => CommunityCards,
31
+ DEAL_STAGGER_MS: () => DEAL_STAGGER_MS,
32
+ DEFAULT_BET_SCALE: () => DEFAULT_BET_SCALE,
33
+ DEFAULT_DENOMINATIONS: () => DEFAULT_DENOMINATIONS,
34
+ DEFAULT_FELT_GAP: () => DEFAULT_FELT_GAP,
35
+ DEFAULT_MARGIN_X: () => DEFAULT_MARGIN_X,
36
+ DEFAULT_MARGIN_Y: () => DEFAULT_MARGIN_Y,
37
+ DEFAULT_SEAT_GAP: () => DEFAULT_SEAT_GAP,
38
+ DEFAULT_SEAT_H: () => DEFAULT_SEAT_H,
39
+ DEFAULT_SEAT_W: () => DEFAULT_SEAT_W,
40
+ DESIGN_H: () => DESIGN_H,
41
+ DESIGN_W: () => DESIGN_W,
42
+ FLIP_STAGGER_MS: () => FLIP_STAGGER_MS,
43
+ GLOW_GAP: () => GLOW_GAP,
28
44
  HoleCards: () => HoleCards,
29
45
  PlayingCard: () => PlayingCard,
30
46
  PokerTable: () => CardTable,
31
47
  Pot: () => Pot,
48
+ RIM_GAP: () => RIM_GAP,
49
+ SEAT_SHRINK_FLOOR: () => SEAT_SHRINK_FLOOR,
50
+ SIZES: () => SIZES,
32
51
  Seat: () => Seat,
52
+ chipBreakdown: () => chipBreakdown,
53
+ chipCount: () => chipCount,
54
+ chipSpotColor: () => chipSpotColor,
55
+ chipsTotal: () => chipsTotal,
33
56
  cn: () => cn,
57
+ contrastRatio: () => contrastRatio,
58
+ describeCard: () => describeCard,
59
+ ellipseBox: () => ellipseBox,
34
60
  ellipsePoint: () => ellipsePoint,
61
+ fitSeatScale: () => fitSeatScale,
62
+ flatten: () => flatten,
35
63
  formatBB: () => formatBB,
64
+ getRankName: () => getRankName,
65
+ getSuitName: () => getSuitName,
36
66
  getSuitSymbol: () => getSuitSymbol,
67
+ isKnownSuit: () => isKnownSuit,
68
+ isRedSuit: () => isRedSuit,
69
+ layoutSeats: () => layoutSeats,
70
+ minSeatSeparation: () => minSeatSeparation,
71
+ mostVisible: () => mostVisible,
37
72
  nextSeatIndex: () => nextSeatIndex,
38
73
  parseCard: () => parseCard,
74
+ parseColor: () => parseColor,
75
+ relativeLuminance: () => relativeLuminance,
39
76
  ringPoints: () => ringPoints,
40
- seatAngle: () => seatAngle
77
+ seatAngle: () => seatAngle,
78
+ seatPoint: () => seatPoint,
79
+ seatRingRadii: () => seatRingRadii,
80
+ tableRadii: () => tableRadii
41
81
  });
42
82
  module.exports = __toCommonJS(index_exports);
43
83
 
@@ -49,17 +89,147 @@ function cn(...parts) {
49
89
 
50
90
  // src/format.ts
51
91
  var BB = 20;
52
- function formatBB(amount) {
53
- const bb = amount / BB;
54
- return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;
92
+ function formatBB(amount, bb = BB) {
93
+ const value = amount / bb;
94
+ return `${Number.isInteger(value) ? value : value.toFixed(1)} BB`;
95
+ }
96
+
97
+ // src/color.ts
98
+ function parseColor(color) {
99
+ const input = color.trim();
100
+ const hex = input.match(/^#([0-9a-f]{3,8})$/i);
101
+ if (hex) {
102
+ const raw = hex[1];
103
+ if (raw.length === 3 || raw.length === 4) {
104
+ const [r, g, b, a] = raw.split("").map((c) => parseInt(c + c, 16));
105
+ return { r, g, b, a: raw.length === 4 ? a / 255 : 1 };
106
+ }
107
+ if (raw.length === 6 || raw.length === 8) {
108
+ return {
109
+ r: parseInt(raw.slice(0, 2), 16),
110
+ g: parseInt(raw.slice(2, 4), 16),
111
+ b: parseInt(raw.slice(4, 6), 16),
112
+ a: raw.length === 8 ? parseInt(raw.slice(6, 8), 16) / 255 : 1
113
+ };
114
+ }
115
+ return null;
116
+ }
117
+ const fn = input.match(/^rgba?\(([^)]+)\)$/i);
118
+ if (!fn) return null;
119
+ const parts = fn[1].split(/[,\s/]+/).filter(Boolean).map(Number);
120
+ if (parts.length < 3 || parts.slice(0, 3).some((n) => !Number.isFinite(n))) return null;
121
+ return { r: parts[0], g: parts[1], b: parts[2], a: Number.isFinite(parts[3]) ? parts[3] : 1 };
122
+ }
123
+ function relativeLuminance(color) {
124
+ const parsed = parseColor(color);
125
+ if (!parsed) return null;
126
+ const channel = (c) => {
127
+ const s = Math.min(255, Math.max(0, c)) / 255;
128
+ return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
129
+ };
130
+ return 0.2126 * channel(parsed.r) + 0.7152 * channel(parsed.g) + 0.0722 * channel(parsed.b);
131
+ }
132
+ function flatten(foreground, background) {
133
+ const fg = parseColor(foreground);
134
+ const bg = parseColor(background);
135
+ if (!fg || !bg) return null;
136
+ const mix = (f, b) => fg.a * f + (1 - fg.a) * b;
137
+ return { r: mix(fg.r, bg.r), g: mix(fg.g, bg.g), b: mix(fg.b, bg.b), a: 1 };
138
+ }
139
+ function contrastRatio(foreground, background) {
140
+ const composited = flatten(foreground, background);
141
+ if (!composited) return null;
142
+ const a = relativeLuminance(`rgb(${composited.r}, ${composited.g}, ${composited.b})`);
143
+ const b = relativeLuminance(background);
144
+ if (a === null || b === null) return null;
145
+ const [hi, lo] = a > b ? [a, b] : [b, a];
146
+ return (hi + 0.05) / (lo + 0.05);
147
+ }
148
+ function mostVisible(candidates, background) {
149
+ let best = candidates[0];
150
+ let bestRatio = -1;
151
+ for (const candidate of candidates) {
152
+ const ratio = contrastRatio(candidate, background);
153
+ if (ratio !== null && ratio > bestRatio) {
154
+ best = candidate;
155
+ bestRatio = ratio;
156
+ }
157
+ }
158
+ return best;
159
+ }
160
+
161
+ // src/sizes.ts
162
+ var SIZES = ["xs", "sm", "md", "lg"];
163
+
164
+ // src/motion.ts
165
+ var COUNT_UP_MS = 500;
166
+ var DEAL_STAGGER_MS = 100;
167
+ var FLIP_STAGGER_MS = 70;
168
+
169
+ // src/cards.ts
170
+ var RANK_NAMES = {
171
+ A: "Ace",
172
+ K: "King",
173
+ Q: "Queen",
174
+ J: "Jack",
175
+ "10": "Ten",
176
+ "9": "Nine",
177
+ "8": "Eight",
178
+ "7": "Seven",
179
+ "6": "Six",
180
+ "5": "Five",
181
+ "4": "Four",
182
+ "3": "Three",
183
+ "2": "Two"
184
+ };
185
+ var SUIT_NAMES = {
186
+ s: "spades",
187
+ h: "hearts",
188
+ d: "diamonds",
189
+ c: "clubs"
190
+ };
191
+ var SUIT_SYMBOLS = {
192
+ s: "\u2660",
193
+ h: "\u2665",
194
+ d: "\u2666",
195
+ c: "\u2663"
196
+ };
197
+ function parseCard(card) {
198
+ if (!card || card.length < 2) return { rank: "", suit: "" };
199
+ return { rank: card.slice(0, -1), suit: card.slice(-1).toLowerCase() };
200
+ }
201
+ function getSuitSymbol(suit) {
202
+ return SUIT_SYMBOLS[suit.toLowerCase()] ?? suit;
203
+ }
204
+ function getRankName(rank) {
205
+ return RANK_NAMES[rank.toUpperCase()] ?? rank;
206
+ }
207
+ function getSuitName(suit) {
208
+ return SUIT_NAMES[suit.toLowerCase()] ?? suit;
209
+ }
210
+ function isRedSuit(suit) {
211
+ const s = suit.toLowerCase();
212
+ return s === "h" || s === "d";
213
+ }
214
+ function isKnownSuit(suit) {
215
+ return suit.toLowerCase() in SUIT_SYMBOLS;
216
+ }
217
+ function describeCard(card) {
218
+ if (card === null || card === "") return "face-down card";
219
+ const { rank, suit } = parseCard(card);
220
+ const rankName = RANK_NAMES[rank.toUpperCase()];
221
+ const suitName = SUIT_NAMES[suit];
222
+ if (!rankName || !suitName) return card;
223
+ return `${rankName} of ${suitName}`;
55
224
  }
56
225
 
57
226
  // src/seat.ts
58
227
  function nextSeatIndex(current, seatCount, opts = {}) {
59
- const n = Math.max(1, Math.floor(seatCount));
228
+ const floored = Math.floor(seatCount);
229
+ const n = Number.isFinite(floored) && floored > 0 ? floored : 1;
60
230
  const dir = opts.dir ?? 1;
61
231
  const skip = opts.skip;
62
- const start = (Math.floor(current) % n + n) % n;
232
+ const start = ((Math.floor(current) || 0) % n + n) % n;
63
233
  let i = start;
64
234
  for (let step = 0; step < n - 1; step++) {
65
235
  i = (i + dir + n) % n;
@@ -86,6 +256,112 @@ function ringPoints(count, cx, cy, rx, ry, startAngle = 90) {
86
256
  return Array.from({ length: n }, (_, i) => ellipsePoint(cx, cy, rx, ry, seatAngle(i, n, startAngle)));
87
257
  }
88
258
 
259
+ // src/layout.ts
260
+ var DESIGN_W = 1200;
261
+ var DESIGN_H = 900;
262
+ var DEFAULT_MARGIN_X = 164 / DESIGN_W;
263
+ var DEFAULT_MARGIN_Y = 196 / DESIGN_H;
264
+ var DEFAULT_FELT_GAP = 11;
265
+ var DEFAULT_BET_SCALE = 0.72;
266
+ var DEFAULT_SEAT_GAP = 24;
267
+ var DEFAULT_SEAT_W = 160;
268
+ var DEFAULT_SEAT_H = 176;
269
+ var RIM_GAP = 6;
270
+ var GLOW_GAP = 2;
271
+ var SEAT_SHRINK_FLOOR = 0.5;
272
+ function tableRadii(designWidth, designHeight, marginX, marginY) {
273
+ return {
274
+ rx: Math.max(0, designWidth / 2 - marginX * designWidth),
275
+ ry: Math.max(0, designHeight / 2 - marginY * designHeight)
276
+ };
277
+ }
278
+ function seatRingRadii(designWidth, designHeight, tableRx, tableRy, seatW, seatH, feltGap) {
279
+ const rx = Math.min(tableRx - feltGap + seatW / 2 + 6, designWidth / 2 - seatW / 2);
280
+ const ry = Math.min(tableRy - feltGap + seatH / 2 + 6, designHeight / 2 - seatH / 2);
281
+ return { rx: Math.max(0, rx), ry: Math.max(0, ry) };
282
+ }
283
+ function seatPoint(index, count, cx, cy, ringRx, ringRy, startAngle = 90) {
284
+ return ellipsePoint(cx, cy, ringRx, ringRy, seatAngle(index, count, startAngle));
285
+ }
286
+ function ellipseBox(designWidth, designHeight, rx, ry) {
287
+ const top = designHeight / 2 - ry;
288
+ const left = designWidth / 2 - rx;
289
+ return { top, right: left, bottom: top, left };
290
+ }
291
+ function boxSeparation(a, b) {
292
+ const dx = Math.max(b.x - a.x - (a.width + b.width) / 2, a.x - b.x - (a.width + b.width) / 2);
293
+ const dy = Math.max(b.y - a.y - (a.height + b.height) / 2, a.y - b.y - (a.height + b.height) / 2);
294
+ if (dx > 0 && dy > 0) return Math.hypot(dx, dy);
295
+ if (dx > 0) return dx;
296
+ if (dy > 0) return dy;
297
+ return Math.max(dx, dy);
298
+ }
299
+ function minSeatSeparation(seats) {
300
+ let worst = Infinity;
301
+ for (let i = 0; i < seats.length; i++) {
302
+ for (let j = i + 1; j < seats.length; j++) {
303
+ const s = boxSeparation(seats[i], seats[j]);
304
+ if (s < worst) worst = s;
305
+ }
306
+ }
307
+ return worst;
308
+ }
309
+ function positionsFor(count, cx, cy, ringRx, ringRy, seatW, seatH, startAngle) {
310
+ return Array.from({ length: count }, (_, index) => {
311
+ const p = seatPoint(index, count, cx, cy, ringRx, ringRy, startAngle);
312
+ return { index, x: p.x, y: p.y, width: seatW, height: seatH };
313
+ });
314
+ }
315
+ function fitSeatScale(opts) {
316
+ if (opts.seatGap <= 0) return 1;
317
+ const { rx, ry } = tableRadii(opts.designWidth, opts.designHeight, opts.marginX, opts.marginY);
318
+ const cx = opts.designWidth / 2;
319
+ const cy = opts.designHeight / 2;
320
+ for (let step = 0; step <= 50; step++) {
321
+ const scale = 1 - step * 0.01;
322
+ const w = Math.max(1, Math.round(opts.seatWidth * scale));
323
+ const h = Math.max(1, Math.round(opts.seatHeight * scale));
324
+ const ring = seatRingRadii(opts.designWidth, opts.designHeight, rx, ry, w, h, opts.feltGap);
325
+ const seats = positionsFor(opts.count, cx, cy, ring.rx, ring.ry, w, h, opts.startAngle);
326
+ if (minSeatSeparation(seats) >= opts.seatGap) return scale;
327
+ if (scale <= SEAT_SHRINK_FLOOR) break;
328
+ }
329
+ return SEAT_SHRINK_FLOOR;
330
+ }
331
+ function layoutSeats(opts) {
332
+ const { rx, ry } = tableRadii(opts.designWidth, opts.designHeight, opts.marginX, opts.marginY);
333
+ const cx = opts.designWidth / 2;
334
+ const cy = opts.designHeight / 2;
335
+ const scale = fitSeatScale(opts);
336
+ const seatW = Math.max(1, Math.round(opts.seatWidth * scale));
337
+ const seatH = Math.max(1, Math.round(opts.seatHeight * scale));
338
+ const ring = seatRingRadii(opts.designWidth, opts.designHeight, rx, ry, seatW, seatH, opts.feltGap);
339
+ const betScale = Math.min(1, Math.max(0.2, opts.betRadiusScale ?? DEFAULT_BET_SCALE));
340
+ const betRx = rx * betScale;
341
+ const betRy = ry * betScale;
342
+ const box = (bx, by) => ellipseBox(opts.designWidth, opts.designHeight, bx, by);
343
+ return {
344
+ cx,
345
+ cy,
346
+ tableRx: rx,
347
+ tableRy: ry,
348
+ ringRx: ring.rx,
349
+ ringRy: ring.ry,
350
+ betRx,
351
+ betRy,
352
+ seatW,
353
+ seatH,
354
+ seats: positionsFor(opts.count, cx, cy, ring.rx, ring.ry, seatW, seatH, opts.startAngle),
355
+ boxes: {
356
+ table: box(rx, ry),
357
+ felt: box(rx - opts.feltGap, ry - opts.feltGap),
358
+ rim: box(rx - RIM_GAP, ry - RIM_GAP),
359
+ glow: box(rx - GLOW_GAP, ry - GLOW_GAP),
360
+ bettingLine: box(betRx, betRy)
361
+ }
362
+ };
363
+ }
364
+
89
365
  // src/components/PlayingCard.tsx
90
366
  var import_jsx_runtime = require("react/jsx-runtime");
91
367
  var SIZE_CLASSES = {
@@ -126,82 +402,77 @@ var BACK_EMBLEM_SIZE = {
126
402
  md: 28,
127
403
  lg: 42
128
404
  };
129
- function parseCard(card) {
130
- if (!card || card.length < 2) return { rank: "", suit: "" };
131
- return { rank: card.slice(0, -1), suit: card.slice(-1) };
132
- }
133
- function getSuitSymbol(suit) {
134
- switch (suit) {
135
- case "s":
136
- return "\u2660";
137
- case "h":
138
- return "\u2665";
139
- case "d":
140
- return "\u2666";
141
- case "c":
142
- return "\u2663";
143
- default:
144
- return suit;
145
- }
146
- }
147
405
  function getSuitClass(suit) {
148
- switch (suit) {
149
- case "s":
150
- case "c":
151
- return "text-neutral-900";
152
- case "h":
153
- case "d":
154
- return "text-red-600";
155
- default:
156
- return "";
157
- }
406
+ if (isRedSuit(suit)) return "text-red-600";
407
+ return isKnownSuit(suit) ? "text-neutral-900" : "";
158
408
  }
159
409
  var CARD_FACE = "relative bg-gradient-to-br from-white via-white to-neutral-100 rounded-md border border-neutral-300/90 overflow-hidden shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]";
160
- function PlayingCard({ card, size = "md", className }) {
161
- if (!card) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardBack, { size, className });
410
+ function PlayingCard({ card, size = "md", label, className }) {
411
+ if (!card) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardBack, { size, className, label });
412
+ const name = label ?? describeCard(card);
162
413
  const { rank, suit } = parseCard(card);
163
414
  const suitSymbol = getSuitSymbol(suit);
164
415
  const suitClass = getSuitClass(suit);
165
416
  const large = size === "md" || size === "lg";
166
417
  if (!large) {
167
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn(SIZE_CLASSES[size], CARD_FACE, className), style: { aspectRatio: "10/14" }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute inset-0 flex flex-col leading-none", suitClass), children: [
168
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(SMALL_RANK[size], "font-bold tracking-tight"), children: rank }) }),
169
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: SMALL_SUIT[size], children: suitSymbol }) })
170
- ] }) });
418
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
419
+ "div",
420
+ {
421
+ role: "img",
422
+ "aria-label": name,
423
+ className: cn(SIZE_CLASSES[size], CARD_FACE, className),
424
+ style: { aspectRatio: "10/14" },
425
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute inset-0 flex flex-col leading-none", suitClass), children: [
426
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(SMALL_RANK[size], "font-bold tracking-tight"), children: rank }) }),
427
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: SMALL_SUIT[size], children: suitSymbol }) })
428
+ ] })
429
+ }
430
+ );
171
431
  }
172
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn(SIZE_CLASSES[size], CARD_FACE, className), style: { aspectRatio: "10/14" }, children: [
173
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
174
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
175
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
176
- ] }),
177
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
178
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
179
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
180
- ] }),
181
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("absolute inset-0 flex items-center justify-center opacity-90", suitClass), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: CENTER_SUIT[size], children: suitSymbol }) })
182
- ] });
432
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
433
+ "div",
434
+ {
435
+ role: "img",
436
+ "aria-label": name,
437
+ className: cn(SIZE_CLASSES[size], CARD_FACE, className),
438
+ style: { aspectRatio: "10/14" },
439
+ children: [
440
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
441
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
442
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
443
+ ] }),
444
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
445
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
446
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
447
+ ] }),
448
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("absolute inset-0 flex items-center justify-center opacity-90", suitClass), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: CENTER_SUIT[size], children: suitSymbol }) })
449
+ ]
450
+ }
451
+ );
183
452
  }
184
453
  function CardBackEmblem({ size }) {
185
454
  const s = BACK_EMBLEM_SIZE[size];
186
455
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: s, height: s, viewBox: "0 0 40 40", className: "drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]", "aria-hidden": "true", children: [
187
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { fill: "#f0c04a", opacity: "0.92", children: [
456
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { fill: "var(--color-card-back-emblem, #f0c04a)", opacity: "0.92", children: [
188
457
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "11", rx: "4", ry: "7" }),
189
458
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "29", rx: "4", ry: "7" }),
190
459
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "11", cy: "20", rx: "7", ry: "4" }),
191
460
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "29", cy: "20", rx: "7", ry: "4" })
192
461
  ] }),
193
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "20", cy: "20", r: "3.2", fill: "#5a0c18" })
462
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "20", cy: "20", r: "3.2", fill: "var(--color-card-back, #5a0c18)" })
194
463
  ] });
195
464
  }
196
- function CardBack({ size = "md", className }) {
465
+ function CardBack({ size = "md", label = "face-down card", className }) {
197
466
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
198
467
  "div",
199
468
  {
469
+ role: "img",
470
+ "aria-label": label,
200
471
  className: cn(
201
472
  SIZE_CLASSES[size],
202
473
  "relative rounded-md p-[3px] overflow-hidden",
203
- "bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]",
204
- "border-2 border-[#c9a24b]/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]",
474
+ "bg-gradient-to-br from-card-back-light via-card-back to-card-back-dark",
475
+ "border-2 border-card-back-ink/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]",
205
476
  className
206
477
  ),
207
478
  style: { aspectRatio: "10/14" },
@@ -209,8 +480,23 @@ function CardBack({ size = "md", className }) {
209
480
  }
210
481
  );
211
482
  }
212
- function HoleCards({ cards, size = "lg", className, deal = true }) {
213
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("flex gap-2", className), children: cards.map((card, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn(deal && "animate-deal-in"), style: deal ? { animationDelay: `${i * 100}ms` } : void 0, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlayingCard, { card, size }) }, i)) });
483
+ function HoleCards({ cards, size = "lg", className, deal = true, dealKey }) {
484
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("flex gap-2", className), children: cards.map((card, i) => (
485
+ /*
486
+ * Keying on the card rather than the index is what makes the deal
487
+ * animation replay: React reuses an element whose key is unchanged, and
488
+ * a reused element never restarts its CSS animation.
489
+ */
490
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
491
+ "div",
492
+ {
493
+ className: cn(deal && "animate-deal-in"),
494
+ style: deal ? { animationDelay: `${i * DEAL_STAGGER_MS}ms` } : void 0,
495
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlayingCard, { card, size })
496
+ },
497
+ dealKey !== void 0 ? `${dealKey}:${i}` : `${i}:${card ?? "back"}`
498
+ )
499
+ )) });
214
500
  }
215
501
  function CommunityCards({ cards, size = "md", className, animated = true, slots }) {
216
502
  const count = slots ?? cards.length;
@@ -219,14 +505,171 @@ function CommunityCards({ cards, size = "md", className, animated = true, slots
219
505
  "div",
220
506
  {
221
507
  className: cn("h-full w-full", animated && "animate-card-flip"),
222
- style: animated ? { animationDelay: `${i * 70}ms` } : void 0,
508
+ style: animated ? { animationDelay: `${i * FLIP_STAGGER_MS}ms` } : void 0,
223
509
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlayingCard, { card, size })
224
510
  }
225
511
  ) : null }, i)) });
226
512
  }
227
513
 
228
- // src/components/Seat.tsx
514
+ // src/chips.ts
515
+ var DEFAULT_DENOMINATIONS = [
516
+ { value: 100, color: "#1a1a2e", className: "fill-chip-black" },
517
+ { value: 25, color: "#27ae60", className: "fill-chip-green" },
518
+ { value: 10, color: "#2980b9", className: "fill-chip-blue" },
519
+ { value: 5, color: "#c0392b", className: "fill-chip-red" },
520
+ { value: 1, color: "#ecf0f1", className: "fill-chip-white" }
521
+ ];
522
+ var SPOT_CANDIDATES = ["rgba(255,255,255,0.9)", "rgba(0,0,0,0.45)"];
523
+ function chipSpotColor(bodyColor) {
524
+ return mostVisible(SPOT_CANDIDATES, bodyColor);
525
+ }
526
+ function chipBreakdown(amount, denominations = DEFAULT_DENOMINATIONS) {
527
+ const usable = denominations.filter((d) => Number.isFinite(d.value) && d.value >= 1);
528
+ const sorted = [...usable].sort((a, b) => b.value - a.value);
529
+ let remaining = Math.max(0, Math.floor(Number.isFinite(amount) ? amount : 0));
530
+ const entries = [];
531
+ for (const denomination of sorted) {
532
+ const count = Math.floor(remaining / denomination.value);
533
+ if (count > 0) {
534
+ entries.push({ ...denomination, count });
535
+ remaining -= count * denomination.value;
536
+ }
537
+ }
538
+ return entries;
539
+ }
540
+ function chipsTotal(entries) {
541
+ return entries.reduce((sum, e) => sum + e.value * e.count, 0);
542
+ }
543
+ function chipCount(entries) {
544
+ return entries.reduce((sum, e) => sum + e.count, 0);
545
+ }
546
+
547
+ // src/components/Chip.tsx
548
+ var import_react = require("react");
229
549
  var import_jsx_runtime2 = require("react/jsx-runtime");
550
+ var CHIP_WIDTH = { sm: 22, md: 30, lg: 42 };
551
+ var SPOT_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];
552
+ function Chip({
553
+ value,
554
+ color = "#2980b9",
555
+ colorClassName,
556
+ spotColor,
557
+ size = "md",
558
+ label,
559
+ className
560
+ }) {
561
+ const clipId = `chip-clip-${(0, import_react.useId)().replace(/:/g, "")}`;
562
+ const width = CHIP_WIDTH[size];
563
+ const spots = spotColor ?? chipSpotColor(color);
564
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
565
+ "svg",
566
+ {
567
+ width,
568
+ height: width * 0.625,
569
+ viewBox: "0 0 48 30",
570
+ role: "img",
571
+ "aria-label": label ?? (value !== void 0 ? `${value} chip` : "chip"),
572
+ className,
573
+ children: [
574
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("clipPath", { id: clipId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "15", rx: "22", ry: "13" }) }) }),
575
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "18", rx: "22", ry: "13", className: colorClassName, fill: color }),
576
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "18", rx: "22", ry: "13", fill: "rgba(0,0,0,0.32)" }),
577
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "15", rx: "22", ry: "13", className: colorClassName, fill: color }),
578
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("g", { clipPath: `url(#${clipId})`, children: SPOT_ANGLES.map((angle) => {
579
+ const rad = angle * Math.PI / 180;
580
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
581
+ "rect",
582
+ {
583
+ x: -1.7,
584
+ y: -3,
585
+ width: 3.4,
586
+ height: 6,
587
+ rx: 1.3,
588
+ fill: spots,
589
+ transform: `translate(${24 + 19.3 * Math.cos(rad)} ${15 + 10.7 * Math.sin(rad)}) rotate(${angle})`
590
+ },
591
+ angle
592
+ );
593
+ }) }),
594
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "15", rx: "21", ry: "12.1", fill: "none", stroke: "rgba(255,255,255,0.3)", strokeWidth: "1.2" }),
595
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "15", rx: "13.4", ry: "7.9", fill: "rgba(255,255,255,0.94)" }),
596
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("ellipse", { cx: "24", cy: "15", rx: "13.4", ry: "7.9", fill: "none", stroke: "rgba(0,0,0,0.18)", strokeWidth: "0.8" }),
597
+ value !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
598
+ "text",
599
+ {
600
+ x: "24",
601
+ y: "18.3",
602
+ textAnchor: "middle",
603
+ fontSize: "9.5",
604
+ fontWeight: "700",
605
+ fill: "#171717",
606
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
607
+ children: value
608
+ }
609
+ )
610
+ ]
611
+ }
612
+ );
613
+ }
614
+ function ChipStack({
615
+ amount,
616
+ size = "md",
617
+ denominations = DEFAULT_DENOMINATIONS,
618
+ maxChips = 12,
619
+ label,
620
+ className
621
+ }) {
622
+ const entries = chipBreakdown(amount, denominations);
623
+ const total = chipCount(entries);
624
+ const drawn = [];
625
+ for (const entry of entries) {
626
+ for (let i = 0; i < entry.count && drawn.length < maxChips; i++) drawn.push(entry);
627
+ if (drawn.length >= maxChips) break;
628
+ }
629
+ const hidden = total - drawn.length;
630
+ const offset = Math.max(3, Math.round(CHIP_WIDTH[size] * 0.17));
631
+ const breakdown = entries.map((e) => `${e.count}x${e.label ?? e.value}`).join(", ");
632
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
633
+ "div",
634
+ {
635
+ className: cn("inline-flex flex-col items-center gap-1", className),
636
+ role: "img",
637
+ "aria-label": `${amount} in ${total} ${total === 1 ? "chip" : "chips"}${breakdown ? `: ${breakdown}` : ""}`,
638
+ children: [
639
+ drawn.length > 0 && // Presentation only — the stack already carries the accessible name.
640
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { "aria-hidden": "true", className: "flex flex-col items-center drop-shadow-[0_3px_4px_rgba(0,0,0,0.5)]", children: drawn.map((entry, i) => (
641
+ // Later chips are tucked under the ones above them, so z-index has
642
+ // to descend as the stack grows.
643
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "relative", style: { marginTop: i === 0 ? 0 : -offset, zIndex: drawn.length - i }, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
644
+ Chip,
645
+ {
646
+ value: entry.label ?? entry.value,
647
+ color: entry.color,
648
+ colorClassName: entry.className,
649
+ spotColor: entry.spotColor,
650
+ size
651
+ }
652
+ ) }, i)
653
+ )) }),
654
+ hidden > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "font-mono text-[10px] leading-none text-neutral-400", children: [
655
+ "+",
656
+ hidden
657
+ ] }),
658
+ label !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "leading-none", children: label })
659
+ ]
660
+ }
661
+ );
662
+ }
663
+
664
+ // src/components/seatSize.ts
665
+ var import_react2 = require("react");
666
+ var SeatSizeContext = (0, import_react2.createContext)(null);
667
+ function useSeatSize() {
668
+ return (0, import_react2.useContext)(SeatSizeContext);
669
+ }
670
+
671
+ // src/components/Seat.tsx
672
+ var import_jsx_runtime3 = require("react/jsx-runtime");
230
673
  function Seat({
231
674
  isActive = false,
232
675
  isWinner = false,
@@ -244,21 +687,24 @@ function Seat({
244
687
  topLeft,
245
688
  topRight,
246
689
  topCenter,
247
- width = 160,
248
- height = 176,
690
+ width,
691
+ height,
249
692
  className,
250
693
  frameClassName,
251
694
  frameStyle
252
695
  }) {
253
- const activeClass = animated ? "border-amber-400 animate-turn-glow" : "border-amber-400 shadow-[0_0_18px_rgba(243,156,18,0.5)]";
254
- const winnerClass = animated ? "border-amber-400 animate-winner-glow" : "border-amber-400 shadow-[0_0_0_3px_rgba(243,156,18,0.9)]";
255
- const effTopLeft = topLeft !== void 0 ? topLeft : isWinner && winnerCrown ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-3xl drop-shadow-[0_2px_4px_rgba(0,0,0,0.6)] animate-bounce", children: "\u{1F451}" }) : void 0;
256
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: cn("relative flex flex-col items-center", className), style: { width }, children: [
257
- topCenter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1", children: topCenter }),
258
- effTopLeft !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute -top-3 -left-2 z-40", children: effTopLeft }),
259
- topRight !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute -top-2 -right-2 z-40", children: topRight }),
260
- above !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2", children: above }),
261
- /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
696
+ const laidOut = useSeatSize();
697
+ const frameWidth = width ?? laidOut?.width ?? DEFAULT_SEAT_W;
698
+ const frameHeight = height ?? laidOut?.height ?? DEFAULT_SEAT_H;
699
+ const activeClass = animated ? "border-gold animate-turn-glow" : "border-gold shadow-[0_0_18px_color-mix(in_oklab,var(--color-gold)_50%,transparent)]";
700
+ const winnerClass = animated ? "border-gold animate-winner-glow" : "border-gold shadow-[0_0_0_3px_color-mix(in_oklab,var(--color-gold)_90%,transparent)]";
701
+ const effTopLeft = topLeft !== void 0 ? topLeft : isWinner && winnerCrown ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-3xl drop-shadow-[0_2px_4px_rgba(0,0,0,0.6)] animate-bounce", children: "\u{1F451}" }) : void 0;
702
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("relative flex flex-col items-center", className), style: { width: frameWidth }, children: [
703
+ topCenter !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1", children: topCenter }),
704
+ effTopLeft !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute -top-3 -left-2 z-40", children: effTopLeft }),
705
+ topRight !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute -top-2 -right-2 z-40", children: topRight }),
706
+ above !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2", children: above }),
707
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
262
708
  "div",
263
709
  {
264
710
  role: onClick ? "button" : void 0,
@@ -267,98 +713,86 @@ function Seat({
267
713
  onKeyDown: onClick ? (e) => {
268
714
  if (e.key === "Enter" || e.key === " ") onClick();
269
715
  } : void 0,
270
- style: { width, height, ...frameStyle },
716
+ style: { width: frameWidth, height: frameHeight, ...frameStyle },
271
717
  className: cn(
272
718
  "relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition",
273
719
  "border-neutral-700",
274
720
  isActive && activeClass,
275
721
  isWinner && winnerClass,
276
722
  isDimmed && "opacity-50",
277
- onClick && "cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400",
723
+ onClick && "cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-gold",
278
724
  frameClassName
279
725
  ),
280
726
  children: [
281
- header !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex w-full shrink-0 items-center justify-between gap-1 text-[11px]", children: header }),
282
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex min-h-0 flex-1 items-center justify-center", children }),
283
- footer !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300", children: footer }),
284
- overlay !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 text-center", children: overlay })
727
+ header !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex w-full shrink-0 items-center justify-between gap-1 text-[11px]", children: header }),
728
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex min-h-0 flex-1 items-center justify-center", children }),
729
+ footer !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300", children: footer }),
730
+ overlay !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 text-center", children: overlay })
285
731
  ]
286
732
  }
287
733
  ),
288
- badge !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute top-0 right-0 z-40", children: badge }),
289
- below !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-1 flex flex-col items-center gap-1", children: below })
734
+ badge !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute top-0 right-0 z-40", children: badge }),
735
+ below !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "mt-1 flex flex-col items-center gap-1", children: below })
290
736
  ] });
291
737
  }
292
738
 
293
739
  // src/components/Pot.tsx
294
- var import_react = require("react");
295
- var import_jsx_runtime3 = require("react/jsx-runtime");
740
+ var import_react3 = require("react");
741
+ var import_jsx_runtime4 = require("react/jsx-runtime");
742
+ var defaultFormat = (amount) => amount.toLocaleString();
743
+ function prefersReducedMotion() {
744
+ if (typeof window === "undefined" || typeof window.matchMedia !== "function") return false;
745
+ return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
746
+ }
296
747
  function CountUp({ value, format }) {
297
- const [shown, setShown] = (0, import_react.useState)(value);
298
- const fromRef = (0, import_react.useRef)(value);
299
- (0, import_react.useEffect)(() => {
300
- const from = fromRef.current;
748
+ const [shown, setShown] = (0, import_react3.useState)(value);
749
+ const shownRef = (0, import_react3.useRef)(value);
750
+ (0, import_react3.useEffect)(() => {
751
+ const from = shownRef.current;
301
752
  const to = value;
302
753
  if (from === to) return;
754
+ if (prefersReducedMotion()) {
755
+ shownRef.current = to;
756
+ setShown(to);
757
+ return;
758
+ }
303
759
  let raf = 0;
304
- const start = performance.now();
305
- const dur = 500;
760
+ const dur = COUNT_UP_MS;
761
+ let origin = null;
306
762
  const tick = (now) => {
307
- const t = Math.min(1, (now - start) / dur);
763
+ if (origin === null) origin = now;
764
+ const t = Math.min(1, Math.max(0, (now - origin) / dur));
308
765
  const eased = 1 - Math.pow(1 - t, 3);
309
- setShown(Math.round(from + (to - from) * eased));
766
+ const next = Math.round(from + (to - from) * eased);
767
+ shownRef.current = next;
768
+ setShown(next);
310
769
  if (t < 1) raf = requestAnimationFrame(tick);
311
- else fromRef.current = to;
312
770
  };
313
771
  raf = requestAnimationFrame(tick);
314
772
  return () => cancelAnimationFrame(raf);
315
773
  }, [value]);
316
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_jsx_runtime3.Fragment, { children: format(shown) });
774
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(import_jsx_runtime4.Fragment, { children: format(shown) });
317
775
  }
318
- function Pot({ amount, label, format = formatBB, animate = true, className }) {
319
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("flex items-center gap-2 font-mono text-sm text-amber-400", className), children: [
320
- label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-neutral-400", children: label }),
321
- /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "whitespace-nowrap rounded-full border border-amber-400/50 bg-neutral-900/80 px-3 py-1 shadow-inner", children: animate ? /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CountUp, { value: amount, format }) : format(amount) })
776
+ function Pot({ amount, label, format = defaultFormat, animate = true, className }) {
777
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("div", { className: cn("flex items-center gap-2 font-mono text-sm text-gold", className), children: [
778
+ label && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-neutral-400", children: label }),
779
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "whitespace-nowrap rounded-full border border-gold/50 bg-neutral-900/80 px-3 py-1 shadow-inner", children: animate ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(CountUp, { value: amount, format }) : format(amount) })
322
780
  ] });
323
781
  }
324
782
 
325
783
  // src/components/CardTable.tsx
326
- var import_react2 = require("react");
327
- var import_jsx_runtime4 = require("react/jsx-runtime");
328
- var DESIGN_W = 1200;
329
- var DESIGN_H = 900;
330
- var MARGIN_X = 164;
331
- var MARGIN_Y = 196;
332
- var TABLE_RX = DESIGN_W / 2 - MARGIN_X;
333
- var TABLE_RY = DESIGN_H / 2 - MARGIN_Y;
334
- var FELT_GAP = 11;
335
- var BET_SCALE = 0.72;
336
- var BET_RX = TABLE_RX * BET_SCALE;
337
- var BET_RY = TABLE_RY * BET_SCALE;
338
- var CONFETTI_COLORS = ["#f39c12", "#e74c3c", "#2ecc71", "#3498db", "#f1c40f", "#9b59b6"];
339
- function calculateSeatLayouts(seatCount, seatWidth, seatHeight, startAngle) {
340
- const cx = DESIGN_W / 2;
341
- const cy = DESIGN_H / 2;
342
- const ringX = TABLE_RX - FELT_GAP + seatWidth / 2 + 6;
343
- const ringY = TABLE_RY - FELT_GAP + seatHeight / 2 + 6;
344
- const layouts = [];
345
- for (let i = 0; i < seatCount; i++) {
346
- const pt = ellipsePoint(cx, cy, ringX, ringY, seatAngle(i, seatCount, startAngle));
347
- layouts.push({
348
- x: pt.x - seatWidth / 2,
349
- y: pt.y - seatHeight / 2,
350
- index: i
351
- });
352
- }
353
- return layouts;
354
- }
784
+ var import_react4 = require("react");
785
+ var import_jsx_runtime5 = require("react/jsx-runtime");
786
+ var MIN_FIT_ASPECT = 0.5;
787
+ var MAX_FIT_ASPECT = 2.5;
788
+ var DEFAULT_CONFETTI_COLORS = ["#f39c12", "#e74c3c", "#2ecc71", "#3498db", "#f1c40f", "#9b59b6"];
355
789
  function CardTable({
356
790
  seatCount = 6,
357
791
  seats,
358
792
  renderSeat,
359
793
  bets,
360
794
  renderBet,
361
- betRadiusScale = BET_SCALE,
795
+ betRadiusScale = DEFAULT_BET_SCALE,
362
796
  startAngle = 90,
363
797
  yourSeatIndex = null,
364
798
  center,
@@ -366,96 +800,142 @@ function CardTable({
366
800
  yourTurn = false,
367
801
  winnerSeats = [],
368
802
  confetti = true,
369
- seatWidth = 160,
370
- seatHeight = 176,
803
+ confettiColors = DEFAULT_CONFETTI_COLORS,
804
+ seatWidth = DEFAULT_SEAT_W,
805
+ seatHeight = DEFAULT_SEAT_H,
806
+ seatGap = DEFAULT_SEAT_GAP,
807
+ designWidth = DESIGN_W,
808
+ designHeight = DESIGN_H,
809
+ marginX = DEFAULT_MARGIN_X,
810
+ marginY = DEFAULT_MARGIN_Y,
811
+ feltGap = DEFAULT_FELT_GAP,
812
+ fit = "design",
371
813
  className
372
814
  }) {
373
- const count = Math.max(2, Math.min(10, Math.floor(seatCount)));
374
- const outerRef = (0, import_react2.useRef)(null);
375
- const [scale, setScale] = (0, import_react2.useState)(1);
376
- const layouts = (0, import_react2.useMemo)(
377
- () => calculateSeatLayouts(count, seatWidth, seatHeight, startAngle),
378
- [count, seatWidth, seatHeight, startAngle]
379
- );
380
- const betRx = TABLE_RX * betRadiusScale;
381
- const betRy = TABLE_RY * betRadiusScale;
382
- const hasBets = bets !== void 0 || renderBet !== void 0;
383
- (0, import_react2.useLayoutEffect)(() => {
815
+ const requested = Math.floor(seatCount);
816
+ const count = Number.isFinite(requested) ? Math.max(1, requested) : 1;
817
+ const outerRef = (0, import_react4.useRef)(null);
818
+ const [box, setBox] = (0, import_react4.useState)({ w: 0, h: 0 });
819
+ (0, import_react4.useLayoutEffect)(() => {
384
820
  const el = outerRef.current;
385
821
  if (!el) return;
386
822
  const compute = () => {
387
823
  const w = el.clientWidth;
388
824
  const h = el.clientHeight;
389
825
  if (!w || !h) return;
390
- setScale(Math.min(w / DESIGN_W, h / DESIGN_H));
826
+ setBox({ w, h });
391
827
  };
392
828
  compute();
393
829
  const ro = new ResizeObserver(compute);
394
830
  ro.observe(el);
395
831
  return () => ro.disconnect();
396
832
  }, []);
397
- const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
398
- const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
399
- const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
400
- const ringInset = { inset: `${MARGIN_Y + 2}px ${MARGIN_X + 2}px` };
401
- const betLineInset = {
402
- inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
403
- };
404
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
833
+ const geo = (0, import_react4.useMemo)(() => {
834
+ const dh = designHeight;
835
+ const dw = fit === "container" && box.w > 0 && box.h > 0 ? dh * Math.min(MAX_FIT_ASPECT, Math.max(MIN_FIT_ASPECT, box.w / box.h)) : designWidth;
836
+ const layout2 = layoutSeats({
837
+ count,
838
+ designWidth: dw,
839
+ designHeight: dh,
840
+ marginX,
841
+ marginY,
842
+ feltGap,
843
+ seatWidth,
844
+ seatHeight,
845
+ // Always applied. Every Seat inside the table is told its frame size
846
+ // through context, so the shrink is no longer limited to `renderSeat`.
847
+ seatGap,
848
+ startAngle,
849
+ betRadiusScale
850
+ });
851
+ const scale2 = box.w > 0 && box.h > 0 ? Math.min(box.w / dw, box.h / dh) : 1;
852
+ return { designW: dw, designH: dh, scale: scale2, layout: layout2 };
853
+ }, [
854
+ count,
855
+ designWidth,
856
+ designHeight,
857
+ marginX,
858
+ marginY,
859
+ feltGap,
860
+ seatWidth,
861
+ seatHeight,
862
+ seatGap,
863
+ startAngle,
864
+ fit,
865
+ betRadiusScale,
866
+ box.w,
867
+ box.h
868
+ ]);
869
+ const { designW, designH, scale, layout } = geo;
870
+ const { cx, cy, seatW, seatH, betRx, betRy, boxes } = layout;
871
+ const hasBets = bets !== void 0 || renderBet !== void 0;
872
+ const px = (b) => ({
873
+ top: `${b.top}px`,
874
+ right: `${b.right}px`,
875
+ bottom: `${b.bottom}px`,
876
+ left: `${b.left}px`
877
+ });
878
+ const tableFrameInset = px(boxes.table);
879
+ const feltInset = px(boxes.felt);
880
+ const rimInset = px(boxes.rim);
881
+ const ringInset = px(boxes.glow);
882
+ const betLineInset = px(boxes.bettingLine);
883
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
405
884
  "div",
406
885
  {
407
886
  ref: outerRef,
408
887
  className: cn("relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden", className),
409
- children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
888
+ children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: { width: designW * scale, height: designH * scale }, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
410
889
  "div",
411
890
  {
412
891
  className: "relative",
413
- style: { width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: "top left" },
892
+ style: { width: designW, height: designH, transform: `scale(${scale})`, transformOrigin: "top left" },
414
893
  children: [
415
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
894
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
416
895
  "div",
417
896
  {
418
- className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl",
897
+ className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--color-gold-light)_16%,transparent),transparent_72%)] blur-3xl",
419
898
  "aria-hidden": "true"
420
899
  }
421
900
  ),
422
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
901
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
423
902
  "div",
424
903
  {
425
- className: "absolute rounded-[50%] bg-gradient-to-b from-[#a5744a] via-[#6b3f22] to-[#38200f] shadow-[0_26px_70px_-12px_rgba(0,0,0,0.85),inset_0_3px_0_rgba(255,255,255,0.20),inset_0_-6px_16px_rgba(0,0,0,0.5)]",
904
+ className: "absolute rounded-[50%] bg-gradient-to-b from-rail via-rail-dark to-rail-deep shadow-[0_26px_70px_-12px_rgba(0,0,0,0.85),inset_0_3px_0_rgba(255,255,255,0.20),inset_0_-6px_16px_rgba(0,0,0,0.5)]",
426
905
  style: tableFrameInset,
427
906
  "aria-hidden": "true"
428
907
  }
429
908
  ),
430
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
909
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
431
910
  "div",
432
911
  {
433
- className: "absolute rounded-[50%] bg-[radial-gradient(ellipse_at_50%_32%,#23804b,#0f4d2e_55%,#093b22)] shadow-[inset_0_0_90px_rgba(0,0,0,0.55),inset_0_3px_10px_rgba(255,255,255,0.06)]",
912
+ className: "absolute rounded-[50%] bg-[radial-gradient(ellipse_at_50%_32%,var(--color-felt-light),var(--color-felt)_55%,var(--color-felt-dark))] shadow-[inset_0_0_90px_rgba(0,0,0,0.55),inset_0_3px_10px_rgba(255,255,255,0.06)]",
434
913
  style: feltInset,
435
914
  "aria-hidden": "true"
436
915
  }
437
916
  ),
438
- ambient && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
439
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
917
+ ambient && /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
918
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
440
919
  "div",
441
920
  {
442
- className: "absolute animate-spotlight rounded-[50%] bg-[radial-gradient(closest-side,rgba(255,214,120,0.14),transparent_70%)] blur-2xl",
921
+ className: "absolute animate-spotlight rounded-[50%] bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--color-gold-glow)_14%,transparent),transparent_70%)] blur-2xl",
443
922
  style: feltInset,
444
923
  "aria-hidden": "true"
445
924
  }
446
925
  ),
447
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute overflow-hidden rounded-[50%]", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 animate-felt-shimmer bg-[linear-gradient(115deg,transparent_42%,rgba(255,255,255,0.06)_50%,transparent_58%)] bg-[length:220%_100%]" }) })
926
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute overflow-hidden rounded-[50%]", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute inset-0 animate-felt-shimmer bg-[linear-gradient(115deg,transparent_42%,rgba(255,255,255,0.06)_50%,transparent_58%)] bg-[length:220%_100%]" }) })
448
927
  ] }),
449
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
928
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
450
929
  "div",
451
930
  {
931
+ "data-part": "betting-line",
452
932
  className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
453
933
  style: betLineInset,
454
934
  "aria-hidden": "true"
455
935
  }
456
936
  ),
457
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute flex items-center justify-center pointer-events-none", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { className: "text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none", children: "\u2660\u2665\u2666\u2663" }) }),
458
- yourTurn && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
937
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute flex items-center justify-center pointer-events-none", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none", children: "\u2660\u2665\u2666\u2663" }) }),
938
+ yourTurn && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
459
939
  "div",
460
940
  {
461
941
  className: "pointer-events-none absolute z-10 animate-turn-glow rounded-[50%]",
@@ -463,20 +943,33 @@ function CardTable({
463
943
  "aria-hidden": "true"
464
944
  }
465
945
  ),
466
- center !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center", children: center }),
467
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 z-20", children: layouts.map(({ x, y, index }) => {
946
+ center !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center", children: center }),
947
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute inset-0 z-20", children: /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(SeatSizeContext.Provider, { value: { width: seatW, height: seatH }, children: layout.seats.map((seat) => {
948
+ const { index, x, y } = seat;
468
949
  const isYou = index === yourSeatIndex;
469
- const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou) : null;
950
+ const box2 = { index, isYou, x, y, width: seatW, height: seatH };
951
+ const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou, box2) : null;
470
952
  if (content === null) return null;
471
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: content }, index);
472
- }) }),
473
- hasBets && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pointer-events-none absolute inset-0 z-20", children: Array.from({ length: count }).map((_, index) => {
953
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
954
+ "div",
955
+ {
956
+ "data-part": "seat",
957
+ "data-seat": index,
958
+ className: "absolute",
959
+ style: { left: `${x - seatW / 2}px`, top: `${y - seatH / 2}px` },
960
+ children: content
961
+ },
962
+ index
963
+ );
964
+ }) }) }),
965
+ hasBets && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pointer-events-none absolute inset-0 z-20", children: layout.seats.map(({ index }) => {
474
966
  const content = bets ? bets[index] ?? null : renderBet ? renderBet(index) : null;
475
967
  if (content === null || content === void 0) return null;
476
- const pt = ellipsePoint(DESIGN_W / 2, DESIGN_H / 2, betRx, betRy, seatAngle(index, count, startAngle));
477
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
968
+ const pt = ellipsePoint(cx, cy, betRx, betRy, seatAngle(index, count, startAngle));
969
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
478
970
  "div",
479
971
  {
972
+ "data-seat-bet": index,
480
973
  className: "absolute",
481
974
  style: { left: `${pt.x}px`, top: `${pt.y}px`, transform: "translate(-50%, -50%)" },
482
975
  children: content
@@ -484,12 +977,12 @@ function CardTable({
484
977
  index
485
978
  );
486
979
  }) }),
487
- confetti && winnerSeats.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "pointer-events-none absolute inset-0 z-30 overflow-hidden", "aria-hidden": "true", children: Array.from({ length: 44 }).map((_, i) => {
980
+ confetti && winnerSeats.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "pointer-events-none absolute inset-0 z-30 overflow-hidden", "aria-hidden": "true", children: Array.from({ length: 44 }).map((_, i) => {
488
981
  const w = 5 + i % 4 * 2;
489
982
  const h = 9 + i % 3 * 4;
490
983
  const dur = 1.7 + i % 5 * 0.28;
491
984
  const delay = i % 12 * 0.14;
492
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
985
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
493
986
  "span",
494
987
  {
495
988
  className: "absolute top-[26%] animate-confetti rounded-[2px]",
@@ -497,7 +990,7 @@ function CardTable({
497
990
  left: `${i * 37 % 96}%`,
498
991
  width: w,
499
992
  height: h,
500
- backgroundColor: CONFETTI_COLORS[i % CONFETTI_COLORS.length],
993
+ backgroundColor: confettiColors[i % confettiColors.length],
501
994
  animationDuration: `${dur}s`,
502
995
  animationDelay: `${delay}s`,
503
996
  animationIterationCount: "infinite"
@@ -506,7 +999,7 @@ function CardTable({
506
999
  i
507
1000
  );
508
1001
  }) }),
509
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
1002
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
510
1003
  ]
511
1004
  }
512
1005
  ) })
@@ -516,21 +1009,61 @@ function CardTable({
516
1009
  // Annotate the CommonJS export names for ESM import in node:
517
1010
  0 && (module.exports = {
518
1011
  BB,
1012
+ COUNT_UP_MS,
519
1013
  CardBack,
520
1014
  CardTable,
1015
+ Chip,
1016
+ ChipStack,
521
1017
  CommunityCards,
1018
+ DEAL_STAGGER_MS,
1019
+ DEFAULT_BET_SCALE,
1020
+ DEFAULT_DENOMINATIONS,
1021
+ DEFAULT_FELT_GAP,
1022
+ DEFAULT_MARGIN_X,
1023
+ DEFAULT_MARGIN_Y,
1024
+ DEFAULT_SEAT_GAP,
1025
+ DEFAULT_SEAT_H,
1026
+ DEFAULT_SEAT_W,
1027
+ DESIGN_H,
1028
+ DESIGN_W,
1029
+ FLIP_STAGGER_MS,
1030
+ GLOW_GAP,
522
1031
  HoleCards,
523
1032
  PlayingCard,
524
1033
  PokerTable,
525
1034
  Pot,
1035
+ RIM_GAP,
1036
+ SEAT_SHRINK_FLOOR,
1037
+ SIZES,
526
1038
  Seat,
1039
+ chipBreakdown,
1040
+ chipCount,
1041
+ chipSpotColor,
1042
+ chipsTotal,
527
1043
  cn,
1044
+ contrastRatio,
1045
+ describeCard,
1046
+ ellipseBox,
528
1047
  ellipsePoint,
1048
+ fitSeatScale,
1049
+ flatten,
529
1050
  formatBB,
1051
+ getRankName,
1052
+ getSuitName,
530
1053
  getSuitSymbol,
1054
+ isKnownSuit,
1055
+ isRedSuit,
1056
+ layoutSeats,
1057
+ minSeatSeparation,
1058
+ mostVisible,
531
1059
  nextSeatIndex,
532
1060
  parseCard,
1061
+ parseColor,
1062
+ relativeLuminance,
533
1063
  ringPoints,
534
- seatAngle
1064
+ seatAngle,
1065
+ seatPoint,
1066
+ seatRingRadii,
1067
+ tableRadii
535
1068
  });
536
1069
  //# sourceMappingURL=index.cjs.map