@stargazeryan/poker-ui 0.1.0 → 0.3.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,5 +1,7 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
2
3
 
4
+ /** Join class names and resolve conflicting Tailwind utilities (last wins). */
3
5
  declare function cn(...parts: Array<string | false | null | undefined>): string;
4
6
 
5
7
  /** Big Blind value used for stack display. */
@@ -7,6 +9,22 @@ declare const BB = 20;
7
9
  /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
8
10
  declare function formatBB(amount: number): string;
9
11
 
12
+ /**
13
+ * Seat-ring helpers. Pure functions — they contain no game-specific rules.
14
+ */
15
+ interface NextSeatOptions {
16
+ /** +1 for clockwise, -1 for counter-clockwise. Default +1. */
17
+ dir?: 1 | -1;
18
+ /** Return true to skip a seat (e.g. folded / empty / out). */
19
+ skip?: (index: number) => boolean;
20
+ }
21
+ /**
22
+ * Return the index of the next seat on the ring after `current`, skipping any
23
+ * seat matched by `opts.skip`. Wraps around. Returns `current` if every other
24
+ * seat is skipped.
25
+ */
26
+ declare function nextSeatIndex(current: number, seatCount: number, opts?: NextSeatOptions): number;
27
+
10
28
  type CardSize = "xs" | "sm" | "md" | "lg";
11
29
  /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
