@stargazeryan/poker-ui 0.2.0 → 0.4.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,6 +1,7 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
3
3
 
4
+ /** Join class names and resolve conflicting Tailwind utilities (last wins). */
4
5
  declare function cn(...parts: Array<string | false | null | undefined>): string;
5
6
 
6
7
  /** Big Blind value used for stack display. */
@@ -39,7 +40,8 @@ interface PlayingCardProps {
39
40
  }
40
41
  /**
41
42
  * A single playing card. Size is driven by the `size` prop (never viewport
42
- * breakpoints) so cards scale predictably inside any container.
43
+ * breakpoints) so cards scale predictably inside any container. Small sizes
44
+ * (xs / sm) keep only the upright corner index.
43
45
  */
44
46
  declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
45
47
  interface CardBackProps {
@@ -52,46 +54,71 @@ interface HoleCardsProps {
52
54
  cards: (string | null)[];
53
55
  size?: CardSize;
54
56
  className?: string;
57
+ /** Animate the cards dealing in, one after another. Default true. */
58
+ deal?: boolean;
55
59
  }
56
- /** A row of a player's two hole cards. */
57
- declare function HoleCards({ cards, size, className }: HoleCardsProps): react.JSX.Element;
60
+ /** A row of a player's hole cards. */
61
+ declare function HoleCards({ cards, size, className, deal }: HoleCardsProps): react.JSX.Element;
58
62
  interface CommunityCardsProps {
59
63
  cards: (string | null)[];
60
64
  size?: CardSize;
61
65
  className?: string;
66
+ /** Animate each card with a 3D flip. Default true. */
67
+ animated?: boolean;
68
+ /** Reserve a fixed number of slots so the row width never changes. */
69
+ slots?: number;
62
70
  }
63
- /** The five community cards, each dealt with a staggered flip animation. */
64
- declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
71
+ /** A row of community cards, each flipping in with a staggered delay. */
72
+ declare function CommunityCards({ cards, size, className, animated, slots }: CommunityCardsProps): react.JSX.Element;
65
73
 
66
74
  interface SeatProps {
67
- /** Turn/active highlight (generic). */
75
+ /** Turn/active highlight (generic). Animated by default. */
68
76
  isActive?: boolean;
69
- /** Winner highlight (generic). */
77
+ /** Winner highlight (generic). Animated by default. */
70
78
  isWinner?: boolean;
71
79
  /** Dimmed, e.g. folded or out (generic). */
72
80
  isDimmed?: boolean;
81
+ /** Enable the turn/winner glow animations. Default true. */
82
+ animated?: boolean;
83
+ /** Render a default 👑 in the top-left corner when `isWinner`. Default false. */
84
+ winnerCrown?: boolean;
73
85
  /** Optional click handler (e.g. "sit here"). */
74
86
  onClick?: () => void;
75
- /** Slot: top row — name / badges. */
87
+ /** Slot: top row inside the frame — name / badges. */
76
88
  header?: ReactNode;
77
- /** Slot: top-right floating element — e.g. a dealer button. */
78
- badge?: ReactNode;
79
89
  /** Slot: center content — 0..N cards, avatar, anything. */
80
90
  children?: ReactNode;
81
- /** Slot: bottom row — stack / bet / status. */
91
+ /** Slot: bottom row inside the frame — stack / status. */
82
92
  footer?: ReactNode;
83
- /** Slot: overlay — e.g. an "All-in" label. */
93
+ /** Slot: floating element at the frame's top-right (legacy alias of topRight). */
94
+ badge?: ReactNode;
95
+ /** Slot: overlay centered over the frame. */
84
96
  overlay?: ReactNode;
97
+ /** Slot: floating content above the frame, centered (e.g. an action bubble). */
98
+ above?: ReactNode;
99
+ /** Slot: content below the frame, centered (e.g. countdown / bet pill). */
100
+ below?: ReactNode;
101
+ /** Slot: floating content at the frame's top-left corner (e.g. a crown). */
102
+ topLeft?: ReactNode;
103
+ /** Slot: floating content at the frame's top-right corner (e.g. a kick button). */
104
+ topRight?: ReactNode;
105
+ /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */
106
+ topCenter?: ReactNode;
85
107
  width?: number;
86
108
  height?: number;
87
109
  className?: string;
110
+ /** Extra classes for the inner frame, to override visuals. */
111
+ frameClassName?: string;
112
+ /** Inline styles for the inner frame. */
113
+ frameStyle?: CSSProperties;
88
114
  }
89
115
  /**
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`.
116
+ * A neutral seat: a frame with generic visual states and named slots. Put
117
+ * whatever you want into `header` / `children` / `footer`, and use
118
+ * `above` / `below` / `topLeft` / `topRight` / `topCenter` / `overlay` for
119
+ * floating bits. It knows nothing about any particular game.
93
120
  */
94
- declare function Seat({ isActive, isWinner, isDimmed, onClick, header, badge, children, footer, overlay, width, height, className, }: SeatProps): react.JSX.Element;
121
+ declare function Seat({ isActive, isWinner, isDimmed, animated, winnerCrown, onClick, header, children, footer, badge, overlay, above, below, topLeft, topRight, topCenter, width, height, className, frameClassName, frameStyle, }: SeatProps): react.JSX.Element;
95
122
 
96
123
  interface PotProps {
97
124
  amount: number;
@@ -99,10 +126,12 @@ interface PotProps {
99
126
  label?: string;
100
127
  /** Formatter for the amount; defaults to Big-Blind formatting. */
101
128
  format?: (amount: number) => string;
129
+ /** Animate the amount counting up when it changes. Default true. */
130
+ animate?: boolean;
102
131
  className?: string;
103
132
  }
104
133
  /** A small, standalone pot pill. Optional — the table does not require it. */
105
- declare function Pot({ amount, label, format, className }: PotProps): react.JSX.Element;
134
+ declare function Pot({ amount, label, format, animate, className }: PotProps): react.JSX.Element;
106
135
 
107
136
  interface CardTableProps {
108
137
  /** Number of seats (2–10). Default 6. */
@@ -115,6 +144,14 @@ interface CardTableProps {
115
144
  yourSeatIndex?: number | null;
116
145
  /** Optional content in the middle of the table (cards, pot, anything). */
117
146
  center?: ReactNode;
147
+ /** Ambient spotlight + felt shimmer. Default true. */
148
+ ambient?: boolean;
149
+ /** Highlight the table edge (e.g. when it is your turn). Default false. */
150
+ yourTurn?: boolean;
151
+ /** Winning seat indices; when non-empty, confetti falls. */
152
+ winnerSeats?: number[];
153
+ /** Show confetti for winners. Default true. */
154
+ confetti?: boolean;
118
155
  /** Seat frame size used for layout. */
119
156
  seatWidth?: number;
120
157
  seatHeight?: number;
@@ -125,6 +162,6 @@ interface CardTableProps {
125
162
  * onto its container and arranges `seatCount` seats around the felt. Purely
126
163
  * presentational: supply seat content and an optional `center` slot.
127
164
  */
128
- declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
165
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, ambient, yourTurn, winnerSeats, confetti, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
129
166
 
130
167
  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,6 +1,7 @@
1
1
  import * as react from 'react';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, CSSProperties } from 'react';
3
3
 
4
+ /** Join class names and resolve conflicting Tailwind utilities (last wins). */
4
5
  declare function cn(...parts: Array<string | false | null | undefined>): string;
5
6
 
6
7
  /** Big Blind value used for stack display. */
@@ -39,7 +40,8 @@ interface PlayingCardProps {
39
40
  }
40
41
  /**
41
42
  * A single playing card. Size is driven by the `size` prop (never viewport
42
- * breakpoints) so cards scale predictably inside any container.
43
+ * breakpoints) so cards scale predictably inside any container. Small sizes
44
+ * (xs / sm) keep only the upright corner index.
43
45
  */
44
46
  declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
45
47
  interface CardBackProps {
@@ -52,46 +54,71 @@ interface HoleCardsProps {
52
54
  cards: (string | null)[];
53
55
  size?: CardSize;
54
56
  className?: string;
57
+ /** Animate the cards dealing in, one after another. Default true. */
58
+ deal?: boolean;
55
59
  }
56
- /** A row of a player's two hole cards. */
57
- declare function HoleCards({ cards, size, className }: HoleCardsProps): react.JSX.Element;
60
+ /** A row of a player's hole cards. */
61
+ declare function HoleCards({ cards, size, className, deal }: HoleCardsProps): react.JSX.Element;
58
62
  interface CommunityCardsProps {
59
63
  cards: (string | null)[];
60
64
  size?: CardSize;
61
65
  className?: string;
66
+ /** Animate each card with a 3D flip. Default true. */
67
+ animated?: boolean;
68
+ /** Reserve a fixed number of slots so the row width never changes. */
69
+ slots?: number;
62
70
  }
63
- /** The five community cards, each dealt with a staggered flip animation. */
64
- declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
71
+ /** A row of community cards, each flipping in with a staggered delay. */
72
+ declare function CommunityCards({ cards, size, className, animated, slots }: CommunityCardsProps): react.JSX.Element;
65
73
 
66
74
  interface SeatProps {
67
- /** Turn/active highlight (generic). */
75
+ /** Turn/active highlight (generic). Animated by default. */
68
76
  isActive?: boolean;
69
- /** Winner highlight (generic). */
77
+ /** Winner highlight (generic). Animated by default. */
70
78
  isWinner?: boolean;
71
79
  /** Dimmed, e.g. folded or out (generic). */
72
80
  isDimmed?: boolean;
81
+ /** Enable the turn/winner glow animations. Default true. */
82
+ animated?: boolean;
83
+ /** Render a default 👑 in the top-left corner when `isWinner`. Default false. */
84
+ winnerCrown?: boolean;
73
85
  /** Optional click handler (e.g. "sit here"). */
74
86
  onClick?: () => void;
75
- /** Slot: top row — name / badges. */
87
+ /** Slot: top row inside the frame — name / badges. */
76
88
  header?: ReactNode;
77
- /** Slot: top-right floating element — e.g. a dealer button. */
78
- badge?: ReactNode;
79
89
  /** Slot: center content — 0..N cards, avatar, anything. */
80
90
  children?: ReactNode;
81
- /** Slot: bottom row — stack / bet / status. */
91
+ /** Slot: bottom row inside the frame — stack / status. */
82
92
  footer?: ReactNode;
83
- /** Slot: overlay — e.g. an "All-in" label. */
93
+ /** Slot: floating element at the frame's top-right (legacy alias of topRight). */
94
+ badge?: ReactNode;
95
+ /** Slot: overlay centered over the frame. */
84
96
  overlay?: ReactNode;
97
+ /** Slot: floating content above the frame, centered (e.g. an action bubble). */
98
+ above?: ReactNode;
99
+ /** Slot: content below the frame, centered (e.g. countdown / bet pill). */
100
+ below?: ReactNode;
101
+ /** Slot: floating content at the frame's top-left corner (e.g. a crown). */
102
+ topLeft?: ReactNode;
103
+ /** Slot: floating content at the frame's top-right corner (e.g. a kick button). */
104
+ topRight?: ReactNode;
105
+ /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */
106
+ topCenter?: ReactNode;
85
107
  width?: number;
86
108
  height?: number;
87
109
  className?: string;
110
+ /** Extra classes for the inner frame, to override visuals. */
111
+ frameClassName?: string;
112
+ /** Inline styles for the inner frame. */
113
+ frameStyle?: CSSProperties;
88
114
  }
89
115
  /**
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`.
116
+ * A neutral seat: a frame with generic visual states and named slots. Put
117
+ * whatever you want into `header` / `children` / `footer`, and use
118
+ * `above` / `below` / `topLeft` / `topRight` / `topCenter` / `overlay` for
119
+ * floating bits. It knows nothing about any particular game.
93
120
  */
94
- declare function Seat({ isActive, isWinner, isDimmed, onClick, header, badge, children, footer, overlay, width, height, className, }: SeatProps): react.JSX.Element;
121
+ declare function Seat({ isActive, isWinner, isDimmed, animated, winnerCrown, onClick, header, children, footer, badge, overlay, above, below, topLeft, topRight, topCenter, width, height, className, frameClassName, frameStyle, }: SeatProps): react.JSX.Element;
95
122
 
96
123
  interface PotProps {
97
124
  amount: number;
@@ -99,10 +126,12 @@ interface PotProps {
99
126
  label?: string;
100
127
  /** Formatter for the amount; defaults to Big-Blind formatting. */
101
128
  format?: (amount: number) => string;
129
+ /** Animate the amount counting up when it changes. Default true. */
130
+ animate?: boolean;
102
131
  className?: string;
103
132
  }
104
133
  /** A small, standalone pot pill. Optional — the table does not require it. */
105
- declare function Pot({ amount, label, format, className }: PotProps): react.JSX.Element;
134
+ declare function Pot({ amount, label, format, animate, className }: PotProps): react.JSX.Element;
106
135
 
107
136
  interface CardTableProps {
108
137
  /** Number of seats (2–10). Default 6. */
@@ -115,6 +144,14 @@ interface CardTableProps {
115
144
  yourSeatIndex?: number | null;
116
145
  /** Optional content in the middle of the table (cards, pot, anything). */
117
146
  center?: ReactNode;
147
+ /** Ambient spotlight + felt shimmer. Default true. */
148
+ ambient?: boolean;
149
+ /** Highlight the table edge (e.g. when it is your turn). Default false. */
150
+ yourTurn?: boolean;
151
+ /** Winning seat indices; when non-empty, confetti falls. */
152
+ winnerSeats?: number[];
153
+ /** Show confetti for winners. Default true. */
154
+ confetti?: boolean;
118
155
  /** Seat frame size used for layout. */
119
156
  seatWidth?: number;
120
157
  seatHeight?: number;
@@ -125,6 +162,6 @@ interface CardTableProps {
125
162
  * onto its container and arranges `seatCount` seats around the felt. Purely
126
163
  * presentational: supply seat content and an optional `center` slot.
127
164
  */
128
- declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
165
+ declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, ambient, yourTurn, winnerSeats, confetti, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
129
166
 
130
167
  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
@@ -93,7 +94,7 @@ function PlayingCard({ card, size = "md", className }) {
93
94
  const { rank, suit } = parseCard(card);
94
95
  const suitSymbol = getSuitSymbol(suit);
95
96
  const suitClass = getSuitClass(suit);
96
- const showCenter = size === "md" || size === "lg";
97
+ const large = size === "md" || size === "lg";
97
98
  return /* @__PURE__ */ jsxs(
98
99
  "div",
99
100
  {
@@ -110,11 +111,11 @@ function PlayingCard({ card, size = "md", className }) {
110
111
  /* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
111
112
  /* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
112
113
  ] }),
113
- /* @__PURE__ */ jsxs("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
114
+ large && /* @__PURE__ */ jsxs("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
114
115
  /* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
115
116
  /* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
116
117
  ] }),
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
+ large && /* @__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
  ]
119
120
  }
120
121
  );
@@ -147,11 +148,20 @@ function CardBack({ size = "md", className }) {
147
148
  }
148
149
  );
149
150
  }
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)) });
151
+ function HoleCards({ cards, size = "lg", className, deal = true }) {
152
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-2", className), children: cards.map((card, i) => /* @__PURE__ */ jsx("div", { className: cn(deal && "animate-deal-in"), style: deal ? { animationDelay: `${i * 100}ms` } : void 0, children: /* @__PURE__ */ jsx(PlayingCard, { card, size }) }, i)) });
152
153
  }
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)) });
154
+ function CommunityCards({ cards, size = "md", className, animated = true, slots }) {
155
+ const count = slots ?? cards.length;
156
+ const list = Array.from({ length: count }, (_, i) => cards[i] ?? null);
157
+ return /* @__PURE__ */ jsx("div", { className: cn("flex gap-1.5", animated && "[perspective:800px]", className), children: list.map((card, i) => /* @__PURE__ */ jsx("div", { className: SIZE_CLASSES[size], style: { aspectRatio: "10/14" }, children: card ? /* @__PURE__ */ jsx(
158
+ "div",
159
+ {
160
+ className: cn("h-full w-full", animated && "animate-card-flip"),
161
+ style: animated ? { animationDelay: `${i * 70}ms` } : void 0,
162
+ children: /* @__PURE__ */ jsx(PlayingCard, { card, size })
163
+ }
164
+ ) : null }, i)) });
155
165
  }
156
166
 
157
167
  // src/components/Seat.tsx
@@ -160,17 +170,33 @@ function Seat({
160
170
  isActive = false,
161
171
  isWinner = false,
162
172
  isDimmed = false,
173
+ animated = true,
174
+ winnerCrown = false,
163
175
  onClick,
164
176
  header,
165
- badge,
166
177
  children,
167
178
  footer,
179
+ badge,
168
180
  overlay,
181
+ above,
182
+ below,
183
+ topLeft,
184
+ topRight,
185
+ topCenter,
169
186
  width = 160,
170
187
  height = 176,
171
- className
188
+ className,
189
+ frameClassName,
190
+ frameStyle
172
191
  }) {
173
- return /* @__PURE__ */ jsxs2("div", { className: cn("relative", className), style: { width, height }, children: [
192
+ const activeClass = animated ? "border-amber-400 animate-turn-glow" : "border-amber-400 shadow-[0_0_18px_rgba(243,156,18,0.5)]";
193
+ const winnerClass = animated ? "border-amber-400 animate-winner-glow" : "border-amber-400 shadow-[0_0_0_3px_rgba(243,156,18,0.9)]";
194
+ const effTopLeft = topLeft !== void 0 ? topLeft : isWinner && winnerCrown ? /* @__PURE__ */ jsx2("span", { className: "text-3xl drop-shadow-[0_2px_4px_rgba(0,0,0,0.6)] animate-bounce", children: "\u{1F451}" }) : void 0;
195
+ return /* @__PURE__ */ jsxs2("div", { className: cn("relative flex flex-col items-center", className), style: { width }, children: [
196
+ topCenter !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1", children: topCenter }),
197
+ effTopLeft !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-3 -left-2 z-40", children: effTopLeft }),
198
+ topRight !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute -top-2 -right-2 z-40", children: topRight }),
199
+ above !== void 0 && /* @__PURE__ */ jsx2("div", { className: "pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2", children: above }),
174
200
  /* @__PURE__ */ jsxs2(
175
201
  "div",
176
202
  {
@@ -180,37 +206,64 @@ function Seat({
180
206
  onKeyDown: onClick ? (e) => {
181
207
  if (e.key === "Enter" || e.key === " ") onClick();
182
208
  } : void 0,
209
+ style: { width, height, ...frameStyle },
183
210
  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)]",
211
+ "relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition",
212
+ "border-neutral-700",
213
+ isActive && activeClass,
214
+ isWinner && winnerClass,
187
215
  isDimmed && "opacity-50",
188
- onClick && "cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-cyan-400"
216
+ onClick && "cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400",
217
+ frameClassName
189
218
  ),
190
219
  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 })
220
+ header !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full shrink-0 items-center justify-between gap-1 text-[11px]", children: header }),
221
+ /* @__PURE__ */ jsx2("div", { className: "flex min-h-0 flex-1 items-center justify-center", children }),
222
+ footer !== void 0 && /* @__PURE__ */ jsx2("div", { className: "flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300", children: footer }),
223
+ 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 })
194
224
  ]
195
225
  }
196
226
  ),
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 })
227
+ badge !== void 0 && /* @__PURE__ */ jsx2("div", { className: "absolute top-0 right-0 z-40", children: badge }),
228
+ below !== void 0 && /* @__PURE__ */ jsx2("div", { className: "mt-1 flex flex-col items-center gap-1", children: below })
199
229
  ] });
200
230
  }
201
231
 
202
232
  // src/components/Pot.tsx
203
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
204
- function Pot({ amount, label, format = formatBB, className }) {
233
+ import { useEffect, useRef, useState } from "react";
234
+ import { Fragment, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
235
+ function CountUp({ value, format }) {
236
+ const [shown, setShown] = useState(value);
237
+ const fromRef = useRef(value);
238
+ useEffect(() => {
239
+ const from = fromRef.current;
240
+ const to = value;
241
+ if (from === to) return;
242
+ let raf = 0;
243
+ const start = performance.now();
244
+ const dur = 500;
245
+ const tick = (now) => {
246
+ const t = Math.min(1, (now - start) / dur);
247
+ const eased = 1 - Math.pow(1 - t, 3);
248
+ setShown(Math.round(from + (to - from) * eased));
249
+ if (t < 1) raf = requestAnimationFrame(tick);
250
+ else fromRef.current = to;
251
+ };
252
+ raf = requestAnimationFrame(tick);
253
+ return () => cancelAnimationFrame(raf);
254
+ }, [value]);
255
+ return /* @__PURE__ */ jsx3(Fragment, { children: format(shown) });
256
+ }
257
+ function Pot({ amount, label, format = formatBB, animate = true, className }) {
205
258
  return /* @__PURE__ */ jsxs3("div", { className: cn("flex items-center gap-2 font-mono text-sm text-amber-400", className), children: [
206
259
  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) })
260
+ /* @__PURE__ */ jsx3("span", { className: "whitespace-nowrap rounded-full border border-amber-400/50 bg-neutral-900/80 px-3 py-1 shadow-inner", children: animate ? /* @__PURE__ */ jsx3(CountUp, { value: amount, format }) : format(amount) })
208
261
  ] });
209
262
  }
210
263
 
211
264
  // src/components/CardTable.tsx
212
- import { useRef, useLayoutEffect, useState, useMemo } from "react";
213
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
265
+ import { useRef as useRef2, useLayoutEffect, useState as useState2, useMemo } from "react";
266
+ import { Fragment as Fragment2, jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
214
267
  var DESIGN_W = 1200;
215
268
  var DESIGN_H = 900;
216
269
  var MARGIN_X = 164;
@@ -221,6 +274,7 @@ var FELT_GAP = 11;
221
274
  var BET_SCALE = 0.72;
222
275
  var BET_RX = TABLE_RX * BET_SCALE;
223
276
  var BET_RY = TABLE_RY * BET_SCALE;
277
+ var CONFETTI_COLORS = ["#f39c12", "#e74c3c", "#2ecc71", "#3498db", "#f1c40f", "#9b59b6"];
224
278
  function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
225
279
  const rad = angleDeg * Math.PI / 180;
226
280
  return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
@@ -251,13 +305,17 @@ function CardTable({
251
305
  renderSeat,
252
306
  yourSeatIndex = null,
253
307
  center,
308
+ ambient = true,
309
+ yourTurn = false,
310
+ winnerSeats = [],
311
+ confetti = true,
254
312
  seatWidth = 160,
255
313
  seatHeight = 176,
256
314
  className
257
315
  }) {
258
316
  const count = Math.max(2, Math.min(10, Math.floor(seatCount)));
259
- const outerRef = useRef(null);
260
- const [scale, setScale] = useState(1);
317
+ const outerRef = useRef2(null);
318
+ const [scale, setScale] = useState2(1);
261
319
  const layouts = useMemo(
262
320
  () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),
263
321
  [count, yourSeatIndex, seatWidth, seatHeight]
@@ -279,6 +337,7 @@ function CardTable({
279
337
  const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
280
338
  const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
281
339
  const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
340
+ const ringInset = { inset: `${MARGIN_Y + 2}px ${MARGIN_X + 2}px` };
282
341
  const betLineInset = {
283
342
  inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
284
343
  };
@@ -316,6 +375,17 @@ function CardTable({
316
375
  "aria-hidden": "true"
317
376
  }
318
377
  ),
378
+ ambient && /* @__PURE__ */ jsxs4(Fragment2, { children: [
379
+ /* @__PURE__ */ jsx4(
380
+ "div",
381
+ {
382
+ className: "absolute animate-spotlight rounded-[50%] bg-[radial-gradient(closest-side,rgba(255,214,120,0.14),transparent_70%)] blur-2xl",
383
+ style: feltInset,
384
+ "aria-hidden": "true"
385
+ }
386
+ ),
387
+ /* @__PURE__ */ jsx4("div", { className: "absolute overflow-hidden rounded-[50%]", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ jsx4("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%]" }) })
388
+ ] }),
319
389
  /* @__PURE__ */ jsx4(
320
390
  "div",
321
391
  {
@@ -325,12 +395,34 @@ function CardTable({
325
395
  }
326
396
  ),
327
397
  /* @__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" }) }),
398
+ yourTurn && /* @__PURE__ */ jsx4(
399
+ "div",
400
+ {
401
+ className: "pointer-events-none absolute z-10 animate-turn-glow rounded-[50%]",
402
+ style: ringInset,
403
+ "aria-hidden": "true"
404
+ }
405
+ ),
328
406
  center !== void 0 && /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center", children: center }),
329
407
  /* @__PURE__ */ jsx4("div", { className: "absolute inset-0 z-20", children: layouts.map(({ x, y, index, isYou }) => {
330
408
  const content = seats ? seats[index] ?? null : renderSeat ? renderSeat(index, isYou) : null;
331
409
  if (content === null) return null;
332
410
  return /* @__PURE__ */ jsx4("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: content }, index);
333
411
  }) }),
412
+ confetti && winnerSeats.length > 0 && /* @__PURE__ */ jsx4("div", { className: "pointer-events-none absolute inset-0 z-30 overflow-hidden", "aria-hidden": "true", children: Array.from({ length: 16 }).map((_, i) => /* @__PURE__ */ jsx4(
413
+ "span",
414
+ {
415
+ className: "absolute top-[34%] animate-confetti rounded-[2px]",
416
+ style: {
417
+ left: `${6 + i * 5.6}%`,
418
+ width: 6,
419
+ height: 11,
420
+ backgroundColor: CONFETTI_COLORS[i % CONFETTI_COLORS.length],
421
+ animationDelay: `${i % 8 * 90}ms`
422
+ }
423
+ },
424
+ i
425
+ )) }),
334
426
  /* @__PURE__ */ jsx4("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
335
427
  ]
336
428
  }