@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/README.md +205 -12
- package/dist/index.cjs +746 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -23
- package/dist/index.d.ts +547 -23
- package/dist/index.js +696 -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;
|
|
@@ -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
|
-
|
|
90
|
-
|
|
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(
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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-
|
|
145
|
-
"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)]",
|
|
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) =>
|
|
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 *
|
|
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/
|
|
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
|
|
189
|
-
height
|
|
609
|
+
width,
|
|
610
|
+
height,
|
|
190
611
|
className,
|
|
191
612
|
frameClassName,
|
|
192
613
|
frameStyle
|
|
193
614
|
}) {
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
/* @__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(
|
|
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-
|
|
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__ */
|
|
223
|
-
/* @__PURE__ */
|
|
224
|
-
footer !== void 0 && /* @__PURE__ */
|
|
225
|
-
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 })
|
|
226
650
|
]
|
|
227
651
|
}
|
|
228
652
|
),
|
|
229
|
-
badge !== void 0 && /* @__PURE__ */
|
|
230
|
-
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 })
|
|
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
|
|
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
|
|
668
|
+
const shownRef = useRef(value);
|
|
240
669
|
useEffect(() => {
|
|
241
|
-
const from =
|
|
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
|
|
246
|
-
|
|
679
|
+
const dur = COUNT_UP_MS;
|
|
680
|
+
let origin = null;
|
|
247
681
|
const tick = (now) => {
|
|
248
|
-
|
|
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
|
-
|
|
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__ */
|
|
693
|
+
return /* @__PURE__ */ jsx4(Fragment, { children: format(shown) });
|
|
258
694
|
}
|
|
259
|
-
function Pot({ amount, label, format =
|
|
260
|
-
return /* @__PURE__ */
|
|
261
|
-
label && /* @__PURE__ */
|
|
262
|
-
/* @__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) })
|
|
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
|
|
269
|
-
var
|
|
270
|
-
var
|
|
271
|
-
var
|
|
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
|
-
|
|
315
|
-
|
|
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
|
|
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 [
|
|
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
|
-
|
|
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
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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__ */
|
|
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:
|
|
811
|
+
style: { width: designW, height: designH, transform: `scale(${scale})`, transformOrigin: "top left" },
|
|
356
812
|
children: [
|
|
357
|
-
/* @__PURE__ */
|
|
813
|
+
/* @__PURE__ */ jsx5(
|
|
358
814
|
"div",
|
|
359
815
|
{
|
|
360
|
-
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",
|
|
361
817
|
"aria-hidden": "true"
|
|
362
818
|
}
|
|
363
819
|
),
|
|
364
|
-
/* @__PURE__ */
|
|
820
|
+
/* @__PURE__ */ jsx5(
|
|
365
821
|
"div",
|
|
366
822
|
{
|
|
367
|
-
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)]",
|
|
368
824
|
style: tableFrameInset,
|
|
369
825
|
"aria-hidden": "true"
|
|
370
826
|
}
|
|
371
827
|
),
|
|
372
|
-
/* @__PURE__ */
|
|
828
|
+
/* @__PURE__ */ jsx5(
|
|
373
829
|
"div",
|
|
374
830
|
{
|
|
375
|
-
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)]",
|
|
376
832
|
style: feltInset,
|
|
377
833
|
"aria-hidden": "true"
|
|
378
834
|
}
|
|
379
835
|
),
|
|
380
|
-
ambient && /* @__PURE__ */
|
|
381
|
-
/* @__PURE__ */
|
|
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,
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
400
|
-
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(
|
|
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__ */
|
|
409
|
-
/* @__PURE__ */
|
|
410
|
-
const
|
|
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__ */
|
|
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__ */
|
|
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__ */
|
|
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:
|
|
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__ */
|
|
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
|