12
30
  declare function parseCard(card: string): {
@@ -46,57 +64,83 @@ interface CommunityCardsProps {
46
64
  /** The five community cards, each dealt with a staggered flip animation. */
47
65
  declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
48
66
 
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
67
  interface SeatProps {
66
- seat: SeatView | null;
67
- size?: CardSize;
68
+ /** Turn/active highlight (generic). */
69
+ isActive?: boolean;
70
+ /** Winner highlight (generic). */
71
+ isWinner?: boolean;
72
+ /** Dimmed, e.g. folded or out (generic). */
73
+ isDimmed?: boolean;
74
+ /** Optional click handler (e.g. "sit here"). */
75
+ onClick?: () => void;
76
+ /** Slot: top row inside the frame — name / badges. */
77
+ header?: ReactNode;
78
+ /** Slot: center content — 0..N cards, avatar, anything. */
79
+ children?: ReactNode;
80
+ /** Slot: bottom row inside the frame — stack / status. */
81
+ footer?: ReactNode;
82
+ /** Slot: floating element at the frame's top-right (legacy alias of topRight). */
83
+ badge?: ReactNode;
84
+ /** Slot: overlay centered over the frame. */
85
+ overlay?: ReactNode;
86
+ /** Slot: floating content above the frame, centered (e.g. an action bubble). */
87
+ above?: ReactNode;
88
+ /** Slot: content below the frame, centered (e.g. countdown / bet pill). */
89
+ below?: ReactNode;
90
+ /** Slot: floating content at the frame's top-left corner (e.g. a crown). */
91
+ topLeft?: ReactNode;
92
+ /** Slot: floating content at the frame's top-right corner (e.g. a kick button). */
93
+ topRight?: ReactNode;
94
+ /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */
95
+ topCenter?: ReactNode;
96
+ width?: number;
97
+ height?: number;
68
98
  className?: string;
99
+ /** Extra classes for the inner frame, to override visuals. */
100
+ frameClassName?: string;
101
+ /** Inline styles for the inner frame. */
102
+ frameStyle?: CSSProperties;
69
103
  }
70
104
  /**
71
- * A presentational seat: nickname, stack, hole cards, dealer/turn/winner
72
- * markers. It is fully controlled — all state arrives through `seat`.
105
+ * A neutral seat: a frame with generic visual states and named slots. Put
106
+ * whatever you want into `header` / `children` / `footer`, and use
107
+ * `above` / `below` / `topLeft` / `topRight` / `topCenter` / `overlay` for
108
+ * floating bits. It knows nothing about any particular game.
73
109
  */
74
- declare function Seat({ seat, size, className }: SeatProps): react.JSX.Element;
110
+ declare function Seat({ isActive, isWinner, isDimmed, onClick, header, children, footer, badge, overlay, above, below, topLeft, topRight, topCenter, width, height, className, frameClassName, frameStyle, }: SeatProps): react.JSX.Element;
75
111
 
76
- interface PotRow {
77
- label: string;
112
+ interface PotProps {
78
113
  amount: number;
114
+ /** Optional label, e.g. "Main" / "Side". */
115
+ label?: string;
116
+ /** Formatter for the amount; defaults to Big-Blind formatting. */
117
+ format?: (amount: number) => string;
118
+ className?: string;
79
119
  }
80
- interface PokerTableProps {
81
- /** Six seats (some may be empty). */
82
- seats: (SeatView | null)[];
120
+ /** A small, standalone pot pill. Optional — the table does not require it. */
121
+ declare function Pot({ amount, label, format, className }: PotProps): react.JSX.Element;
122
+
123
+ interface CardTableProps {
124
+ /** Number of seats (2–10). Default 6. */
125
+ seatCount?: number;
126
+ /** Optional per-seat content. Length should equal `seatCount`. */
127
+ seats?: (ReactNode | null)[];
128
+ /** Alternative to `seats`: render a seat by index. */
129
+ renderSeat?: (index: number, isYou: boolean) => ReactNode;
83
130
  /** Which seat belongs to the viewer; rotates that seat to the bottom. */
84
131
  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;
132
+ /** Optional content in the middle of the table (cards, pot, anything). */
133
+ center?: ReactNode;
134
+ /** Seat frame size used for layout. */
135
+ seatWidth?: number;
136
+ seatHeight?: number;
93
137
  className?: string;
94
138
  }
95
139
  /**
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.
140
+ * A responsive, game-agnostic card table. It scales a fixed 1200x900 design
141
+ * onto its container and arranges `seatCount` seats around the felt. Purely
142
+ * presentational: supply seat content and an optional `center` slot.
99
143
  */
100
- declare function PokerTable({ seats, yourSeatIndex, communityCards, pot, pots, isWaiting, winnerSeats, seatSize, renderSeat, className, }: PokerTableProps): react.JSX.Element;
144
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
101
145
 
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 };
146
+ 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,5 +1,7 @@
1
1
  import * as react from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
2
3
 
4
+ /** Join class names and resolve conflicting Tailwind utilities (last wins). */
3
5
  declare function cn(...parts: Array<string | false | null | undefined>): string;
4
6
 
5
7
  /** Big Blind value used for stack display. */
@@ -7,6 +9,22 @@ declare const BB = 20;
7
9
  /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
8
10
  declare function formatBB(amount: number): string;
9
11
 
12
+ /**
13
+ * Seat-ring helpers. Pure functions — they contain no game-specific rules.
14
+ */
15
+ interface NextSeatOptions {
16
+ /** +1 for clockwise, -1 for counter-clockwise. Default +1. */
17
+ dir?: 1 | -1;
18
+ /** Return true to skip a seat (e.g. folded / empty / out). */
19
+ skip?: (index: number) => boolean;
20
+ }
21
+ /**
22
+ * Return the index of the next seat on the ring after `current`, skipping any
23
+ * seat matched by `opts.skip`. Wraps around. Returns `current` if every other
24
+ * seat is skipped.
25
+ */
26
+ declare function nextSeatIndex(current: number, seatCount: number, opts?: NextSeatOptions): number;
27
+
10
28
  type CardSize = "xs" | "sm" | "md" | "lg";
11
29
  /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
12
30
  declare function parseCard(card: string): {
@@ -46,57 +64,83 @@ interface CommunityCardsProps {
46
64
  /** The five community cards, each dealt with a staggered flip animation. */
47
65
  declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
48
66
 
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
67
  interface SeatProps {
66
- seat: SeatView | null;
67
- size?: CardSize;
68
+ /** Turn/active highlight (generic). */
69
+ isActive?: boolean;
70
+ /** Winner highlight (generic). */
71
+ isWinner?: boolean;
72
+ /** Dimmed, e.g. folded or out (generic). */
73
+ isDimmed?: boolean;
74
+ /** Optional click handler (e.g. "sit here"). */
75
+ onClick?: () => void;
76
+ /** Slot: top row inside the frame — name / badges. */
77
+ header?: ReactNode;
78
+ /** Slot: center content — 0..N cards, avatar, anything. */
79
+ children?: ReactNode;
80
+ /** Slot: bottom row inside the frame — stack / status. */
81
+ footer?: ReactNode;
82
+ /** Slot: floating element at the frame's top-right (legacy alias of topRight). */
83
+ badge?: ReactNode;
84
+ /** Slot: overlay centered over the frame. */
85
+ overlay?: ReactNode;
86
+ /** Slot: floating content above the frame, centered (e.g. an action bubble). */
87
+ above?: ReactNode;
88
+ /** Slot: content below the frame, centered (e.g. countdown / bet pill). */
89
+ below?: ReactNode;
90
+ /** Slot: floating content at the frame's top-left corner (e.g. a crown). */
91
+ topLeft?: ReactNode;
92
+ /** Slot: floating content at the frame's top-right corner (e.g. a kick button). */
93
+ topRight?: ReactNode;
94
+ /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */
95
+ topCenter?: ReactNode;
96
+ width?: number;
97
+ height?: number;
68
98
  className?: string;
99
+ /** Extra classes for the inner frame, to override visuals. */
100
+ frameClassName?: string;
101
+ /** Inline styles for the inner frame. */
102
+ frameStyle?: CSSProperties;
69
103
  }
70
104
  /**
71
- * A presentational seat: nickname, stack, hole cards, dealer/turn/winner
72
- * markers. It is fully controlled — all state arrives through `seat`.
105
+ * A neutral seat: a frame with generic visual states and named slots. Put
106
+ * whatever you want into `header` / `children` / `footer`, and use
107
+ * `above` / `below` / `topLeft` / `topRight` / `topCenter` / `overlay` for
108
+ * floating bits. It knows nothing about any particular game.
73
109
  */
74
- declare function Seat({ seat, size, className }: SeatProps): react.JSX.Element;
110
+ declare function Seat({ isActive, isWinner, isDimmed, onClick, header, children, footer, badge, overlay, above, below, topLeft, topRight, topCenter, width, height, className, frameClassName, frameStyle, }: SeatProps): react.JSX.Element;
75
111
 
76
- interface PotRow {
77
- label: string;
112
+ interface PotProps {
78
113
  amount: number;
114
+ /** Optional label, e.g. "Main" / "Side". */
115
+ label?: string;
116
+ /** Formatter for the amount; defaults to Big-Blind formatting. */
117
+ format?: (amount: number) => string;
118
+ className?: string;
79
119
  }
80
- interface PokerTableProps {
81
- /** Six seats (some may be empty). */
82
- seats: (SeatView | null)[];
120
+ /** A small, standalone pot pill. Optional — the table does not require it. */
121
+ declare function Pot({ amount, label, format, className }: PotProps): react.JSX.Element;
122
+
123
+ interface CardTableProps {
124
+ /** Number of seats (2–10). Default 6. */
125
+ seatCount?: number;
126
+ /** Optional per-seat content. Length should equal `seatCount`. */
127
+ seats?: (ReactNode | null)[];
128
+ /** Alternative to `seats`: render a seat by index. */
129
+ renderSeat?: (index: number, isYou: boolean) => ReactNode;
83
130
  /** Which seat belongs to the viewer; rotates that seat to the bottom. */
84
131
  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;
132
+ /** Optional content in the middle of the table (cards, pot, anything). */
133
+ center?: ReactNode;
134
+ /** Seat frame size used for layout. */
135
+ seatWidth?: number;
136
+ seatHeight?: number;
93
137
  className?: string;
94
138
  }
95
139
  /**
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.
140
+ * A responsive, game-agnostic card table. It scales a fixed 1200x900 design
141
+ * onto its container and arranges `seatCount` seats around the felt. Purely
142
+ * presentational: supply seat content and an optional `center` slot.
99
143
  */
100
- declare function PokerTable({ seats, yourSeatIndex, communityCards, pot, pots, isWaiting, winnerSeats, seatSize, renderSeat, className, }: PokerTableProps): react.JSX.Element;
144
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
101
145
 
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 };
146
+ 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
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
 
3
3
  // src/cn.ts
4
+ import { twMerge } from "tailwind-merge";
4
5
  function cn(...parts) {
5
- return parts.filter(Boolean).join(" ");
6
+ return twMerge(parts.filter(Boolean).join(" "));
6
7
  }
7
8
 
8
9
  // src/format.ts
@@ -12,6 +13,20 @@ function formatBB(amount) {
12
13
  return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;
13
14
  }
14
15
 
16
+ // src/seat.ts
17
+ function nextSeatIndex(current, seatCount, opts = {}) {
18
+ const n = Math.max(1, Math.floor(seatCount));
19
+ const dir = opts.dir ?? 1;
20
+ const skip = opts.skip;
21
+ const start = (Math.floor(current) % n + n) % n;
22
+ let i = start;
23
+ for (let step = 0; step < n - 1; step++) {
24
+ i = (i + dir + n) % n;
25
+ if (!skip || !skip(i)) return i;
26
+ }
27
+ return start;
28
+ }
29
+
15
30
  // src/components/PlayingCard.tsx
16
31
  import { jsx, jsxs } from "react/jsx-runtime";
17
32
  var SIZE_CLASSES = {
@@ -142,61 +157,75 @@ function CommunityCards({ cards, size = "md", className }) {
142
157
 
143
158
  // src/components/Seat.tsx
144
159
  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 }),
160
+ function Seat({
161
+ isActive = false,
162
+ isWinner = false,
163
+ isDimmed = false,
164
+ onClick,
165
+ header,
166
+ children,
167
+ footer,
168
+ badge,
169
+ overlay,
170
+ above,
171
+ below,
172
+ topLeft,
173
+ topRight,
174
+ topCenter,
175
+ width = 160,
176
+ height = 176,
177
+ className,
178
+ frameClassName,
179
+ frameStyle
180
+ }) {
181
+ return /* @__PURE__ */ jsxs2("div", { className: cn("relative flex flex-col items-center", className), style: { width }, children: [
182
+ topCenter !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1", children: topCenter }),
183
+ topLeft !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-3 -left-2 z-40", children: topLeft }),
184
+ topRight !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-2 -right-2 z-40", children: topRight }),
185
+ above !== void 0 && /* @__PURE__ */ jsx2("div", { className: "pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2", children: above }),
162
186
  /* @__PURE__ */ jsxs2(
163
187
  "div",
164
188
  {
189
+ role: onClick ? "button" : void 0,
190
+ tabIndex: onClick ? 0 : void 0,
191
+ onClick,
192
+ onKeyDown: onClick ? (e) => {
193
+ if (e.key === "Enter" || e.key === " ") onClick();
194
+ } : void 0,
195
+ style: { width, height, ...frameStyle },
165
196
  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"
197
+ "relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition",
198
+ isActive ? "border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]" : "border-neutral-700",
199
+ isWinner && "border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]",
200
+ isDimmed && "opacity-50",
201
+ onClick && "cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400",
202
+ frameClassName
170
203
  ),
171
204
  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
- ] })
205
+ header !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full shrink-0 items-center justify-between gap-1 text-[11px]", children: header }),
206
+ /* @__PURE__ */ jsx2("div", { className: "flex min-h-0 flex-1 items-center justify-center", children }),
207
+ footer !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300", children: footer }),
208
+ 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 })
188
209
  ]
189
210
  }
190
- )
211
+ ),
212
+ badge !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute top-0 right-0 z-40", children: badge }),
213
+ below !== void 0 && /* @__PURE__ */ jsx2("div", { className: "mt-1 flex flex-col items-center gap-1", children: below })
191
214
  ] });
