@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.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;
@@ -27,6 +157,130 @@ function nextSeatIndex(current, seatCount, opts = {}) {
27
157
  return start;
28
158
  }
29
159
 
160
+ // src/geometry.ts
161
+ function normCount(count) {
162
+ const n = Math.floor(count);
163
+ return Number.isFinite(n) && n > 0 ? n : 1;
164
+ }
165
+ function seatAngle(index, seatCount, startAngle = 90) {
166
+ const n = normCount(seatCount);
167
+ return startAngle + index * 360 / n;
168
+ }
169
+ function ellipsePoint(cx, cy, rx, ry, angleDeg) {
170
+ const rad = angleDeg * Math.PI / 180;
171
+ return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
172
+ }
173
+ function ringPoints(count, cx, cy, rx, ry, startAngle = 90) {
174
+ const n = normCount(count);
175
+ return Array.from({ length: n }, (_, i) => ellipsePoint(cx, cy, rx, ry, seatAngle(i, n, startAngle)));
176
+ }
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
+
30
284
  // src/components/PlayingCard.tsx
31
285
  import { jsx, jsxs } from "react/jsx-runtime";
32
286
  var SIZE_CLASSES = {
@@ -67,82 +321,77 @@ var BACK_EMBLEM_SIZE = {
67
321
  md: 28,
68
322
  lg: 42
69
323
  };
70
- function parseCard(card) {
71
- if (!card || card.length < 2) return { rank: "", suit: "" };
72
- return { rank: card.slice(0, -1), suit: card.slice(-1) };
73
- }
74
- function getSuitSymbol(suit) {
75
- switch (suit) {
76
- case "s":
77
- return "\u2660";
78
- case "h":
79
- return "\u2665";
80
- case "d":
81
- return "\u2666";
82
- case "c":
83
- return "\u2663";
84
- default:
85
- return suit;
86
- }
87
- }
88
324
  function getSuitClass(suit) {
89
- switch (suit) {
90
- case "s":
91
- case "c":
92
- return "text-neutral-900";
93
- case "h":
94
- case "d":
95
- return "text-red-600";
96
- default:
97
- return "";
98
- }
325
+ if (isRedSuit(suit)) return "text-red-600";
326
+ return isKnownSuit(suit) ? "text-neutral-900" : "";
99
327
  }
100
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)]";
101
- function PlayingCard({ card, size = "md", className }) {
102
- 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);
103
332
  const { rank, suit } = parseCard(card);
104
333
  const suitSymbol = getSuitSymbol(suit);
105
334
  const suitClass = getSuitClass(suit);
106
335
  const large = size === "md" || size === "lg";
107
336
  if (!large) {
108
- 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: [
109
- /* @__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 }) }),
110
- /* @__PURE__ */ jsx("div", { className: "flex flex-1 items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: SMALL_SUIT[size], children: suitSymbol }) })
111
- ] }) });
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
+ );
112
350
  }
