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