@stargazeryan/poker-ui 0.0.0-stage → 0.2.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.
@@ -0,0 +1,130 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare function cn(...parts: Array<string | false | null | undefined>): string;
5
+
6
+ /** Big Blind value used for stack display. */
7
+ declare const BB = 20;
8
+ /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
9
+ declare function formatBB(amount: number): string;
10
+
11
+ /**
12
+ * Seat-ring helpers. Pure functions — they contain no game-specific rules.
13
+ */
14
+ interface NextSeatOptions {
15
+ /** +1 for clockwise, -1 for counter-clockwise. Default +1. */
16
+ dir?: 1 | -1;
17
+ /** Return true to skip a seat (e.g. folded / empty / out). */
18
+ skip?: (index: number) => boolean;
19
+ }
20
+ /**
21
+ * Return the index of the next seat on the ring after `current`, skipping any
22
+ * seat matched by `opts.skip`. Wraps around. Returns `current` if every other
23
+ * seat is skipped.
24
+ */
25
+ declare function nextSeatIndex(current: number, seatCount: number, opts?: NextSeatOptions): number;
26
+
27
+ type CardSize = "xs" | "sm" | "md" | "lg";
28
+ /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
29
+ declare function parseCard(card: string): {
30
+ rank: string;
31
+ suit: string;
32
+ };
33
+ declare function getSuitSymbol(suit: string): string;
34
+ interface PlayingCardProps {
35
+ /** Compact card code ("As", "10h") or null to render a face-down card back. */
36
+ card: string | null;
37
+ size?: CardSize;
38
+ className?: string;
39
+ }
40
+ /**
41
+ * A single playing card. Size is driven by the `size` prop (never viewport
42
+ * breakpoints) so cards scale predictably inside any container.
43
+ */
44
+ declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
45
+ interface CardBackProps {
46
+ size?: CardSize;
47
+ className?: string;
48
+ }
49
+ /** The face-down card back (dark red + gold rosette). */
50
+ declare function CardBack({ size, className }: CardBackProps): react.JSX.Element;
51
+ interface HoleCardsProps {
52
+ cards: (string | null)[];
53
+ size?: CardSize;
54
+ className?: string;
55
+ }
56
+ /** A row of a player's two hole cards. */
57
+ declare function HoleCards({ cards, size, className }: HoleCardsProps): react.JSX.Element;
58
+ interface CommunityCardsProps {
59
+ cards: (string | null)[];
60
+ size?: CardSize;
61
+ className?: string;
62
+ }
63
+ /** The five community cards, each dealt with a staggered flip animation. */
64
+ declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
65
+
66
+ interface SeatProps {
67
+ /** Turn/active highlight (generic). */
68
+ isActive?: boolean;
69
+ /** Winner highlight (generic). */
70
+ isWinner?: boolean;
71
+ /** Dimmed, e.g. folded or out (generic). */
72
+ isDimmed?: boolean;
73
+ /** Optional click handler (e.g. "sit here"). */
74
+ onClick?: () => void;
75
+ /** Slot: top row — name / badges. */
76
+ header?: ReactNode;
77
+ /** Slot: top-right floating element — e.g. a dealer button. */
78
+ badge?: ReactNode;
79
+ /** Slot: center content — 0..N cards, avatar, anything. */
80
+ children?: ReactNode;
81
+ /** Slot: bottom row — stack / bet / status. */
82
+ footer?: ReactNode;
83
+ /** Slot: overlay — e.g. an "All-in" label. */
84
+ overlay?: ReactNode;
85
+ width?: number;
86
+ height?: number;
87
+ className?: string;
88
+ }
89
+ /**
90
+ * A neutral seat: a frame with generic visual states and named slots.
91
+ * It knows nothing about any particular game — put whatever you want into
92
+ * `header` / `children` / `footer` / `badge` / `overlay`.
93
+ */
94
+ declare function Seat({ isActive, isWinner, isDimmed, onClick, header, badge, children, footer, overlay, width, height, className, }: SeatProps): react.JSX.Element;
95
+
96
+ interface PotProps {
97
+ amount: number;
98
+ /** Optional label, e.g. "Main" / "Side". */
99
+ label?: string;
100
+ /** Formatter for the amount; defaults to Big-Blind formatting. */
101
+ format?: (amount: number) => string;
102
+ className?: string;
103
+ }
104
+ /** A small, standalone pot pill. Optional — the table does not require it. */
105
+ declare function Pot({ amount, label, format, className }: PotProps): react.JSX.Element;
106
+
107
+ interface CardTableProps {
108
+ /** Number of seats (2–10). Default 6. */
109
+ seatCount?: number;
110
+ /** Optional per-seat content. Length should equal `seatCount`. */
111
+ seats?: (ReactNode | null)[];
112
+ /** Alternative to `seats`: render a seat by index. */
113
+ renderSeat?: (index: number, isYou: boolean) => ReactNode;
114
+ /** Which seat belongs to the viewer; rotates that seat to the bottom. */
115
+ yourSeatIndex?: number | null;
116
+ /** Optional content in the middle of the table (cards, pot, anything). */
117
+ center?: ReactNode;
118
+ /** Seat frame size used for layout. */
119
+ seatWidth?: number;
120
+ seatHeight?: number;
121
+ className?: string;
122
+ }
123
+ /**
124
+ * A responsive, game-agnostic card table. It scales a fixed 1200x900 design
125
+ * onto its container and arranges `seatCount` seats around the felt. Purely
126
+ * presentational: supply seat content and an optional `center` slot.
127
+ */
128
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
129
+
130
+ export { BB, CardBack, type CardBackProps, type CardSize, CardTable, type CardTableProps, CommunityCards, type CommunityCardsProps, HoleCards, type HoleCardsProps, type NextSeatOptions, PlayingCard, type PlayingCardProps, CardTable as PokerTable, type CardTableProps as PokerTableProps, Pot, type PotProps, Seat, type SeatProps, cn, formatBB, getSuitSymbol, nextSeatIndex, parseCard };
@@ -0,0 +1,130 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ declare function cn(...parts: Array<string | false | null | undefined>): string;
5
+
6
+ /** Big Blind value used for stack display. */
7
+ declare const BB = 20;
8
+ /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
9
+ declare function formatBB(amount: number): string;
10
+
11
+ /**
12
+ * Seat-ring helpers. Pure functions — they contain no game-specific rules.
13
+ */
14
+ interface NextSeatOptions {
15
+ /** +1 for clockwise, -1 for counter-clockwise. Default +1. */
16
+ dir?: 1 | -1;
17
+ /** Return true to skip a seat (e.g. folded / empty / out). */
18
+ skip?: (index: number) => boolean;
19
+ }
20
+ /**
21
+ * Return the index of the next seat on the ring after `current`, skipping any
22
+ * seat matched by `opts.skip`. Wraps around. Returns `current` if every other
23
+ * seat is skipped.
24
+ */
25
+ declare function nextSeatIndex(current: number, seatCount: number, opts?: NextSeatOptions): number;
26
+
27
+ type CardSize = "xs" | "sm" | "md" | "lg";
28
+ /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
29
+ declare function parseCard(card: string): {
30
+ rank: string;
31
+ suit: string;
32
+ };
33
+ declare function getSuitSymbol(suit: string): string;
34
+ interface PlayingCardProps {
35
+ /** Compact card code ("As", "10h") or null to render a face-down card back. */
36
+ card: string | null;
37
+ size?: CardSize;
38
+ className?: string;
39
+ }
40
+ /**
41
+ * A single playing card. Size is driven by the `size` prop (never viewport
42
+ * breakpoints) so cards scale predictably inside any container.
43
+ */
44
+ declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
45
+ interface CardBackProps {
46
+ size?: CardSize;
47
+ className?: string;
48
+ }
49
+ /** The face-down card back (dark red + gold rosette). */
50
+ declare function CardBack({ size, className }: CardBackProps): react.JSX.Element;
51
+ interface HoleCardsProps {
52
+ cards: (string | null)[];
53
+ size?: CardSize;
54
+ className?: string;
55
+ }
56
+ /** A row of a player's two hole cards. */
57
+ declare function HoleCards({ cards, size, className }: HoleCardsProps): react.JSX.Element;
58
+ interface CommunityCardsProps {
59
+ cards: (string | null)[];
60
+ size?: CardSize;
61
+ className?: string;
62
+ }
63
+ /** The five community cards, each dealt with a staggered flip animation. */
64
+ declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
65
+
66
+ interface SeatProps {
67
+ /** Turn/active highlight (generic). */
68
+ isActive?: boolean;
69
+ /** Winner highlight (generic). */
70
+ isWinner?: boolean;
71
+ /** Dimmed, e.g. folded or out (generic). */
72
+ isDimmed?: boolean;
73
+ /** Optional click handler (e.g. "sit here"). */
74
+ onClick?: () => void;
75
+ /** Slot: top row — name / badges. */
76
+ header?: ReactNode;
77
+ /** Slot: top-right floating element — e.g. a dealer button. */
78
+ badge?: ReactNode;
79
+ /** Slot: center content — 0..N cards, avatar, anything. */
80
+ children?: ReactNode;
81
+ /** Slot: bottom row — stack / bet / status. */
82
+ footer?: ReactNode;
83
+ /** Slot: overlay — e.g. an "All-in" label. */
84
+ overlay?: ReactNode;
85
+ width?: number;
86
+ height?: number;
87
+ className?: string;
88
+ }
89
+ /**
90
+ * A neutral seat: a frame with generic visual states and named slots.
91
+ * It knows nothing about any particular game — put whatever you want into
92
+ * `header` / `children` / `footer` / `badge` / `overlay`.
93
+ */
94
+ declare function Seat({ isActive, isWinner, isDimmed, onClick, header, badge, children, footer, overlay, width, height, className, }: SeatProps): react.JSX.Element;
95
+
96
+ interface PotProps {
97
+ amount: number;
98
+ /** Optional label, e.g. "Main" / "Side". */
99
+ label?: string;
100
+ /** Formatter for the amount; defaults to Big-Blind formatting. */
101
+ format?: (amount: number) => string;
102
+ className?: string;
103
+ }
104
+ /** A small, standalone pot pill. Optional — the table does not require it. */
105
+ declare function Pot({ amount, label, format, className }: PotProps): react.JSX.Element;
106
+
107
+ interface CardTableProps {
108
+ /** Number of seats (2–10). Default 6. */
109
+ seatCount?: number;
110
+ /** Optional per-seat content. Length should equal `seatCount`. */
111
+ seats?: (ReactNode | null)[];
112
+ /** Alternative to `seats`: render a seat by index. */
113
+ renderSeat?: (index: number, isYou: boolean) => ReactNode;
114
+ /** Which seat belongs to the viewer; rotates that seat to the bottom. */
115
+ yourSeatIndex?: number | null;
116
+ /** Optional content in the middle of the table (cards, pot, anything). */
117
+ center?: ReactNode;
118
+ /** Seat frame size used for layout. */
119
+ seatWidth?: number;
120
+ seatHeight?: number;
121
+ className?: string;
122
+ }
123
+ /**
124
+ * A responsive, game-agnostic card table. It scales a fixed 1200x900 design
125
+ * onto its container and arranges `seatCount` seats around the felt. Purely
126
+ * presentational: supply seat content and an optional `center` slot.
127
+ */
128
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
129
+
130
+ export { BB, CardBack, type CardBackProps, type CardSize, CardTable, type CardTableProps, CommunityCards, type CommunityCardsProps, HoleCards, type HoleCardsProps, type NextSeatOptions, PlayingCard, type PlayingCardProps, CardTable as PokerTable, type CardTableProps as PokerTableProps, Pot, type PotProps, Seat, type SeatProps, cn, formatBB, getSuitSymbol, nextSeatIndex, parseCard };
package/dist/index.js ADDED
@@ -0,0 +1,357 @@
1
+ "use client";
2
+
3
+ // src/cn.ts
4
+ function cn(...parts) {
5
+ return parts.filter(Boolean).join(" ");
6
+ }
7
+
8
+ // src/format.ts
9
+ var BB = 20;
10
+ function formatBB(amount) {
11
+ const bb = amount / BB;
12
+ return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;
13
+ }
14
+
15
+ // src/seat.ts
16
+ function nextSeatIndex(current, seatCount, opts = {}) {
17
+ const n = Math.max(1, Math.floor(seatCount));
18
+ const dir = opts.dir ?? 1;
19
+ const skip = opts.skip;
20
+ const start = (Math.floor(current) % n + n) % n;
21
+ let i = start;
22
+ for (let step = 0; step < n - 1; step++) {
23
+ i = (i + dir + n) % n;
24
+ if (!skip || !skip(i)) return i;
25
+ }
26
+ return start;
27
+ }
28
+
29
+ // src/components/PlayingCard.tsx
30
+ import { jsx, jsxs } from "react/jsx-runtime";
31
+ var SIZE_CLASSES = {
32
+ xs: "w-8 h-11",
33
+ sm: "w-10 h-14",
34
+ md: "w-16 h-22",
35
+ lg: "w-24 h-32"
36
+ };
37
+ var CORNER_RANK = {
38
+ xs: "text-[11px]",
39
+ sm: "text-sm",
40
+ md: "text-lg",
41
+ lg: "text-2xl"
42
+ };
43
+ var CORNER_SUIT = {
44
+ xs: "text-[9px]",
45
+ sm: "text-xs",
46
+ md: "text-sm",
47
+ lg: "text-base"
48
+ };
49
+ var CENTER_SUIT = {
50
+ xs: "text-base",
51
+ sm: "text-xl",
52
+ md: "text-3xl",
53
+ lg: "text-5xl"
54
+ };
55
+ var BACK_EMBLEM_SIZE = {
56
+ xs: 14,
57
+ sm: 18,
58
+ md: 28,
59
+ lg: 42
60
+ };
61
+ function parseCard(card) {
62
+ if (!card || card.length < 2) return { rank: "", suit: "" };
63
+ return { rank: card.slice(0, -1), suit: card.slice(-1) };
64
+ }
65
+ function getSuitSymbol(suit) {
66
+ switch (suit) {
67
+ case "s":
68
+ return "\u2660";
69
+ case "h":
70
+ return "\u2665";
71
+ case "d":
72
+ return "\u2666";
73
+ case "c":
74
+ return "\u2663";
75
+ default:
76
+ return suit;
77
+ }
78
+ }
79
+ function getSuitClass(suit) {
80
+ switch (suit) {
81
+ case "s":
82
+ case "c":
83
+ return "text-neutral-900";
84
+ case "h":
85
+ case "d":
86
+ return "text-red-600";
87
+ default:
88
+ return "";
89
+ }
90
+ }
91
+ function PlayingCard({ card, size = "md", className }) {
92
+ if (!card) return /* @__PURE__ */ jsx(CardBack, { size, className });
93
+ const { rank, suit } = parseCard(card);
94
+ const suitSymbol = getSuitSymbol(suit);
95
+ const suitClass = getSuitClass(suit);
96
+ const showCenter = size === "md" || size === "lg";
97
+ return /* @__PURE__ */ jsxs(
98
+ "div",
99
+ {
100
+ className: cn(
101
+ SIZE_CLASSES[size],
102
+ "relative bg-gradient-to-br from-white via-white to-neutral-100",
103
+ "rounded-md border border-neutral-300/90 overflow-hidden",
104
+ "shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]",
105
+ className
106
+ ),
107
+ style: { aspectRatio: "10/14" },
108
+ children: [
109
+ /* @__PURE__ */ jsxs("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
110
+ /* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
111
+ /* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
112
+ ] }),
113
+ /* @__PURE__ */ jsxs("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
114
+ /* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
115
+ /* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
116
+ ] }),
117
+ showCenter && /* @__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 }) })
118
+ ]
119
+ }
120
+ );
121
+ }
122
+ function CardBackEmblem({ size }) {
123
+ const s = BACK_EMBLEM_SIZE[size];
124
+ 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: [
125
+ /* @__PURE__ */ jsxs("g", { fill: "#f0c04a", opacity: "0.92", children: [
126
+ /* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "11", rx: "4", ry: "7" }),
127
+ /* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "29", rx: "4", ry: "7" }),
128
+ /* @__PURE__ */ jsx("ellipse", { cx: "11", cy: "20", rx: "7", ry: "4" }),
129
+ /* @__PURE__ */ jsx("ellipse", { cx: "29", cy: "20", rx: "7", ry: "4" })
130
+ ] }),
131
+ /* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "3.2", fill: "#5a0c18" })
132
+ ] });
133
+ }
134
+ function CardBack({ size = "md", className }) {
135
+ return /* @__PURE__ */ jsx(
136
+ "div",
137
+ {
138
+ className: cn(
139
+ SIZE_CLASSES[size],
140
+ "relative rounded-md p-[3px] overflow-hidden",
141
+ "bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]",
142
+ "border-2 border-[#c9a24b]/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]",
143
+ className
144
+ ),
145
+ style: { aspectRatio: "10/14" },
146
+ children: /* @__PURE__ */ jsx("div", { className: "h-full w-full rounded-[4px] border border-white/20 flex items-center justify-center bg-[repeating-linear-gradient(45deg,rgba(255,255,255,0.09)_0_6px,transparent_6px_12px),repeating-linear-gradient(-45deg,rgba(255,255,255,0.06)_0_6px,transparent_6px_12px)]", children: /* @__PURE__ */ jsx(CardBackEmblem, { size }) })
147
+ }
148
+ );
149
+ }
150
+ function HoleCards({ cards, size = "lg", className }) {
151
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-2", className), children: cards.map((card, i) => /* @__PURE__ */ jsx(PlayingCard, { card, size }, i)) });
152
+ }
153
+ function CommunityCards({ cards, size = "md", className }) {
154
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-1.5", className), children: cards.map((card, i) => /* @__PURE__ */ jsx("div", { className: "animate-card-deal", style: { animationDelay: `${i * 90}ms` }, children: /* @__PURE__ */ jsx(PlayingCard, { card, size }) }, i)) });
155
+ }
156
+
157
+ // src/components/Seat.tsx
158
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
159
+ function Seat({
160
+ isActive = false,
161
+ isWinner = false,
162
+ isDimmed = false,
163
+ onClick,
164
+ header,
165
+ badge,
166
+ children,
167
+ footer,
168
+ overlay,
169
+ width = 160,
170
+ height = 176,
171
+ className
172
+ }) {
173
+ return /* @__PURE__ */ jsxs2("div", { className: cn("relative", className), style: { width, height }, children: [
174
+ /* @__PURE__ */ jsxs2(
175
+ "div",
176
+ {
177
+ role: onClick ? "button" : void 0,
178
+ tabIndex: onClick ? 0 : void 0,
179
+ onClick,
180
+ onKeyDown: onClick ? (e) => {
181
+ if (e.key === "Enter" || e.key === " ") onClick();
182
+ } : void 0,
183
+ className: cn(
184
+ "flex h-full w-full flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 backdrop-blur transition",
185
+ isActive ? "border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]" : "border-neutral-700",
186
+ isWinner && "border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]",
187
+ isDimmed && "opacity-50",
188
+ onClick && "cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400"
189
+ ),
190
+ children: [
191
+ header !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full items-center justify-between gap-1 text-[11px]", children: header }),
192
+ /* @__PURE__ */ jsx2("div", { className: "flex flex-1 items-center justify-center", children }),
193
+ footer !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full items-center justify-between text-[11px] text-neutral-300", children: footer })
194
+ ]
195
+ }
196
+ ),
197
+ badge !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-1 -right-1", children: badge }),
198
+ overlay !== void 0 && /* @__PURE__ */ jsx2("div", { className: "pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 text-center", children: overlay })
199
+ ] });
200
+ }
201
+
202
+ // src/components/Pot.tsx
203
+ import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
204
+ function Pot({ amount, label, format = formatBB, className }) {
205
+ return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-2 font-mono text-sm text-amber-400", className), children: [
206
+ label && /* @__PURE__ */ jsx3("span", { className: "text-neutral-400", children: label }),
207
+ /* @__PURE__ */ jsx3("span", { className: "whitespace-nowrap rounded-full border border-amber-400/50 bg-neutral-900/80 px-3 py-1 shadow-inner", children: format(amount) })
208
+ ] });
209
+ }
210
+
211
+ // src/components/CardTable.tsx
212
+ import { useRef, useLayoutEffect, useState, useMemo } from "react";
213
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
214
+ var DESIGN_W = 1200;
215
+ var DESIGN_H = 900;
216
+ var MARGIN_X = 164;
217
+ var MARGIN_Y = 196;
218
+ var TABLE_RX = DESIGN_W / 2 - MARGIN_X;
219
+ var TABLE_RY = DESIGN_H / 2 - MARGIN_Y;
220
+ var FELT_GAP = 11;
221
+ var BET_SCALE = 0.72;
222
+ var BET_RX = TABLE_RX * BET_SCALE;
223
+ var BET_RY = TABLE_RY * BET_SCALE;
224
+ function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
225
+ const rad = angleDeg * Math.PI / 180;
226
+ return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
227
+ }
228
+ function calculateSeatLayouts(seatCount, yourSeatIndex, seatWidth, seatHeight) {
229
+ const cx = DESIGN_W / 2;
230
+ const cy = DESIGN_H / 2;
231
+ const ringX = TABLE_RX - FELT_GAP + seatWidth / 2 + 6;
232
+ const ringY = TABLE_RY - FELT_GAP + seatHeight / 2 + 6;
233
+ const step = 360 / seatCount;
234
+ const angleOf = (i) => -90 + i * step;
235
+ const rotationOffset = yourSeatIndex !== null ? 90 - angleOf(yourSeatIndex) : 0;
236
+ const layouts = [];
237
+ for (let i = 0; i < seatCount; i++) {
238
+ const pt = getEllipsePoint(cx, cy, ringX, ringY, angleOf(i) + rotationOffset);
239
+ layouts.push({
240
+ x: pt.x - seatWidth / 2,
241
+ y: pt.y - seatHeight / 2,
242
+ index: i,
243
+ isYou: i === yourSeatIndex
244
+ });
245
+ }
246
+ return layouts;
247
+ }
248
+ function CardTable({
249
+ seatCount = 6,
250
+ seats,
251
+ renderSeat,
252
+ yourSeatIndex = null,
253
+ center,
254
+ seatWidth = 160,
255
+ seatHeight = 176,
256
+ className
257
+ }) {
258
+ const count = Math.max(2, Math.min(10, Math.floor(seatCount)));
259
+ const outerRef = useRef(null);
260
+ const [scale, setScale] = useState(1);
261
+ const layouts = useMemo(
262
+ () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),
263
+ [count, yourSeatIndex, seatWidth, seatHeight]
264
+ );
265
+ useLayoutEffect(() => {
266
+ const el = outerRef.current;
267
+ if (!el) return;
268
+ const compute = () => {
269
+ const w = el.clientWidth;
270
+ const h = el.clientHeight;
271
+ if (!w || !h) return;
272
+ setScale(Math.min(w / DESIGN_W, h / DESIGN_H));
273
+ };
274
+ compute();
275
+ const ro = new ResizeObserver(compute);
276
+ ro.observe(el);
277
+ return () => ro.disconnect();
278
+ }, []);
279
+ const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
280
+ const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
281
+ const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
282
+ const betLineInset = {
283
+ inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
284
+ };
285
+ return /* @__PURE__ */ jsx4(
286
+ "div",
287
+ {
288
+ ref: outerRef,
289
+ className: cn("relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden", className),
290
+ children: /* @__PURE__ */ jsx4("div", { style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ jsxs4(
291
+ "div",
292
+ {
293
+ className: "relative",
294
+ style: { width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: "top left" },
295
+ children: [
296
+ /* @__PURE__ */ jsx4(
297
+ "div",
298
+ {
299
+ className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl",
300
+ "aria-hidden": "true"
301
+ }
302
+ ),
303
+ /* @__PURE__ */ jsx4(
304
+ "div",
305
+ {
306
+ className: "absolute rounded-[50%] bg-gradient-to-b from-[#a5744a] via-[#6b3f22] to-[#38200f] shadow-[0_26px_70px_-12px_rgba(0,0,0,0.85),inset_0_3px_0_rgba(255,255,255,0.20),inset_0_-6px_16px_rgba(0,0,0,0.5)]",
307
+ style: tableFrameInset,
308
+ "aria-hidden": "true"
309
+ }
310
+ ),
311
+ /* @__PURE__ */ jsx4(
312
+ "div",
313
+ {
314
+ className: "absolute rounded-[50%] bg-[radial-gradient(ellipse_at_50%_32%,#23804b,#0f4d2e_55%,#093b22)] shadow-[inset_0_0_90px_rgba(0,0,0,0.55),inset_0_3px_10px_rgba(255,255,255,0.06)]",
315
+ style: feltInset,
316
+ "aria-hidden": "true"
317
+ }
318
+ ),
319
+ /* @__PURE__ */ jsx4(
320
+ "div",
321
+ {
322
+ className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
323
+ style: betLineInset,
324
+ "aria-hidden": "true"
325
+ }
326
+ ),
327
+ /* @__PURE__ */ jsx4("div", { className: "absolute flex items-center justify-center pointer-events-none", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ jsx4("span", { className: "text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none", children: "\u2660\u2665\u2666\u2663" }) }),
328
+ center !== void 0 && /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center", children: center }),
329
+ /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 z-20", children: layouts.map(({ x, y, index, isYou }) => {
330
+ const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou) : null;
331
+ if (content === null) return null;
332
+ return /* @__PURE__ */ jsx4("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: content }, index);
333
+ }) }),
334
+ /* @__PURE__ */ jsx4("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
335
+ ]
336
+ }
337
+ ) })
338
+ }
339
+ );
340
+ }
341
+ export {
342
+ BB,
343
+ CardBack,
344
+ CardTable,
345
+ CommunityCards,
346
+ HoleCards,
347
+ PlayingCard,
348
+ CardTable as PokerTable,
349
+ Pot,
350
+ Seat,
351
+ cn,
352
+ formatBB,
353
+ getSuitSymbol,
354
+ nextSeatIndex,
355
+ parseCard
356
+ };
357
+ //# sourceMappingURL=index.js.map