113
- return /* @__PURE__ */ jsxs("div", { className: cn(SIZE_CLASSES[size], CARD_FACE, className), style: { aspectRatio: "10/14" }, children: [
114
- /* @__PURE__ */ jsxs("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
115
- /* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
116
- /* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
117
- ] }),
118
- /* @__PURE__ */ jsxs("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
119
- /* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
120
- /* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
121
- ] }),
122
- /* @__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 }) })
123
- ] });
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
+ );
124
371
  }
125
372
  function CardBackEmblem({ size }) {
126
373
  const s = BACK_EMBLEM_SIZE[size];
127
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: [
128
- /* @__PURE__ */ jsxs("g", { fill: "#f0c04a", opacity: "0.92", children: [
375
+ /* @__PURE__ */ jsxs("g", { fill: "var(--color-card-back-emblem, #f0c04a)", opacity: "0.92", children: [
129
376
  /* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "11", rx: "4", ry: "7" }),
130
377
  /* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "29", rx: "4", ry: "7" }),
131
378
  /* @__PURE__ */ jsx("ellipse", { cx: "11", cy: "20", rx: "7", ry: "4" }),
132
379
  /* @__PURE__ */ jsx("ellipse", { cx: "29", cy: "20", rx: "7", ry: "4" })
133
380
  ] }),
134
- /* @__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)" })
135
382
  ] });
136
383
  }
137
- function CardBack({ size = "md", className }) {
384
+ function CardBack({ size = "md", label = "face-down card", className }) {
138
385
  return /* @__PURE__ */ jsx(
139
386
  "div",
140
387
  {
388
+ role: "img",
389
+ "aria-label": label,
141
390
  className: cn(
142
391
  SIZE_CLASSES[size],
143
392
  "relative rounded-md p-[3px] overflow-hidden",
144
- "bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]",
145
- "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)]",
146
395
  className
147
396
  ),
148
397
  style: { aspectRatio: "10/14" },
@@ -150,8 +399,23 @@ function CardBack({ size = "md", className }) {
150
399
  }
151
400
  );
152
401
  }
153
- function HoleCards({ cards, size = "lg", className, deal = true }) {
154
- 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
+ )) });
155
419
  }
156
420
  function CommunityCards({ cards, size = "md", className, animated = true, slots }) {
157
421
  const count = slots ?? cards.length;
@@ -160,14 +424,171 @@ function CommunityCards({ cards, size = "md", className, animated = true, slots
160
424
  "div",
161
425
  {
162
426
  className: cn("h-full w-full", animated && "animate-card-flip"),
163
- style: animated ? { animationDelay: `${i * 70}ms` } : void 0,
427
+ style: animated ? { animationDelay: `${i * FLIP_STAGGER_MS}ms` } : void 0,
164
428
  children: /* @__PURE__ */ jsx(PlayingCard, { card, size })
165
429
  }
166
430
  ) : null }, i)) });
167
431
  }
168
432
 
169
- // 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";
170
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";
171
592
  function Seat({
172
593
  isActive = false,
173
594
  isWinner = false,
@@ -185,21 +606,24 @@ function Seat({
185
606
  topLeft,
186
607
  topRight,
187
608
  topCenter,
188
- width = 160,
189
- height = 176,
609
+ width,
610
+ height,
190
611
  className,
191
612
  frameClassName,
192
613
  frameStyle
193
614
  }) {
194
- const activeClass = animated ? "border-amber-400 animate-turn-glow" : "border-amber-400 shadow-[0_0_18px_rgba(243,156,18,0.5)]";
195
- const winnerClass = animated ? "border-amber-400 animate-winner-glow" : "border-amber-400 shadow-[0_0_0_3px_rgba(243,156,18,0.9)]";
196
- 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;
197
- return /* @__PURE__ */ jsxs2("div", { className: cn("relative flex flex-col items-center", className), style: { width }, children: [
198
- topCenter !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1", children: topCenter }),
199
- effTopLeft !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-3 -left-2 z-40", children: effTopLeft }),
200
- topRight !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-2 -right-2 z-40", children: topRight }),
201
- above !== void 0 && /* @__PURE__ */ jsx2("div", { className: "pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2", children: above }),
202
- /* @__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(
203
627
  "div",
204
628
  {
205
629
  role: onClick ? "button" : void 0,
@@ -208,120 +632,109 @@ function Seat({
208
632
  onKeyDown: onClick ? (e) => {
209
633
  if (e.key === "Enter" || e.key === " ") onClick();
210
634
  } : void 0,
211
- style: { width, height, ...frameStyle },
635
+ style: { width: frameWidth, height: frameHeight, ...frameStyle },
212
636
  className: cn(
213
637
  "relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition",
214
638
  "border-neutral-700",
215
639
  isActive && activeClass,
216
640
  isWinner && winnerClass,
217
641
  isDimmed && "opacity-50",
218
- 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",
219
643
  frameClassName
220
644
  ),
221
645
  children: [
222
- header !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full shrink-0 items-center justify-between gap-1 text-[11px]", children: header }),
223
- /* @__PURE__ */ jsx2("div", { className: "flex min-h-0 flex-1 items-center justify-center", children }),
224
- footer !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300", children: footer }),
225
- 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 })
226
650
  ]
227
651
  }
228
652
  ),
229
- badge !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute top-0 right-0 z-40", children: badge }),
230
- 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 })
231
655
  ] });
