@stargazeryan/poker-ui 0.4.3 → 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,19 +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,
60
+ ellipsePoint: () => ellipsePoint,
61
+ fitSeatScale: () => fitSeatScale,
62
+ flatten: () => flatten,
34
63
  formatBB: () => formatBB,
64
+ getRankName: () => getRankName,
65
+ getSuitName: () => getSuitName,
35
66
  getSuitSymbol: () => getSuitSymbol,
67
+ isKnownSuit: () => isKnownSuit,
68
+ isRedSuit: () => isRedSuit,
69
+ layoutSeats: () => layoutSeats,
70
+ minSeatSeparation: () => minSeatSeparation,
71
+ mostVisible: () => mostVisible,
36
72
  nextSeatIndex: () => nextSeatIndex,
37
- parseCard: () => parseCard
73
+ parseCard: () => parseCard,
74
+ parseColor: () => parseColor,
75
+ relativeLuminance: () => relativeLuminance,
76
+ ringPoints: () => ringPoints,
77
+ seatAngle: () => seatAngle,
78
+ seatPoint: () => seatPoint,
79
+ seatRingRadii: () => seatRingRadii,
80
+ tableRadii: () => tableRadii
38
81
  });
39
82
  module.exports = __toCommonJS(index_exports);
40
83
 