192
215
  }
193
216
 
194
- // src/components/PokerTable.tsx
195
- import { useRef, useLayoutEffect, useState, useMemo } from "react";
217
+ // src/components/Pot.tsx
196
218
  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;
219
+ function Pot({ amount, label, format = formatBB, className }) {
220
+ return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-2 font-mono text-sm text-amber-400", className), children: [
221
+ label && /* @__PURE__ */ jsx3("span", { className: "text-neutral-400", children: label }),
222
+ /* @__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) })
223
+ ] });
224
+ }
225
+
226
+ // src/components/CardTable.tsx
227
+ import { useRef, useLayoutEffect, useState, useMemo } from "react";
228
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
200
229
  var DESIGN_W = 1200;
201
230
  var DESIGN_H = 900;
202
231
  var MARGIN_X = 164;
@@ -207,47 +236,46 @@ var FELT_GAP = 11;
207
236
  var BET_SCALE = 0.72;
208
237
  var BET_RX = TABLE_RX * BET_SCALE;
209
238
  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
239
  function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
214
240
  const rad = angleDeg * Math.PI / 180;
215
241
  return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
216
242
  }
217
- function calculateSeatLayouts(seats, yourSeatIndex) {
243
+ function calculateSeatLayouts(seatCount, yourSeatIndex, seatWidth, seatHeight) {
218
244
  const cx = DESIGN_W / 2;
219
245
  const cy = DESIGN_H / 2;
220
- const rotationOffset = yourSeatIndex !== null ? BASE_ANGLES[3] - BASE_ANGLES[yourSeatIndex] : 0;
246
+ const ringX = TABLE_RX - FELT_GAP + seatWidth / 2 + 6;
247
+ const ringY = TABLE_RY - FELT_GAP + seatHeight / 2 + 6;
248
+ const step = 360 / seatCount;
249
+ const angleOf = (i) => -90 + i * step;
250
+ const rotationOffset = yourSeatIndex !== null ? 90 - angleOf(yourSeatIndex) : 0;
221
251
  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);
252
+ for (let i = 0; i < seatCount; i++) {
253
+ const pt = getEllipsePoint(cx, cy, ringX, ringY, angleOf(i) + rotationOffset);
224
254
  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
255
+ x: pt.x - seatWidth / 2,
256
+ y: pt.y - seatHeight / 2,
257
+ index: i,
258
+ isYou: i === yourSeatIndex
230
259
  });
231
260
  }
232
261
  return layouts;
233
262
  }
234
- function PokerTable({
263
+ function CardTable({
264
+ seatCount = 6,
235
265
  seats,
236
- yourSeatIndex = null,
237
- communityCards = [],
238
- pot = 0,
239
- pots = [],
240
- isWaiting = false,
241
- winnerSeats = [],
242
- seatSize = "sm",
243
266
  renderSeat,
267
+ yourSeatIndex = null,
268
+ center,
269
+ seatWidth = 160,
270
+ seatHeight = 176,
244
271
  className
245
272
  }) {
273
+ const count = Math.max(2, Math.min(10, Math.floor(seatCount)));
246
274
  const outerRef = useRef(null);
247
275
  const [scale, setScale] = useState(1);
248
276
  const layouts = useMemo(
249
- () => calculateSeatLayouts(seats, yourSeatIndex),
250
- [seats, yourSeatIndex]
277
+ () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),
278
+ [count, yourSeatIndex, seatWidth, seatHeight]
251
279
  );
252
280
  useLayoutEffect(() => {
253
281
  const el = outerRef.current;
@@ -269,26 +297,25 @@ function PokerTable({
269
297
  const betLineInset = {
270
298
  inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
271
299
  };
272
- const potRows = pots.length > 0 ? pots : pot > 0 ? [{ label: "Pot", amount: pot }] : [];
273
- return /* @__PURE__ */ jsx3(
300
+ return /* @__PURE__ */ jsx4(
274
301
  "div",
275
302
  {
276
303
  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(
304
+ className: cn("relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden", className),
305
+ children: /* @__PURE__ */ jsx4("div", { style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ jsxs4(
279
306
  "div",
280
307
  {
281
308
  className: "relative",
282
309
  style: { width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: "top left" },
283
310
  children: [
284
- /* @__PURE__ */ jsx3(
311
+ /* @__PURE__ */ jsx4(
285
312
  "div",
286
313
  {
287
314
  className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl",
288
315
  "aria-hidden": "true"
289
316
  }
290
317
  ),
291
- /* @__PURE__ */ jsx3(
318
+ /* @__PURE__ */ jsx4(
292
319
  "div",
293
320
  {
294
321
  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 +323,7 @@ function PokerTable({
296
323
  "aria-hidden": "true"
297
324
  }
298
325
  ),
299
- /* @__PURE__ */ jsx3(
326
+ /* @__PURE__ */ jsx4(
300
327
  "div",
301
328
  {
302
329
  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 +331,7 @@ function PokerTable({
304
331
  "aria-hidden": "true"
305
332
  }
306
333
  ),
307
- /* @__PURE__ */ jsx3(
334
+ /* @__PURE__ */ jsx4(
308
335
  "div",
309
336
  {
310
337
  className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
@@ -312,22 +339,14 @@ function PokerTable({
312
339
  "aria-hidden": "true"
313
340
  }
314
341
  ),
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);
342
+ /* @__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" }) }),
343
+ center !== void 0 && /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center", children: center }),
344
+ /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 z-20", children: layouts.map(({ x, y, index, isYou }) => {
345
+ const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou) : null;
346
+ if (content === null) return null;
347
+ return /* @__PURE__ */ jsx4("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: content }, index);
329
348
  }) }),
330
- /* @__PURE__ */ jsx3("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
349
+ /* @__PURE__ */ jsx4("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
331
350
  ]
332
351
  }
333
352
  ) })
@@ -337,14 +356,17 @@ function PokerTable({
337
356
  export {
338
357
  BB,
339
358
  CardBack,
359
+ CardTable,
340
360
  CommunityCards,
341
361
  HoleCards,
342
362
  PlayingCard,
343
- PokerTable,
363
+ CardTable as PokerTable,
364
+ Pot,
344
365
  Seat,
345
366
  cn,
346
367
  formatBB,
347
368
  getSuitSymbol,
369
+ nextSeatIndex,
348
370
  parseCard
349
371
  };
350
372
  //# sourceMappingURL=index.js.map