232
656
  }
233
657
 
234
658
  // src/components/Pot.tsx
235
659
  import { useEffect, useRef, useState } from "react";
236
- 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
+ }
237
666
  function CountUp({ value, format }) {
238
667
  const [shown, setShown] = useState(value);
239
- const fromRef = useRef(value);
668
+ const shownRef = useRef(value);
240
669
  useEffect(() => {
241
- const from = fromRef.current;
670
+ const from = shownRef.current;
242
671
  const to = value;
243
672
  if (from === to) return;
673
+ if (prefersReducedMotion()) {
674
+ shownRef.current = to;
675
+ setShown(to);
676
+ return;
677
+ }
244
678
  let raf = 0;
245
- const start = performance.now();
246
- const dur = 500;
679
+ const dur = COUNT_UP_MS;
680
+ let origin = null;
247
681
  const tick = (now) => {
248
- 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));
249
684
  const eased = 1 - Math.pow(1 - t, 3);
250
- setShown(Math.round(from + (to - from) * eased));
685
+ const next = Math.round(from + (to - from) * eased);
686
+ shownRef.current = next;
687
+ setShown(next);
251
688
  if (t < 1) raf = requestAnimationFrame(tick);
252
- else fromRef.current = to;
253
689
  };
254
690
  raf = requestAnimationFrame(tick);
255
691
  return () => cancelAnimationFrame(raf);
256
692
  }, [value]);
257
- return /* @__PURE__ */ jsx3(Fragment, { children: format(shown) });
693
+ return /* @__PURE__ */ jsx4(Fragment, { children: format(shown) });
258
694
  }