@@ -46,17 +89,147 @@ function cn(...parts) {
46
89
 
47
90
  // src/format.ts
48
91
  var BB = 20;
49
- function formatBB(amount) {
50
- const bb = amount / BB;
51
- 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}`;
52
224
  }
53
225
 
54
226
  // src/seat.ts
55
227
  function nextSeatIndex(current, seatCount, opts = {}) {
56
- const n = Math.max(1, Math.floor(seatCount));
228
+ const floored = Math.floor(seatCount);
229
+ const n = Number.isFinite(floored) && floored > 0 ? floored : 1;
57
230
  const dir = opts.dir ?? 1;
58
231
  const skip = opts.skip;
59
- const start = (Math.floor(current) % n + n) % n;
232
+ const start = ((Math.floor(current) || 0) % n + n) % n;
60
233
  let i = start;
61
234
  for (let step = 0; step < n - 1; step++) {
62
235
  i = (i + dir + n) % n;
@@ -65,6 +238,130 @@ function nextSeatIndex(current, seatCount, opts = {}) {
65
238
  return start;
66
239
  }
67
240
 
241
+ // src/geometry.ts
242
+ function normCount(count) {
243
+ const n = Math.floor(count);
244
+ return Number.isFinite(n) && n > 0 ? n : 1;
245
+ }
246
+ function seatAngle(index, seatCount, startAngle = 90) {
247
+ const n = normCount(seatCount);
248
+ return startAngle + index * 360 / n;
249
+ }
250
+ function ellipsePoint(cx, cy, rx, ry, angleDeg) {
251
+ const rad = angleDeg * Math.PI / 180;
252
+ return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
253
+ }
254
+ function ringPoints(count, cx, cy, rx, ry, startAngle = 90) {
255
+ const n = normCount(count);
256
+ return Array.from({ length: n }, (_, i) => ellipsePoint(cx, cy, rx, ry, seatAngle(i, n, startAngle)));
257
+ }
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
+
68
365
  // src/components/PlayingCard.tsx
69
366
  var import_jsx_runtime = require("react/jsx-runtime");
70
367
  var SIZE_CLASSES = {
@@ -105,82 +402,77 @@ var BACK_EMBLEM_SIZE = {
105
402
  md: 28,
106
403
  lg: 42
107
404
  };
108
- function parseCard(card) {
109
- if (!card || card.length < 2) return { rank: "", suit: "" };
110
- return { rank: card.slice(0, -1), suit: card.slice(-1) };
111
- }
112
- function getSuitSymbol(suit) {
113
- switch (suit) {
114
- case "s":
115
- return "\u2660";
116
- case "h":
117
- return "\u2665";
118
- case "d":
119
- return "\u2666";
120
- case "c":
121
- return "\u2663";
122
- default:
123
- return suit;
124
- }
125
- }
126
405
  function getSuitClass(suit) {
127
- switch (suit) {
128
- case "s":
129
- case "c":
130
- return "text-neutral-900";
131
- case "h":
132
- case "d":
133
- return "text-red-600";
134
- default:
135
- return "";
136
- }
406
+ if (isRedSuit(suit)) return "text-red-600";
407
+ return isKnownSuit(suit) ? "text-neutral-900" : "";
137
408
  }
138
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)]";
139
- function PlayingCard({ card, size = "md", className }) {
140
- 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);
141
413
  const { rank, suit } = parseCard(card);
142
414
  const suitSymbol = getSuitSymbol(suit);
143
415
  const suitClass = getSuitClass(suit);
144
416
  const large = size === "md" || size === "lg";
145
417
  if (!large) {
146
- 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: [
147
- /* @__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 }) }),
148
- /* @__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 }) })
149
- ] }) });
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
+ );
150
431
  }
151
- return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn(SIZE_CLASSES[size], CARD_FACE, className), style: { aspectRatio: "10/14" }, children: [
152
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
153
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
154
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
155
- ] }),
156
- /* @__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: [
157
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
158
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
159
- ] }),
160
- /* @__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 }) })
161
- ] });
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
+ );
162
452
  }
163
453
  function CardBackEmblem({ size }) {
164
454
  const s = BACK_EMBLEM_SIZE[size];
165
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: [
166
- /* @__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: [
167
457
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "11", rx: "4", ry: "7" }),
168
458
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "29", rx: "4", ry: "7" }),
169
459
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "11", cy: "20", rx: "7", ry: "4" }),
170
460
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "29", cy: "20", rx: "7", ry: "4" })
171
461
  ] }),
172
- /* @__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)" })
173
463
  ] });
174
464
  }
175
- function CardBack({ size = "md", className }) {
465
+ function CardBack({ size = "md", label = "face-down card", className }) {
176
466
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
177
467
  "div",
178
468
  {
469
+ role: "img",
470
+ "aria-label": label,
179
471
  className: cn(
180
472
  SIZE_CLASSES[size],
181
473
  "relative rounded-md p-[3px] overflow-hidden",
182
- "bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]",
183
- "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)]",
184
476
  className
185
477
  ),
186
478
  style: { aspectRatio: "10/14" },
@@ -188,8 +480,23 @@ function CardBack({ size = "md", className }) {
188
480
  }
189
481
  );
190
482
  }
191
- function HoleCards({ cards, size = "lg", className, deal = true }) {
192
- 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
+ )) });
193
500
  }
194
501
  function CommunityCards({ cards, size = "md", className, animated = true, slots }) {
195
502
  const count = slots ?? cards.length;
@@ -198,14 +505,171 @@ function CommunityCards({ cards, size = "md", className, animated = true, slots
198
505
  "div",
199
506
  {
200
507
  className: cn("h-full w-full", animated && "animate-card-flip"),
201
- style: animated ? { animationDelay: `${i * 70}ms` } : void 0,
508
+ style: animated ? { animationDelay: `${i * FLIP_STAGGER_MS}ms` } : void 0,
202
509
  children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlayingCard, { card, size })
203
510
  }
204
511
  ) : null }, i)) });
205
512
  }
206
513
 
207
- // 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");
208
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");
209
673
  function Seat({
210
674
  isActive = false,
211
675
  isWinner = false,
@@ -223,21 +687,24 @@ function Seat({
223
687
  topLeft,
224
688
  topRight,
225
689
  topCenter,
226
- width = 160,
227
- height = 176,
690
+ width,
691
+ height,
228
692
  className,
229
693
  frameClassName,
230
694
  frameStyle
231
695
  }) {
232
- const activeClass = animated ? "border-amber-400 animate-turn-glow" : "border-amber-400 shadow-[0_0_18px_rgba(243,156,18,0.5)]";
233
- const winnerClass = animated ? "border-amber-400 animate-winner-glow" : "border-amber-400 shadow-[0_0_0_3px_rgba(243,156,18,0.9)]";
234
- 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;
235
- return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: cn("relative flex flex-col items-center", className), style: { width }, children: [
236
- 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 }),
237
- effTopLeft !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute -top-3 -left-2 z-40", children: effTopLeft }),
238
- topRight !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute -top-2 -right-2 z-40", children: topRight }),
239
- 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 }),
240
- /* @__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)(
241
708
  "div",
242
709
  {
243
710
  role: onClick ? "button" : void 0,
@@ -246,196 +713,229 @@ function Seat({
246
713
  onKeyDown: onClick ? (e) => {
247
714
  if (e.key === "Enter" || e.key === " ") onClick();
248
715
  } : void 0,
249
- style: { width, height, ...frameStyle },
716
+ style: { width: frameWidth, height: frameHeight, ...frameStyle },
250
717
  className: cn(
251
718
  "relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition",
252
719
  "border-neutral-700",
253
720
  isActive && activeClass,
254
721
  isWinner && winnerClass,
255
722
  isDimmed && "opacity-50",
256
- 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",
257
724
  frameClassName
258
725
  ),
259
726
  children: [
260
- 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 }),
261
- /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex min-h-0 flex-1 items-center justify-center", children }),
262
- 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 }),
263
- 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 })
264
731
  ]
265
732
  }
266
733
  ),
267
- badge !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "absolute top-0 right-0 z-40", children: badge }),
268
- 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 })
269
736
  ] });
270
737
  }
271
738
 
272
739
  // src/components/Pot.tsx
273
- var import_react = require("react");
274
- 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
+ }
275
747
  function CountUp({ value, format }) {
276
- const [shown, setShown] = (0, import_react.useState)(value);
277
- const fromRef = (0, import_react.useRef)(value);
278
- (0, import_react.useEffect)(() => {
279
- 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;
280
752
  const to = value;
281
753
  if (from === to) return;
754
+ if (prefersReducedMotion()) {
755
+ shownRef.current = to;
756
+ setShown(to);
757
+ return;
758
+ }
282
759
  let raf = 0;
283
- const start = performance.now();
284
- const dur = 500;
760
+ const dur = COUNT_UP_MS;
761
+ let origin = null;
285
762
  const tick = (now) => {
286
- 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));
287
765
  const eased = 1 - Math.pow(1 - t, 3);
288
- setShown(Math.round(from + (to - from) * eased));
766
+ const next = Math.round(from + (to - from) * eased);
767
+ shownRef.current = next;
768
+ setShown(next);
289
769
  if (t < 1) raf = requestAnimationFrame(tick);
290
- else fromRef.current = to;
291
770
  };
292
771
  raf = requestAnimationFrame(tick);
293
772
  return () => cancelAnimationFrame(raf);
294
773
  }, [value]);
295
- 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) });
296
775
  }
297
- function Pot({ amount, label, format = formatBB, animate = true, className }) {
298
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: cn("flex items-center gap-2 font-mono text-sm text-amber-400", className), children: [
299
- label && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-neutral-400", children: label }),
300
- /* @__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) })
301
780
  ] });
302
781
  }
303
782
 
304
783
  // src/components/CardTable.tsx
305
- var import_react2 = require("react");
306
- var import_jsx_runtime4 = require("react/jsx-runtime");
307
- var DESIGN_W = 1200;
308
- var DESIGN_H = 900;
309
- var MARGIN_X = 164;
310
- var MARGIN_Y = 196;
311
- var TABLE_RX = DESIGN_W / 2 - MARGIN_X;
312
- var TABLE_RY = DESIGN_H / 2 - MARGIN_Y;
313
- var FELT_GAP = 11;
314
- var BET_SCALE = 0.72;
315
- var BET_RX = TABLE_RX * BET_SCALE;
316
- var BET_RY = TABLE_RY * BET_SCALE;
317
- var CONFETTI_COLORS = ["#f39c12", "#e74c3c", "#2ecc71", "#3498db", "#f1c40f", "#9b59b6"];
318
- function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
319
- const rad = angleDeg * Math.PI / 180;
320
- return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
321
- }
322
- function calculateSeatLayouts(seatCount, yourSeatIndex, seatWidth, seatHeight) {
323
- const cx = DESIGN_W / 2;
324
- const cy = DESIGN_H / 2;
325
- const ringX = TABLE_RX - FELT_GAP + seatWidth / 2 + 6;
326
- const ringY = TABLE_RY - FELT_GAP + seatHeight / 2 + 6;
327
- const step = 360 / seatCount;
328
- const angleOf = (i) => -90 + i * step;
329
- const rotationOffset = yourSeatIndex !== null ? 90 - angleOf(yourSeatIndex) : 0;
330
- const layouts = [];
331
- for (let i = 0; i < seatCount; i++) {
332
- const pt = getEllipsePoint(cx, cy, ringX, ringY, angleOf(i) + rotationOffset);
333
- layouts.push({
334
- x: pt.x - seatWidth / 2,
335
- y: pt.y - seatHeight / 2,
336
- index: i,
337
- isYou: i === yourSeatIndex
338
- });
339
- }
340
- return layouts;
341
- }
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"];
342
789
  function CardTable({
343
790
  seatCount = 6,
344
791
  seats,
345
792
  renderSeat,
793
+ bets,
794
+ renderBet,
795
+ betRadiusScale = DEFAULT_BET_SCALE,
796
+ startAngle = 90,
346
797
  yourSeatIndex = null,
347
798
  center,
348
799
  ambient = true,
349
800
  yourTurn = false,
350
801
  winnerSeats = [],
351
802
  confetti = true,
352
- seatWidth = 160,
353
- 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",
354
813
  className
355
814
  }) {
356
- const count = Math.max(2, Math.min(10, Math.floor(seatCount)));
357
- const outerRef = (0, import_react2.useRef)(null);
358
- const [scale, setScale] = (0, import_react2.useState)(1);
359
- const layouts = (0, import_react2.useMemo)(
360
- () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),
361
- [count, yourSeatIndex, seatWidth, seatHeight]
362
- );
363
- (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)(() => {
364
820
  const el = outerRef.current;
365
821
  if (!el) return;
366
822
  const compute = () => {
367
823
  const w = el.clientWidth;
368
824
  const h = el.clientHeight;
369
825
  if (!w || !h) return;
370
- setScale(Math.min(w / DESIGN_W, h / DESIGN_H));
826
+ setBox({ w, h });
371
827
  };
372
828
  compute();
373
829
  const ro = new ResizeObserver(compute);
374
830
  ro.observe(el);
375
831
  return () => ro.disconnect();
376
832
  }, []);
377
- const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
378
- const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
379
- const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
380
- const ringInset = { inset: `${MARGIN_Y + 2}px ${MARGIN_X + 2}px` };
381
- const betLineInset = {
382
- inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
383
- };
384
- 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)(
385
884
  "div",
386
885
  {
387
886
  ref: outerRef,
388
887
  className: cn("relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden", className),
389
- 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)(
390
889
  "div",
391
890
  {
392
891
  className: "relative",
393
- 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" },
394
893
  children: [
395
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
894
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
396
895
  "div",
397
896
  {
398
- 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",
399
898
  "aria-hidden": "true"
400
899
  }
401
900
  ),
402
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
901
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
403
902
  "div",
404
903
  {
405
- 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)]",
406
905
  style: tableFrameInset,
407
906
  "aria-hidden": "true"
408
907
  }
409
908
  ),
410
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
909
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
411
910
  "div",
412
911
  {
413
- 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)]",
414
913
  style: feltInset,
415
914
  "aria-hidden": "true"
416
915
  }
417
916
  ),
418
- ambient && /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(import_jsx_runtime4.Fragment, { children: [
419
- /* @__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)(
420
919
  "div",
421
920
  {
422
- 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",
423
922
  style: feltInset,
424
923
  "aria-hidden": "true"
425
924
  }
426
925
  ),
427
- /* @__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%]" }) })
428
927
  ] }),
429
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
928
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
430
929
  "div",
431
930
  {
931
+ "data-part": "betting-line",
432
932
  className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
433
933
  style: betLineInset,
434
934
  "aria-hidden": "true"
435
935
  }
436
936
  ),
437
- /* @__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" }) }),
438
- 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)(
439
939
  "div",
440
940
  {
441
941
  className: "pointer-events-none absolute z-10 animate-turn-glow rounded-[50%]",
@@ -443,18 +943,46 @@ function CardTable({
443
943
  "aria-hidden": "true"
444
944
  }
445
945
  ),
446
- 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 }),
447
- /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute inset-0 z-20", children: layouts.map(({ x, y, index, isYou }) => {
448
- const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou) : null;
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;
949
+ const isYou = index === yourSeatIndex;
950
+ const box2 = { index, isYou, x, y, width: seatW, height: seatH };
951
+ const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou, box2) : null;
449
952
  if (content === null) return null;
450
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: content }, 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 }) => {
966
+ const content = bets ? bets[index] ?? null : renderBet ? renderBet(index) : null;
967
+ if (content === null || content === void 0) return null;
968
+ const pt = ellipsePoint(cx, cy, betRx, betRy, seatAngle(index, count, startAngle));
969
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
970
+ "div",
971
+ {
972
+ "data-seat-bet": index,
973
+ className: "absolute",
974
+ style: { left: `${pt.x}px`, top: `${pt.y}px`, transform: "translate(-50%, -50%)" },
975
+ children: content
976
+ },
977
+ index
978
+ );
451
979
  }) }),
452
- 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) => {
453
981
  const w = 5 + i % 4 * 2;
454
982
  const h = 9 + i % 3 * 4;
455
983
  const dur = 1.7 + i % 5 * 0.28;
456
984
  const delay = i % 12 * 0.14;
457
- return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
985
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
458
986
  "span",
459
987
  {
460
988
  className: "absolute top-[26%] animate-confetti rounded-[2px]",
@@ -462,7 +990,7 @@ function CardTable({
462
990
  left: `${i * 37 % 96}%`,
463
991
  width: w,
464
992
  height: h,
465
- backgroundColor: CONFETTI_COLORS[i % CONFETTI_COLORS.length],
993
+ backgroundColor: confettiColors[i % confettiColors.length],
466
994
  animationDuration: `${dur}s`,
467
995
  animationDelay: `${delay}s`,
468
996
  animationIterationCount: "infinite"
@@ -471,7 +999,7 @@ function CardTable({
471
999
  i
472
1000
  );
473
1001
  }) }),
474
- /* @__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" })
475
1003
  ]
476
1004
  }
477
1005
  ) })
@@ -481,18 +1009,61 @@ function CardTable({
481
1009
  // Annotate the CommonJS export names for ESM import in node:
482
1010
  0 && (module.exports = {
483
1011
  BB,
1012
+ COUNT_UP_MS,
484
1013
  CardBack,
485
1014
  CardTable,
1015
+ Chip,
1016
+ ChipStack,
486
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,
487
1031
  HoleCards,
488
1032
  PlayingCard,
489
1033
  PokerTable,
490
1034
  Pot,
1035
+ RIM_GAP,
1036
+ SEAT_SHRINK_FLOOR,
1037
+ SIZES,
491
1038
  Seat,
1039
+ chipBreakdown,
1040
+ chipCount,
1041
+ chipSpotColor,
1042
+ chipsTotal,
492
1043
  cn,
1044
+ contrastRatio,
1045
+ describeCard,
1046
+ ellipseBox,
1047
+ ellipsePoint,
1048
+ fitSeatScale,
1049
+ flatten,
493
1050
  formatBB,
1051
+ getRankName,
1052
+ getSuitName,
494
1053
  getSuitSymbol,
1054
+ isKnownSuit,
1055
+ isRedSuit,
1056
+ layoutSeats,
1057
+ minSeatSeparation,
1058
+ mostVisible,
495
1059
  nextSeatIndex,
496
- parseCard
1060
+ parseCard,
1061
+ parseColor,
1062
+ relativeLuminance,
1063
+ ringPoints,
1064
+ seatAngle,
1065
+ seatPoint,
1066
+ seatRingRadii,
1067
+ tableRadii
497
1068
  });
498
1069
  //# sourceMappingURL=index.cjs.map