@stargazeryan/poker-ui 0.1.0 → 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.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode } from 'react';
2
3
 
3
4
  declare function cn(...parts: Array<string | false | null | undefined>): string;
4
5
 
@@ -7,6 +8,22 @@ declare const BB = 20;
7
8
  /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
8
9
  declare function formatBB(amount: number): string;
9
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
+
10
27
  type CardSize = "xs" | "sm" | "md" | "lg";
11
28
  /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
12
29
  declare function parseCard(card: string): {
@@ -46,57 +63,68 @@ interface CommunityCardsProps {
46
63
  /** The five community cards, each dealt with a staggered flip animation. */
47
64
  declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
48
65
 
49
- /** Presentational view-model for one seat (no game logic). */
50
- interface SeatView {
51
- nickname: string;
52
- stack: number;
53
- holeCards?: (string | null)[] | null;
54
- isDealer?: boolean;
55
- isTurn?: boolean;
56
- folded?: boolean;
57
- busted?: boolean;
58
- allIn?: boolean;
59
- currentBet?: number;
60
- isYou?: boolean;
61
- isWinner?: boolean;
62
- /** Optional short label shown in a bubble (e.g. "Raise 500"). */
63
- actionLabel?: string | null;
64
- }
65
66
  interface SeatProps {
66
- seat: SeatView | null;
67
- size?: CardSize;
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;
68
87
  className?: string;
69
88
  }
70
89
  /**
71
- * A presentational seat: nickname, stack, hole cards, dealer/turn/winner
72
- * markers. It is fully controlled — all state arrives through `seat`.
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`.
73
93
  */
74
- declare function Seat({ seat, size, className }: SeatProps): react.JSX.Element;
94
+ declare function Seat({ isActive, isWinner, isDimmed, onClick, header, badge, children, footer, overlay, width, height, className, }: SeatProps): react.JSX.Element;
75
95
 
76
- interface PotRow {
77
- label: string;
96
+ interface PotProps {
78
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;
79
103
  }
80
- interface PokerTableProps {
81
- /** Six seats (some may be empty). */
82
- seats: (SeatView | null)[];
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;
83
114
  /** Which seat belongs to the viewer; rotates that seat to the bottom. */
84
115
  yourSeatIndex?: number | null;
85
- communityCards?: (string | null)[];
86
- pot?: number;
87
- pots?: PotRow[];
88
- isWaiting?: boolean;
89
- winnerSeats?: number[];
90
- seatSize?: CardSize;
91
- /** Optional override for rendering a seat. */
92
- renderSeat?: (seat: SeatView | null, index: number) => React.ReactNode;
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;
93
121
  className?: string;
94
122
  }
95
123
  /**
96
- * A responsive, themable poker table. It scales a fixed 1200x900 design onto
97
- * its container and arranges six seats around the felt. Purely presentational:
98
- * pass seat view-models and card codes in.
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.
99
127
  */
100
- declare function PokerTable({ seats, yourSeatIndex, communityCards, pot, pots, isWaiting, winnerSeats, seatSize, renderSeat, className, }: PokerTableProps): react.JSX.Element;
128
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
101
129
 
102
- export { BB, CardBack, type CardBackProps, type CardSize, CommunityCards, type CommunityCardsProps, HoleCards, type HoleCardsProps, PlayingCard, type PlayingCardProps, PokerTable, type PokerTableProps, type PotRow, Seat, type SeatProps, type SeatView, cn, formatBB, getSuitSymbol, parseCard };
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.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode } from 'react';
2
3
 
3
4
  declare function cn(...parts: Array<string | false | null | undefined>): string;
4
5
 
@@ -7,6 +8,22 @@ declare const BB = 20;
7
8
  /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
8
9
  declare function formatBB(amount: number): string;
9
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
+
10
27
  type CardSize = "xs" | "sm" | "md" | "lg";
11
28
  /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
12
29
  declare function parseCard(card: string): {
@@ -46,57 +63,68 @@ interface CommunityCardsProps {
46
63
  /** The five community cards, each dealt with a staggered flip animation. */
47
64
  declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
48
65
 
49
- /** Presentational view-model for one seat (no game logic). */
50
- interface SeatView {
51
- nickname: string;
52
- stack: number;
53
- holeCards?: (string | null)[] | null;
54
- isDealer?: boolean;
55
- isTurn?: boolean;
56
- folded?: boolean;
57
- busted?: boolean;
58
- allIn?: boolean;
59
- currentBet?: number;
60
- isYou?: boolean;
61
- isWinner?: boolean;
62
- /** Optional short label shown in a bubble (e.g. "Raise 500"). */
63
- actionLabel?: string | null;
64
- }
65
66
  interface SeatProps {
66
- seat: SeatView | null;
67
- size?: CardSize;
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;
68
87
  className?: string;
69
88
  }
70
89
  /**
71
- * A presentational seat: nickname, stack, hole cards, dealer/turn/winner
72
- * markers. It is fully controlled — all state arrives through `seat`.
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`.
73
93
  */
74
- declare function Seat({ seat, size, className }: SeatProps): react.JSX.Element;
94
+ declare function Seat({ isActive, isWinner, isDimmed, onClick, header, badge, children, footer, overlay, width, height, className, }: SeatProps): react.JSX.Element;
75
95
 
76
- interface PotRow {
77
- label: string;
96
+ interface PotProps {
78
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;
79
103
  }
80
- interface PokerTableProps {
81
- /** Six seats (some may be empty). */
82
- seats: (SeatView | null)[];
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;
83
114
  /** Which seat belongs to the viewer; rotates that seat to the bottom. */
84
115
  yourSeatIndex?: number | null;
85
- communityCards?: (string | null)[];
86
- pot?: number;
87
- pots?: PotRow[];
88
- isWaiting?: boolean;
89
- winnerSeats?: number[];
90
- seatSize?: CardSize;
91
- /** Optional override for rendering a seat. */
92
- renderSeat?: (seat: SeatView | null, index: number) => React.ReactNode;
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;
93
121
  className?: string;
94
122
  }
95
123
  /**
96
- * A responsive, themable poker table. It scales a fixed 1200x900 design onto
97
- * its container and arranges six seats around the felt. Purely presentational:
98
- * pass seat view-models and card codes in.
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.
99
127
  */
100
- declare function PokerTable({ seats, yourSeatIndex, communityCards, pot, pots, isWaiting, winnerSeats, seatSize, renderSeat, className, }: PokerTableProps): react.JSX.Element;
128
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
101
129
 
102
- export { BB, CardBack, type CardBackProps, type CardSize, CommunityCards, type CommunityCardsProps, HoleCards, type HoleCardsProps, PlayingCard, type PlayingCardProps, PokerTable, type PokerTableProps, type PotRow, Seat, type SeatProps, type SeatView, cn, formatBB, getSuitSymbol, parseCard };
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 CHANGED
@@ -12,6 +12,20 @@ function formatBB(amount) {
12
12
  return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;
13
13
  }
14
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
+
15
29
  // src/components/PlayingCard.tsx
16
30
  import { jsx, jsxs } from "react/jsx-runtime";
17
31
  var SIZE_CLASSES = {
@@ -142,61 +156,61 @@ function CommunityCards({ cards, size = "md", className }) {
142
156
 
143
157
  // src/components/Seat.tsx
144
158
  import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
145
- function Seat({ seat, size = "sm", className }) {
146
- if (!seat) {
147
- return /* @__PURE__ */ jsx2(
148
- "div",
149
- {
150
- className: cn(
151
- "w-40 h-44 rounded-2xl border-2 border-dashed border-white/10",
152
- "flex items-center justify-center text-white/20 text-xs",
153
- className
154
- ),
155
- children: "Empty seat"
156
- }
157
- );
158
- }
159
- const showCards = !seat.folded && !seat.busted && !!seat.holeCards && seat.holeCards.length > 0;
160
- return /* @__PURE__ */ jsxs2("div", { className: cn("relative w-40 h-44", className), children: [
161
- seat.actionLabel && /* @__PURE__ */ jsx2("div", { className: "animate-bubble-pop absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-neutral-900 shadow-lg", children: seat.actionLabel }),
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: [
162
174
  /* @__PURE__ */ jsxs2(
163
175
  "div",
164
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,
165
183
  className: cn(
166
- "flex h-full w-full flex-col items-center justify-between rounded-2xl border bg-neutral-900/80 p-2 backdrop-blur",
167
- seat.isTurn ? "border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]" : "border-neutral-700",
168
- seat.isWinner && "border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]",
169
- seat.folded && "opacity-50"
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"
170
189
  ),
171
190
  children: [
172
- /* @__PURE__ */ jsxs2("div", { className: "flex w-full items-center justify-between text-[11px]", children: [
173
- /* @__PURE__ */ jsxs2("span", { className: "truncate font-semibold text-neutral-100", children: [
174
- seat.isWinner ? "\u{1F451} " : "",
175
- seat.nickname,
176
- seat.isYou ? " \u{1F448}" : ""
177
- ] }),
178
- seat.isDealer && /* @__PURE__ */ jsx2("span", { className: "flex h-4 w-4 items-center justify-center rounded-full bg-white text-[9px] font-bold text-neutral-900", children: "D" })
179
- ] }),
180
- /* @__PURE__ */ jsx2("div", { className: "flex flex-1 items-center justify-center", children: seat.busted ? /* @__PURE__ */ jsx2("span", { className: "text-xs font-semibold text-red-400", children: "Busted" }) : showCards ? /* @__PURE__ */ jsx2(HoleCards, { cards: seat.holeCards, size }) : /* @__PURE__ */ jsxs2("div", { className: "flex gap-2", children: [
181
- /* @__PURE__ */ jsx2(CardBack, { size }),
182
- /* @__PURE__ */ jsx2(CardBack, { size })
183
- ] }) }),
184
- /* @__PURE__ */ jsxs2("div", { className: "flex w-full items-center justify-between text-[11px] text-neutral-300", children: [
185
- /* @__PURE__ */ jsx2("span", { className: "font-mono", children: formatBB(seat.stack) }),
186
- seat.allIn ? /* @__PURE__ */ jsx2("span", { className: "rounded bg-red-600/80 px-1.5 text-[10px] font-bold text-white", children: "ALL-IN" }) : seat.currentBet && seat.currentBet > 0 ? /* @__PURE__ */ jsx2("span", { className: "font-mono text-amber-300", children: formatBB(seat.currentBet) }) : null
187
- ] })
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 })
188
194
  ]
189
195
  }
190
- )
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 })
191
199
  ] });
192
200
  }
193
201
 
194
- // src/components/PokerTable.tsx
195
- import { useRef, useLayoutEffect, useState, useMemo } from "react";
202
+ // src/components/Pot.tsx
196
203
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
197
- var SEAT_COUNT = 6;
198
- var SEAT_W = 160;
199
- var SEAT_H = 176;
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";
200
214
  var DESIGN_W = 1200;
201
215
  var DESIGN_H = 900;
202
216
  var MARGIN_X = 164;
@@ -207,47 +221,46 @@ var FELT_GAP = 11;
207
221
  var BET_SCALE = 0.72;
208
222
  var BET_RX = TABLE_RX * BET_SCALE;
209
223
  var BET_RY = TABLE_RY * BET_SCALE;
210
- var SEAT_RING_X = TABLE_RX - FELT_GAP + SEAT_W / 2 + 6;
211
- var SEAT_RING_Y = TABLE_RY - FELT_GAP + SEAT_H / 2 + 6;
212
- var BASE_ANGLES = [-90, -30, 30, 90, 150, 210];
213
224
  function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
214
225
  const rad = angleDeg * Math.PI / 180;
215
226
  return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
216
227
  }
217
- function calculateSeatLayouts(seats, yourSeatIndex) {
228
+ function calculateSeatLayouts(seatCount, yourSeatIndex, seatWidth, seatHeight) {
218
229
  const cx = DESIGN_W / 2;
219
230
  const cy = DESIGN_H / 2;
220
- const rotationOffset = yourSeatIndex !== null ? BASE_ANGLES[3] - BASE_ANGLES[yourSeatIndex] : 0;
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;
221
236
  const layouts = [];
222
- for (let i = 0; i < SEAT_COUNT; i++) {
223
- const pt = getEllipsePoint(cx, cy, SEAT_RING_X, SEAT_RING_Y, BASE_ANGLES[i] + rotationOffset);
237
+ for (let i = 0; i < seatCount; i++) {
238
+ const pt = getEllipsePoint(cx, cy, ringX, ringY, angleOf(i) + rotationOffset);
224
239
  layouts.push({
225
- x: pt.x - SEAT_W / 2,
226
- y: pt.y - SEAT_H / 2,
227
- logicalIndex: i,
228
- isYou: i === yourSeatIndex,
229
- seat: seats[i] ?? null
240
+ x: pt.x - seatWidth / 2,
241
+ y: pt.y - seatHeight / 2,
242
+ index: i,
243
+ isYou: i === yourSeatIndex
230
244
  });
231
245
  }
232
246
  return layouts;
233
247
  }
234
- function PokerTable({
248
+ function CardTable({
249
+ seatCount = 6,
235
250
  seats,
236
- yourSeatIndex = null,
237
- communityCards = [],
238
- pot = 0,
239
- pots = [],
240
- isWaiting = false,
241
- winnerSeats = [],
242
- seatSize = "sm",
243
251
  renderSeat,
252
+ yourSeatIndex = null,
253
+ center,
254
+ seatWidth = 160,
255
+ seatHeight = 176,
244
256
  className
245
257
  }) {
258
+ const count = Math.max(2, Math.min(10, Math.floor(seatCount)));
246
259
  const outerRef = useRef(null);
247
260
  const [scale, setScale] = useState(1);
248
261
  const layouts = useMemo(
249
- () => calculateSeatLayouts(seats, yourSeatIndex),
250
- [seats, yourSeatIndex]
262
+ () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),
263
+ [count, yourSeatIndex, seatWidth, seatHeight]
251
264
  );
252
265
  useLayoutEffect(() => {
253
266
  const el = outerRef.current;
@@ -269,26 +282,25 @@ function PokerTable({
269
282
  const betLineInset = {
270
283
  inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
271
284
  };
272
- const potRows = pots.length > 0 ? pots : pot > 0 ? [{ label: "Pot", amount: pot }] : [];
273
- return /* @__PURE__ */ jsx3(
285
+ return /* @__PURE__ */ jsx4(
274
286
  "div",
275
287
  {
276
288
  ref: outerRef,
277
- className: cn("relative flex h-full w-full items-center justify-center overflow-hidden min-h-0", className),
278
- children: /* @__PURE__ */ jsx3("div", { style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ jsxs3(
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(
279
291
  "div",
280
292
  {
281
293
  className: "relative",
282
294
  style: { width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: "top left" },
283
295
  children: [
284
- /* @__PURE__ */ jsx3(
296
+ /* @__PURE__ */ jsx4(
285
297
  "div",
286
298
  {
287
299
  className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl",
288
300
  "aria-hidden": "true"
289
301
  }
290
302
  ),
291
- /* @__PURE__ */ jsx3(
303
+ /* @__PURE__ */ jsx4(
292
304
  "div",
293
305
  {
294
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)]",
@@ -296,7 +308,7 @@ function PokerTable({
296
308
  "aria-hidden": "true"
297
309
  }
298
310
  ),
299
- /* @__PURE__ */ jsx3(
311
+ /* @__PURE__ */ jsx4(
300
312
  "div",
301
313
  {
302
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)]",
@@ -304,7 +316,7 @@ function PokerTable({
304
316
  "aria-hidden": "true"
305
317
  }
306
318
  ),
307
- /* @__PURE__ */ jsx3(
319
+ /* @__PURE__ */ jsx4(
308
320
  "div",
309
321
  {
310
322
  className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
@@ -312,22 +324,14 @@ function PokerTable({
312
324
  "aria-hidden": "true"
313
325
  }
314
326
  ),
315
- /* @__PURE__ */ jsx3("div", { className: "absolute flex items-center justify-center pointer-events-none", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none", children: "\u2660\u2665\u2666\u2663" }) }),
316
- /* @__PURE__ */ jsxs3("div", { className: "absolute inset-0 flex flex-col items-center justify-center pointer-events-none", children: [
317
- /* @__PURE__ */ jsxs3("div", { className: "mb-4 flex flex-col items-center gap-3", children: [
318
- /* @__PURE__ */ jsx3(CommunityCards, { cards: communityCards, size: "lg", className: "filter drop-shadow-lg" }),
319
- potRows.length > 0 && /* @__PURE__ */ jsx3("div", { className: "flex flex-col items-center gap-1", children: potRows.map((p, i) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 font-mono text-sm text-amber-400", children: [
320
- /* @__PURE__ */ jsx3("span", { className: "text-neutral-400", children: p.label }),
321
- /* @__PURE__ */ jsx3("span", { className: "whitespace-nowrap rounded-full border border-amber-400/50 bg-neutral-900/80 px-3 py-1 shadow-inner", children: formatBB(p.amount) })
322
- ] }, i)) })
323
- ] }),
324
- isWaiting && /* @__PURE__ */ jsx3("div", { className: "rounded-full border border-neutral-700 bg-neutral-900/70 px-3 py-1 text-xs text-neutral-400", children: "Waiting to start" })
325
- ] }),
326
- /* @__PURE__ */ jsx3("div", { className: "absolute inset-0 z-20 pointer-events-none", children: layouts.map(({ x, y, logicalIndex, isYou, seat }) => {
327
- const merged = seat ? { ...seat, isYou, isWinner: winnerSeats.includes(logicalIndex) || seat.isWinner } : null;
328
- return /* @__PURE__ */ jsx3("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: renderSeat ? renderSeat(merged, logicalIndex) : /* @__PURE__ */ jsx3(Seat, { seat: merged, size: seatSize }) }, logicalIndex);
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);
329
333
  }) }),
330
- /* @__PURE__ */ jsx3("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
334
+ /* @__PURE__ */ jsx4("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
331
335
  ]
332
336
  }
333
337
  ) })
@@ -337,14 +341,17 @@ function PokerTable({
337
341
  export {
338
342
  BB,
339
343
  CardBack,
344
+ CardTable,
340
345
  CommunityCards,
341
346
  HoleCards,
342
347
  PlayingCard,
343
- PokerTable,
348
+ CardTable as PokerTable,
349
+ Pot,
344
350
  Seat,
345
351
  cn,
346
352
  formatBB,
347
353
  getSuitSymbol,
354
+ nextSeatIndex,
348
355
  parseCard
349
356
  };
350
357
  //# sourceMappingURL=index.js.map