259
- function Pot({ amount, label, format = formatBB, animate = true, className }) {
260
- return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-2 font-mono text-sm text-amber-400", className), children: [
261
- label && /* @__PURE__ */ jsx3("span", { className: "text-neutral-400", children: label }),
262
- /* @__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) })
263
699
  ] });
264
700
  }
265
701
 
266
702
  // src/components/CardTable.tsx
267
703
  import { useRef as useRef2, useLayoutEffect, useState as useState2, useMemo } from "react";
268
- import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
269
- var DESIGN_W = 1200;
270
- var DESIGN_H = 900;
271
- var MARGIN_X = 164;
272
- var MARGIN_Y = 196;
273
- var TABLE_RX = DESIGN_W / 2 - MARGIN_X;
274
- var TABLE_RY = DESIGN_H / 2 - MARGIN_Y;
275
- var FELT_GAP = 11;
276
- var BET_SCALE = 0.72;
277
- var BET_RX = TABLE_RX * BET_SCALE;
278
- var BET_RY = TABLE_RY * BET_SCALE;
279
- var CONFETTI_COLORS = ["#f39c12", "#e74c3c", "#2ecc71", "#3498db", "#f1c40f", "#9b59b6"];
280
- function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
281
- const rad = angleDeg * Math.PI / 180;
282
- return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
283
- }
284
- function calculateSeatLayouts(seatCount, yourSeatIndex, seatWidth, seatHeight) {
285
- const cx = DESIGN_W / 2;
286
- const cy = DESIGN_H / 2;
287
- const ringX = TABLE_RX - FELT_GAP + seatWidth / 2 + 6;
288
- const ringY = TABLE_RY - FELT_GAP + seatHeight / 2 + 6;
289
- const step = 360 / seatCount;
290
- const angleOf = (i) => -90 + i * step;
291
- const rotationOffset = yourSeatIndex !== null ? 90 - angleOf(yourSeatIndex) : 0;
292
- const layouts = [];
293
- for (let i = 0; i < seatCount; i++) {
294
- const pt = getEllipsePoint(cx, cy, ringX, ringY, angleOf(i) + rotationOffset);
295
- layouts.push({
296
- x: pt.x - seatWidth / 2,
297
- y: pt.y - seatHeight / 2,
298
- index: i,
299
- isYou: i === yourSeatIndex
300
- });
301
- }
302
- return layouts;
303
- }
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"];
304
708
  function CardTable({
305
709
  seatCount = 6,
306
710
  seats,
307
711
  renderSeat,
712
+ bets,
713
+ renderBet,
714
+ betRadiusScale = DEFAULT_BET_SCALE,
715
+ startAngle = 90,
308
716
  yourSeatIndex = null,
309
717
  center,
310
718
  ambient = true,
311
719
  yourTurn = false,
312
720
  winnerSeats = [],
313
721
  confetti = true,
314
- seatWidth = 160,
315
- 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",
316
732
  className
317
733
  }) {
318
- 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;
319
736
  const outerRef = useRef2(null);
320
- const [scale, setScale] = useState2(1);
321
- const layouts = useMemo(
322
- () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),
323
- [count, yourSeatIndex, seatWidth, seatHeight]
324
- );
737
+ const [box, setBox] = useState2({ w: 0, h: 0 });
325
738
  useLayoutEffect(() => {
326
739
  const el = outerRef.current;
327
740
  if (!el) return;
@@ -329,75 +742,119 @@ function CardTable({
329
742
  const w = el.clientWidth;
330
743
  const h = el.clientHeight;
331
744
  if (!w || !h) return;
332
- setScale(Math.min(w / DESIGN_W, h / DESIGN_H));
745
+ setBox({ w, h });
333
746
  };
334
747
  compute();
335
748
  const ro = new ResizeObserver(compute);
336
749
  ro.observe(el);
337
750
  return () => ro.disconnect();
338
751
  }, []);
339
- const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
340
- const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
341
- const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
342
- const ringInset = { inset: `${MARGIN_Y + 2}px ${MARGIN_X + 2}px` };
343
- const betLineInset = {
344
- inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
345
- };
346
- 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(
347
803
  "div",
348
804
  {
349
805
  ref: outerRef,
350
806
  className: cn("relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden", className),
351
- 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(
352
808
  "div",
353
809
  {
354
810
  className: "relative",
355
- 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" },
356
812
  children: [
357
- /* @__PURE__ */ jsx4(
813
+ /* @__PURE__ */ jsx5(
358
814
  "div",
359
815
  {
360
- 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",
361
817
  "aria-hidden": "true"
362
818
  }
363
819
  ),
364
- /* @__PURE__ */ jsx4(
820
+ /* @__PURE__ */ jsx5(
365
821
  "div",
366
822
  {
367
- 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)]",
368
824
  style: tableFrameInset,
369
825
  "aria-hidden": "true"
370
826
  }
371
827
  ),
372
- /* @__PURE__ */ jsx4(
828
+ /* @__PURE__ */ jsx5(
373
829
  "div",
374
830
  {
375
- 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)]",
376
832
  style: feltInset,
377
833
  "aria-hidden": "true"
378
834
  }
379
835
  ),
