@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/README.md +182 -13
- package/dist/index.cjs +708 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +499 -23
- package/dist/index.d.ts +499 -23
- package/dist/index.js +661 -168
- package/dist/index.js.map +1 -1
- package/package.json +27 -7
- package/src/styles.css +128 -95
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
|
|
13
|
-
return `${Number.isInteger(
|
|
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
|
|
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
|
-
|
|
108
|
-
|
|
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(
|
|
127
|
-
|
|
128
|
-
|
|
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(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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-
|
|
163
|
-
"border-2 border-
|
|
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) =>
|
|
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 *
|
|
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/
|
|
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
|
|
207
|
-
height
|
|
609
|
+
width,
|
|
610
|
+
height,
|
|
208
611
|
className,
|
|
209
612
|
frameClassName,
|
|
210
613
|
frameStyle
|
|
211
614
|
}) {
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
/* @__PURE__ */
|
|
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-
|
|
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__ */
|
|
241
|
-
/* @__PURE__ */
|
|
242
|
-
footer !== void 0 && /* @__PURE__ */
|
|
243
|
-
overlay !== void 0 && /* @__PURE__ */
|
|
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__ */
|
|
248
|
-
below !== void 0 && /* @__PURE__ */
|
|
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
|
|
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
|
|
668
|
+
const shownRef = useRef(value);
|
|
258
669
|
useEffect(() => {
|
|
259
|
-
const from =
|
|
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
|
|
264
|
-
|
|
679
|
+
const dur = COUNT_UP_MS;
|
|
680
|
+
let origin = null;
|
|
265
681
|
const tick = (now) => {
|
|
266
|
-
|
|
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
|
-
|
|
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__ */
|
|
693
|
+
return /* @__PURE__ */ jsx4(Fragment, { children: format(shown) });
|
|
276
694
|
}
|
|
277
|
-
function Pot({ amount, label, format =
|
|
278
|
-
return /* @__PURE__ */
|
|
279
|
-
label && /* @__PURE__ */
|
|
280
|
-
/* @__PURE__ */
|
|
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
|
|
287
|
-
var
|
|
288
|
-
var
|
|
289
|
-
var
|
|
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 =
|
|
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
|
-
|
|
329
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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__ */
|
|
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:
|
|
811
|
+
style: { width: designW, height: designH, transform: `scale(${scale})`, transformOrigin: "top left" },
|
|
373
812
|
children: [
|
|
374
|
-
/* @__PURE__ */
|
|
813
|
+
/* @__PURE__ */ jsx5(
|
|
375
814
|
"div",
|
|
376
815
|
{
|
|
377
|
-
className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,
|
|
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__ */
|
|
820
|
+
/* @__PURE__ */ jsx5(
|
|
382
821
|
"div",
|
|
383
822
|
{
|
|
384
|
-
className: "absolute rounded-[50%] bg-gradient-to-b from-
|
|
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__ */
|
|
828
|
+
/* @__PURE__ */ jsx5(
|
|
390
829
|
"div",
|
|
391
830
|
{
|
|
392
|
-
className: "absolute rounded-[50%] bg-[radial-gradient(ellipse_at_50%_32
|
|
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__ */
|
|
398
|
-
/* @__PURE__ */
|
|
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,
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
417
|
-
yourTurn && /* @__PURE__ */
|
|
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__ */
|
|
426
|
-
/* @__PURE__ */
|
|
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
|
|
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__ */
|
|
431
|
-
|
|
432
|
-
|
|
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(
|
|
436
|
-
return /* @__PURE__ */
|
|
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__ */
|
|
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__ */
|
|
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:
|
|
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__ */
|
|
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
|