380
- ambient && /* @__PURE__ */ jsxs4(Fragment2, { children: [
381
- /* @__PURE__ */ jsx4(
836
+ ambient && /* @__PURE__ */ jsxs5(Fragment2, { children: [
837
+ /* @__PURE__ */ jsx5(
382
838
  "div",
383
839
  {
384
- 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",
385
841
  style: feltInset,
386
842
  "aria-hidden": "true"
387
843
  }
388
844
  ),
389
- /* @__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%]" }) })
390
846
  ] }),
391
- /* @__PURE__ */ jsx4(
847
+ /* @__PURE__ */ jsx5(
392
848
  "div",
393
849
  {
850
+ "data-part": "betting-line",
394
851
  className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
395
852
  style: betLineInset,
396
853
  "aria-hidden": "true"
397
854
  }
398
855
  ),
399
- /* @__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" }) }),
400
- 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(
401
858
  "div",
402
859
  {
403
860
  className: "pointer-events-none absolute z-10 animate-turn-glow rounded-[50%]",
@@ -405,18 +862,46 @@ function CardTable({
405
862
  "aria-hidden": "true"
406
863
  }
407
864
  ),
408
- center !== void 0 && /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center", children: center }),
409
- /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 z-20", children: layouts.map(({ x, y, index, isYou }) => {
410
- const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou) : null;
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;
868
+ const isYou = index === yourSeatIndex;
869
+ const box2 = { index, isYou, x, y, width: seatW, height: seatH };
870
+ const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou, box2) : null;
411
871
  if (content === null) return null;
412
- return /* @__PURE__ */ jsx4("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: content }, 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 }) => {
885
+ const content = bets ? bets[index] ?? null : renderBet ? renderBet(index) : null;
886
+ if (content === null || content === void 0) return null;
887
+ const pt = ellipsePoint(cx, cy, betRx, betRy, seatAngle(index, count, startAngle));
888
+ return /* @__PURE__ */ jsx5(
889
+ "div",
890
+ {
891
+ "data-seat-bet": index,
892
+ className: "absolute",
893
+ style: { left: `${pt.x}px`, top: `${pt.y}px`, transform: "translate(-50%, -50%)" },
894
+ children: content
895
+ },
896
+ index
897
+ );
413
898
  }) }),
414
- 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) => {
415
900
  const w = 5 + i % 4 * 2;
416
901
  const h = 9 + i % 3 * 4;
417
902
  const dur = 1.7 + i % 5 * 0.28;
418
903
  const delay = i % 12 * 0.14;
419
- return /* @__PURE__ */ jsx4(
904
+ return /* @__PURE__ */ jsx5(
420
905
  "span",
421
906
  {
422
907
  className: "absolute top-[26%] animate-confetti rounded-[2px]",
@@ -424,7 +909,7 @@ function CardTable({
424
909
  left: `${i * 37 % 96}%`,
425
910
  width: w,
426
911
  height: h,
427
- backgroundColor: CONFETTI_COLORS[i % CONFETTI_COLORS.length],
912
+ backgroundColor: confettiColors[i % confettiColors.length],
428
913
  animationDuration: `${dur}s`,
429
914
  animationDelay: `${delay}s`,
430
915
  animationIterationCount: "infinite"
@@ -433,7 +918,7 @@ function CardTable({
433
918
  i
434
919
  );
435
920
  }) }),
436
- /* @__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" })
437
922
  ]
438
923
  }
439
924
  ) })
@@ -442,18 +927,61 @@ function CardTable({
442
927
  }
443
928
  export {
444
929
  BB,
930
+ COUNT_UP_MS,
445
931
  CardBack,
446
932
  CardTable,
933
+ Chip,
934
+ ChipStack,
447
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,
448
949
  HoleCards,
449
950
  PlayingCard,
450
951
  CardTable as PokerTable,
451
952
  Pot,
953
+ RIM_GAP,
954
+ SEAT_SHRINK_FLOOR,
955
+ SIZES,
452
956
  Seat,
957
+ chipBreakdown,
958
+ chipCount,
959
+ chipSpotColor,
960
+ chipsTotal,
453
961
  cn,
962
+ contrastRatio,
963
+ describeCard,
964
+ ellipseBox,
965
+ ellipsePoint,
966
+ fitSeatScale,
967
+ flatten,
454
968
  formatBB,
969
+ getRankName,
970
+ getSuitName,
455
971
  getSuitSymbol,
972
+ isKnownSuit,
973
+ isRedSuit,
974
+ layoutSeats,
975
+ minSeatSeparation,
976
+ mostVisible,
456
977
  nextSeatIndex,
457
- parseCard
978
+ parseCard,
979
+ parseColor,
980
+ relativeLuminance,
981
+ ringPoints,
982
+ seatAngle,
983
+ seatPoint,
984
+ seatRingRadii,
985
+ tableRadii
458
986
  };
459
987
  //# sourceMappingURL=index.js.map