@stargazeryan/poker-ui 0.4.3 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.cts CHANGED
@@ -4,10 +4,140 @@ import { ReactNode, CSSProperties } from 'react';
4
4
  /** Join class names and resolve conflicting Tailwind utilities (last wins). */
5
5
  declare function cn(...parts: Array<string | false | null | undefined>): string;
6
6
 
7
- /** Big Blind value used for stack display. */
7
+ /**
8
+ * Default big blind that `formatBB` uses when you do not pass one.
9
+ *
10
+ * Kept for back-compat. Nothing in the package applies it on your behalf: the
11
+ * components render plain numbers unless you hand them a formatter, because a
12
+ * big blind is a poker rule and this is not a poker library.
13
+ */
8
14
  declare const BB = 20;
9
- /** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
10
- declare function formatBB(amount: number): string;
15
+ /**
16
+ * Format an amount as big blinds, e.g. 1234 -> "61.7 BB".
17
+ *
18
+ * This is a poker *helper*, not a default. Pass your own big blind — a
19
+ * fixed 20 is wrong for most tables:
20
+ *
21
+ * ```ts
22
+ * <Pot amount={1234} format={(n) => formatBB(n, 100)} />
23
+ * ```
24
+ */
25
+ declare function formatBB(amount: number, bb?: number): string;
26
+
27
+ /**
28
+ * Colour maths. Nothing here knows about cards, chips or games — this is the
29
+ * generic layer that contrast decisions are built on.
30
+ *
31
+ * It exists because the package needs to answer "would this be visible on top of
32
+ * that?" in more than one place (edge spots on a chip, and anything else that
33
+ * grows a light/dark ink choice later), and that question should not live inside
34
+ * a component or a game-specific module.
35
+ */
36
+ interface Rgba {
37
+ r: number;
38
+ g: number;
39
+ b: number;
40
+ /** 0–1. */
41
+ a: number;
42
+ }
43
+ /**
44
+ * Parse `#rgb`, `#rrggbb`, `rgb()` or `rgba()` into channels.
45
+ *
46
+ * Returns `null` for anything else — a `var(...)`, a named colour, a value with
47
+ * a color-mix() in it. Callers are expected to have a sensible fallback rather
48
+ * than guess, which is why this does not throw.
49
+ */
50
+ declare function parseColor(color: string): Rgba | null;
51
+ /** WCAG relative luminance (0 = black, 1 = white). `null` if unparseable. */
52
+ declare function relativeLuminance(color: string): number | null;
53
+ /** Composite a possibly translucent colour over an opaque one. */
54
+ declare function flatten(foreground: string, background: string): Rgba | null;
55
+ /**
56
+ * WCAG contrast ratio between two colours, 1–21. `foreground` may be
57
+ * translucent, in which case it is composited over `background` first — which is
58
+ * the question you actually have when you are choosing an ink colour.
59
+ *
60
+ * `null` if either colour is unparseable.
61
+ */
62
+ declare function contrastRatio(foreground: string, background: string): number | null;
63
+ /**
64
+ * Pick whichever candidate is more visible on `background`.
65
+ *
66
+ * Derived from measured contrast rather than a hand-tuned luminance threshold,
67
+ * so a mid-tone background gets the genuinely better of the two instead of
68
+ * whichever side of an arbitrary cut-off it fell on. Falls back to the first
69
+ * candidate when the background cannot be measured.
70
+ */
71
+ declare function mostVisible(candidates: readonly string[], background: string): string;
72
+
73
+ /**
74
+ * The size vocabulary, in one place.
75
+ *
76
+ * Cards and chips keep their own pixel tables — a `sm` card and a `sm` chip are
77
+ * different objects and pretending otherwise would be worse — but they share one
78
+ * list of names. That way adding an `xl` to the vocabulary is a single edit and
79
+ * a component cannot silently support a size the type does not mention.
80
+ */
81
+ declare const SIZES: readonly ["xs", "sm", "md", "lg"];
82
+ /** Any size in the vocabulary. */
83
+ type Size = (typeof SIZES)[number];
84
+ /** `PlayingCard` / `HoleCards` / `CommunityCards`: the full range. */
85
+ type CardSize = Size;
86
+ /** `Chip` / `ChipStack`: a chip has no `xs`. */
87
+ type ChipSize = Exclude<Size, "xs">;
88
+
89
+ /**
90
+ * Animation timing, in one place.
91
+ *
92
+ * These were magic numbers inside three components. They are kept short on
93
+ * purpose: the on-by-default animations are game feel, and a consumer who wants
94
+ * a different tempo turns them off rather than tuning them.
95
+ */
96
+ /** How long the pot takes to count from one value to the next, in ms. */
97
+ declare const COUNT_UP_MS = 500;
98
+ /** Stagger between hole cards dealing in, in ms. */
99
+ declare const DEAL_STAGGER_MS = 100;
100
+ /** Stagger between community cards flipping in, in ms. */
101
+ declare const FLIP_STAGGER_MS = 70;
102
+
103
+ /**
104
+ * Card codes and how to say them out loud. Pure functions, no React.
105
+ *
106
+ * This lives outside the component because two things need it: the artwork
107
+ * (`PlayingCard` / `CardBack`, for the accessible name) and any consumer
108
+ * building a hand list, a log line or a replay. A single vocabulary is what
109
+ * stops every call site inventing its own way of saying "As".
110
+ */
111
+ interface ParsedCard {
112
+ rank: string;
113
+ suit: string;
114
+ }
115
+ /**
116
+ * Split a compact card code like `"As"` or `"10h"` into rank and suit.
117
+ *
118
+ * Deliberately permissive: it takes the last character as the suit and trusts
119
+ * the rest, so `"xyz"` parses as rank `"xy"` / suit `"z"`. Validation is a known
120
+ * gap — use `describeCard`, which falls back to the raw code rather than
121
+ * pretending to know what it read.
122
+ */
123
+ declare function parseCard(card: string): ParsedCard;
124
+ /** The glyph for a suit letter. Unknown suits are passed through unchanged. */
125
+ declare function getSuitSymbol(suit: string): string;
126
+ /** `"A"` → `"Ace"`, `"10"` → `"Ten"`. Unknown ranks pass through. */
127
+ declare function getRankName(rank: string): string;
128
+ /** `"s"` → `"spades"`. Unknown suits pass through. */
129
+ declare function getSuitName(suit: string): string;
130
+ /** Hearts and diamonds are the red ones. */
131
+ declare function isRedSuit(suit: string): boolean;
132
+ /** Whether a suit letter is one this package knows how to draw. */
133
+ declare function isKnownSuit(suit: string): boolean;
134
+ /**
135
+ * A spoken description: `"As"` → `"Ace of spades"`, `null` → `"face-down card"`.
136
+ *
137
+ * Returns the raw code when the rank or suit is not recognised, rather than
138
+ * announcing something the card is not.
139
+ */
140
+ declare function describeCard(card: string | null): string;
11
141
 
12
142
  /**
13
143
  * Seat-ring helpers. Pure functions — they contain no game-specific rules.
@@ -25,18 +155,194 @@ interface NextSeatOptions {
25
155
  */
26
156
  declare function nextSeatIndex(current: number, seatCount: number, opts?: NextSeatOptions): number;
27
157
 
28
- type CardSize = "xs" | "sm" | "md" | "lg";
29
- /** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
30
- declare function parseCard(card: string): {
31
- rank: string;
32
- suit: string;
158
+ /**
159
+ * Pure ellipse geometry for the card table. No game rules, no React.
160
+ *
161
+ * Everything is derived from one parametric ellipse:
162
+ *
163
+ * x(θ) = cx + rx·cos θ
164
+ * y(θ) = cy + ry·sin θ
165
+ *
166
+ * with θ in degrees. Screen coordinates (y grows downward), so θ = 90° is the
167
+ * point directly BELOW the centre.
168
+ *
169
+ * Seat 0 sits at `startAngle` (default 90° = directly below the centre) and
170
+ * every following seat advances by `360 / seatCount`, so any count from 2 to 10
171
+ * spreads evenly. The betting line uses the same formula with a smaller radius.
172
+ */
173
+ interface Point {
174
+ x: number;
175
+ y: number;
176
+ }
177
+ /**
178
+ * Ring angle (degrees) of seat `index`, evenly divided around 360°.
179
+ * Seat 0 is at `startAngle` (default 90° = directly below the table centre).
180
+ */
181
+ declare function seatAngle(index: number, seatCount: number, startAngle?: number): number;
182
+ /** Point on the ellipse at `angleDeg` (degrees; 90° = below the centre). */
183
+ declare function ellipsePoint(cx: number, cy: number, rx: number, ry: number, angleDeg: number): Point;
184
+ /**
185
+ * `count` evenly spaced points on an ellipse, starting at `startAngle`
186
+ * (default 90° = directly below the centre). Handy for seat rings and betting
187
+ * lines alike: pass the ring radii, get the coordinates.
188
+ */
189
+ declare function ringPoints(count: number, cx: number, cy: number, rx: number, ry: number, startAngle?: number): Point[];
190
+
191
+ /**
192
+ * Pure layout math for the card table. No React, no game rules.
193
+ *
194
+ * Everything is derived from one parametric ellipse (see `geometry.ts`). The
195
+ * table, the felt, the rim, the seat ring and the betting line are ellipses
196
+ * sharing a centre and an aspect ratio, and they are all computed **here**.
197
+ *
198
+ * That matters: the betting line used to be computed inside `CardTable` from a
199
+ * module constant while the chips were positioned from the `betRadiusScale`
200
+ * prop, and the two drifted apart. One ellipse, one place that computes it.
201
+ */
202
+ /** Default design space. */
203
+ declare const DESIGN_W = 1200;
204
+ declare const DESIGN_H = 900;
205
+ /** Rail inset as a fraction of the design box (164/1200, 196/900). */
206
+ declare const DEFAULT_MARGIN_X: number;
207
+ declare const DEFAULT_MARGIN_Y: number;
208
+ /** Gap between the rail and the felt, in design-space px. */
209
+ declare const DEFAULT_FELT_GAP = 11;
210
+ /** Default radius of the bet ring, as a fraction of the table radius. */
211
+ declare const DEFAULT_BET_SCALE = 0.72;
212
+ /** Default minimum clearance between neighbouring seat frames. */
213
+ declare const DEFAULT_SEAT_GAP = 24;
214
+ /** Default preferred seat frame size. */
215
+ declare const DEFAULT_SEAT_W = 160;
216
+ declare const DEFAULT_SEAT_H = 176;
217
+ /** How far inside the rail the rim highlight sits, in design-space px. */
218
+ declare const RIM_GAP = 6;
219
+ /** How far inside the rail the your-turn glow sits, in design-space px. */
220
+ declare const GLOW_GAP = 2;
221
+ interface LayoutOptions {
222
+ /** Number of seats. */
223
+ count: number;
224
+ /** Design-space width / height. */
225
+ designWidth: number;
226
+ designHeight: number;
227
+ /** Rail inset as a fraction of designWidth / designHeight (0..0.5). */
228
+ marginX: number;
229
+ marginY: number;
230
+ /** Felt inset from the rail, in design-space px. */
231
+ feltGap: number;
232
+ /** Preferred (maximum) seat frame size. */
233
+ seatWidth: number;
234
+ seatHeight: number;
235
+ /**
236
+ * Minimum clearance in design-space px to keep between neighbouring seat
237
+ * frames. Frames are shrunk uniformly (never below `SEAT_SHRINK_FLOOR`) to
238
+ * honour it. `0` disables the shrink entirely.
239
+ */
240
+ seatGap: number;
241
+ /** Angle of seat 0, in degrees. */
242
+ startAngle: number;
243
+ /** Where the betting line sits, as a fraction of the table radii. Default 0.72. */
244
+ betRadiusScale?: number;
245
+ }
246
+ interface SeatLayout {
247
+ index: number;
248
+ /** Centre of the seat frame, in design space. */
249
+ x: number;
250
+ y: number;
251
+ /** Frame size the layout was computed for (≤ the preferred size when shrunk). */
252
+ width: number;
253
+ height: number;
254
+ }
255
+ /**
256
+ * How far an ellipse sits inside the design box, in design-space px. Spread it
257
+ * onto an absolutely-positioned element and the ellipse lands where the rest of
258
+ * the layout expects.
259
+ */
260
+ interface EllipseBox {
261
+ top: number;
262
+ right: number;
263
+ bottom: number;
264
+ left: number;
265
+ }
266
+ interface Layout {
267
+ /** Seat centre used for every ring, in design space. */
268
+ cx: number;
269
+ cy: number;
270
+ /** Radii of the outer table ellipse. */
271
+ tableRx: number;
272
+ tableRy: number;
273
+ /** Radii of the seat ring. */
274
+ ringRx: number;
275
+ ringRy: number;
276
+ /** Radii of the betting line. */
277
+ betRx: number;
278
+ betRy: number;
279
+ /** Seat frame size actually used. */
280
+ seatW: number;
281
+ seatH: number;
282
+ seats: SeatLayout[];
283
+ /** Every ellipse the table draws, ready to position. */
284
+ boxes: {
285
+ table: EllipseBox;
286
+ felt: EllipseBox;
287
+ rim: EllipseBox;
288
+ glow: EllipseBox;
289
+ bettingLine: EllipseBox;
290
+ };
291
+ }
292
+ /** Smallest fraction of the preferred seat size the auto-shrink will go to. */
293
+ declare const SEAT_SHRINK_FLOOR = 0.5;
294
+ /** Radii of the table ellipse for a given design box and rail inset. */
295
+ declare function tableRadii(designWidth: number, designHeight: number, marginX: number, marginY: number): {
296
+ rx: number;
297
+ ry: number;
33
298
  };
34
- declare function getSuitSymbol(suit: string): string;
299
+ /**
300
+ * Radii of the seat ring. Seats ride the ring at `+ seatSize / 2 + 6`, so the
301
+ * frames straddle the rail, but never past the design box (seats at the extreme
302
+ * left / right / top / bottom used to be clipped in narrow layouts).
303
+ */
304
+ declare function seatRingRadii(designWidth: number, designHeight: number, tableRx: number, tableRy: number, seatW: number, seatH: number, feltGap: number): {
305
+ rx: number;
306
+ ry: number;
307
+ };
308
+ /** Centre point of seat `index` on the seat ring, in design space. */
309
+ declare function seatPoint(index: number, count: number, cx: number, cy: number, ringRx: number, ringRy: number, startAngle?: number): {
310
+ x: number;
311
+ y: number;
312
+ };
313
+ /** Turn radii into the inset box of an absolutely-positioned ellipse. */
314
+ declare function ellipseBox(designWidth: number, designHeight: number, rx: number, ry: number): EllipseBox;
315
+ /**
316
+ * Smallest gap between any two seat frames, over every pair. Positive means no
317
+ * frames overlap. Returns `Infinity` for fewer than two seats.
318
+ */
319
+ declare function minSeatSeparation(seats: SeatLayout[]): number;
320
+ /**
321
+ * Pick the largest uniform scale (1 → `SEAT_SHRINK_FLOOR`, in 1% steps) at which
322
+ * every pair of seat frames clears `seatGap`. Returns the scale to apply.
323
+ *
324
+ * A 10-seat table at the default 160x176 frames only leaves ~26px between
325
+ * neighbours, which reads as crowded; shrinking them a little spreads the ring
326
+ * out. It is also what makes 11–16 seats possible at all. Wide tables (2–8
327
+ * seats) are unaffected — they pass on the first step.
328
+ */
329
+ declare function fitSeatScale(opts: LayoutOptions): number;
330
+ /**
331
+ * Everything the table draws for one configuration: the table ellipse, the felt,
332
+ * the rim, the glow ring, the betting line, the seat ring and every seat centre.
333
+ */
334
+ declare function layoutSeats(opts: LayoutOptions): Layout;
335
+
35
336
  interface PlayingCardProps {
36
337
  /** Compact card code ("As", "10h") or null to render a face-down card back. */
37
338
  card: string | null;
38
339
  /** xs / sm show a single, larger, centered index; md / lg show corner + center. */
39
340
  size?: CardSize;
341
+ /**
342
+ * Accessible name. Defaults to `describeCard(card)` — "Ace of spades" rather
343
+ * than the fragments "A" and "♠" a screen reader would otherwise read out.
344
+ */
345
+ label?: string;
40
346
  className?: string;
41
347
  }
42
348
  /**
@@ -44,22 +350,30 @@ interface PlayingCardProps {
44
350
  * breakpoints) so cards scale predictably inside any container. Small sizes
45
351
  * (xs / sm) show only a larger upright index, centered in the card.
46
352
  */
47
- declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
353
+ declare function PlayingCard({ card, size, label, className }: PlayingCardProps): react.JSX.Element;
48
354
  interface CardBackProps {
49
355
  size?: CardSize;
356
+ /** Accessible name. Defaults to "face-down card". */
357
+ label?: string;
50
358
  className?: string;
51
359
  }
52
360
  /** The face-down card back (dark red + gold rosette). */
53
- declare function CardBack({ size, className }: CardBackProps): react.JSX.Element;
361
+ declare function CardBack({ size, label, className }: CardBackProps): react.JSX.Element;
54
362
  interface HoleCardsProps {
55
363
  cards: (string | null)[];
56
364
  size?: CardSize;
57
365
  className?: string;
58
366
  /** Animate the cards dealing in, one after another. Default true. */
59
367
  deal?: boolean;
368
+ /**
369
+ * Change this to replay the deal animation for an unchanged hand (e.g. a hand
370
+ * id or a round number). Without it, a card whose value changed re-deals on
371
+ * its own, but a new hand holding the same two cards looks identical.
372
+ */
373
+ dealKey?: string | number;
60
374
  }
61
375
  /** A row of a player's hole cards. */
62
- declare function HoleCards({ cards, size, className, deal }: HoleCardsProps): react.JSX.Element;
376
+ declare function HoleCards({ cards, size, className, deal, dealKey }: HoleCardsProps): react.JSX.Element;
63
377
  interface CommunityCardsProps {
64
378
  cards: (string | null)[];
65
379
  size?: CardSize;
@@ -72,6 +386,126 @@ interface CommunityCardsProps {
72
386
  /** A row of community cards, each flipping in with a staggered delay. */
73
387
  declare function CommunityCards({ cards, size, className, animated, slots }: CommunityCardsProps): react.JSX.Element;
74
388
 
389
+ /**
390
+ * Chip denominations and the breakdown of an amount into chips. Pure functions,
391
+ * no React — the same split as `geometry.ts` and `layout.ts`.
392
+ *
393
+ * Amounts are in **chip units** (the smallest chip is 1), not in big blinds.
394
+ */
395
+ interface Denomination {
396
+ /** Face value of the chip. */
397
+ value: number;
398
+ /**
399
+ * Printed on the chip face. Defaults to the value, which is what you want for
400
+ * a 5 or a 100; use it for a "$5" or a "5K" if your table reads differently.
401
+ */
402
+ label?: string;
403
+ /**
404
+ * CSS colour for the body. Used directly as the SVG `fill`, so it can be any
405
+ * CSS colour — including `var(--color-chip-red)`.
406
+ */
407
+ color: string;
408
+ /**
409
+ * Colour of the eight edge spots. Defaults to white, or to a dark spot on a
410
+ * light body — see `chipSpotColor`.
411
+ */
412
+ spotColor?: string;
413
+ /**
414
+ * Optional `fill-*` utility applied on top of `color`. The default set uses
415
+ * these so the `--color-chip-*` theme tokens are actually emitted by Tailwind
416
+ * (it only outputs a theme variable something references) and can therefore be
417
+ * overridden from a consumer's `@theme` block like every other token.
418
+ *
419
+ * A class wins over the `fill` attribute, so `color` acts as the fallback.
420
+ */
421
+ className?: string;
422
+ }
423
+ /**
424
+ * The classic low-stakes set: white 1, red 5, blue 10, green 25, black 100.
425
+ *
426
+ * Blue is the one that varies between houses — plenty of rooms use blue for
427
+ * $10 and plenty for $20. Ten is the default here; override
428
+ * `denominations` to use twenty.
429
+ */
430
+ declare const DEFAULT_DENOMINATIONS: Denomination[];
431
+ /**
432
+ * Spot colour for a chip body: whichever of the two inks is measurably more
433
+ * visible on it.
434
+ *
435
+ * Derived from contrast rather than a luminance threshold, because the obvious
436
+ * implementation — always white spots — puts the white chip
437
+ * (`--color-chip-white`, #ecf0f1) at a 1.13:1 contrast ratio against its own
438
+ * body, so it renders as a blank oval with no edge at all. Colours we cannot
439
+ * measure keep white spots.
440
+ */
441
+ declare function chipSpotColor(bodyColor: string): string;
442
+ interface ChipStackEntry extends Denomination {
443
+ /** How many chips of this denomination the amount needs. */
444
+ count: number;
445
+ }
446
+ /**
447
+ * Break an amount into chips, largest denomination first (the greedy breakdown
448
+ * a dealer actually uses).
449
+ *
450
+ * Anything below the smallest denomination is left out rather than rounded up —
451
+ * `chipsTotal()` tells you what the chips add up to, so you can spot the
452
+ * remainder. Non-integer and negative amounts are floored/clamped to 0.
453
+ */
454
+ declare function chipBreakdown(amount: number, denominations?: Denomination[]): ChipStackEntry[];
455
+ /** What a set of chip entries adds up to. Compare with the amount to see the remainder. */
456
+ declare function chipsTotal(entries: ChipStackEntry[]): number;
457
+ /** Total number of physical chips in a breakdown. */
458
+ declare function chipCount(entries: ChipStackEntry[]): number;
459
+
460
+ interface ChipProps {
461
+ /** Printed on the centre of the face. */
462
+ value?: number | string;
463
+ /** CSS colour for the body. */
464
+ color?: string;
465
+ /** `fill-*` utility applied over `color` — see `Denomination.className`. */
466
+ colorClassName?: string;
467
+ /** Colour of the eight edge spots. Defaults to `chipSpotColor(color)`. */
468
+ spotColor?: string;
469
+ /** Default `"md"`. */
470
+ size?: ChipSize;
471
+ /** Accessible name. Defaults to `"<value> chip"`. */
472
+ label?: string;
473
+ className?: string;
474
+ }
475
+ /**
476
+ * A single chip, drawn as SVG: a coloured disc with a darker edge for
477
+ * thickness, eight white edge spots, a rim highlight and a white centre carrying
478
+ * the value.
479
+ *
480
+ * The colours come from `--color-chip-*` via `fill-*` utilities, so the chip
481
+ * palette is themeable like every other token.
482
+ */
483
+ declare function Chip({ value, color, colorClassName, spotColor, size, label, className, }: ChipProps): react.JSX.Element;
484
+ interface ChipStackProps {
485
+ /** Amount in chip units — see `chipBreakdown`. */
486
+ amount: number;
487
+ /** Default `"md"`. */
488
+ size?: ChipSize;
489
+ /** Default `DEFAULT_DENOMINATIONS` (white 1, red 5, blue 10, green 25, black 100). */
490
+ denominations?: Denomination[];
491
+ /**
492
+ * Most chips to draw, biggest denominations first. A 10,000 bet would
493
+ * otherwise be 100 black chips. The count of the chips left off is shown
494
+ * underneath. Default 12.
495
+ */
496
+ maxChips?: number;
497
+ /** Content under the stack — usually the formatted amount. */
498
+ label?: ReactNode;
499
+ className?: string;
500
+ }
501
+ /**
502
+ * The chips for an amount: a real stack, largest denomination on top.
503
+ *
504
+ * The accessible name is the full breakdown ("240 in 4 chips: 2x100, 1x25,
505
+ * 1x10"), so a screen reader gets the value rather than four anonymous images.
506
+ */
507
+ declare function ChipStack({ amount, size, denominations, maxChips, label, className, }: ChipStackProps): react.JSX.Element;
508
+
75
509
  interface SeatProps {
76
510
  /** Turn/active highlight (generic). Animated by default. */
77
511
  isActive?: boolean;
@@ -105,7 +539,13 @@ interface SeatProps {
105
539
  topRight?: ReactNode;
106
540
  /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */
107
541
  topCenter?: ReactNode;
542
+ /**
543
+ * Frame width. Defaults to the size the surrounding `CardTable` laid out for
544
+ * this seat, or 160 outside a table — the table is the one that knows how much
545
+ * room the ring has.
546
+ */
108
547
  width?: number;
548
+ /** Frame height. Defaults to the laid-out size, or 176 outside a table. */
109
549
  height?: number;
110
550
  className?: string;
111
551
  /** Extra classes for the inner frame, to override visuals. */
@@ -125,7 +565,10 @@ interface PotProps {
125
565
  amount: number;
126
566
  /** Optional label, e.g. "Main" / "Side". */
127
567
  label?: string;
128
- /** Formatter for the amount; defaults to Big-Blind formatting. */
568
+ /**
569
+ * Formatter for the amount. Defaults to a plain, locale-grouped number —
570
+ * pass one if your amounts have a unit.
571
+ */
129
572
  format?: (amount: number) => string;
130
573
  /** Animate the amount counting up when it changes. Default true. */
131
574
  animate?: boolean;
@@ -134,14 +577,54 @@ interface PotProps {
134
577
  /** A small, standalone pot pill. Optional — the table does not require it. */
135
578
  declare function Pot({ amount, label, format, animate, className }: PotProps): react.JSX.Element;
136
579
 
580
+ /** Geometry CardTable laid a seat frame out for, handed to `renderSeat`. */
581
+ interface SeatBox {
582
+ index: number;
583
+ isYou: boolean;
584
+ /** Centre of the frame, in design space. */
585
+ x: number;
586
+ y: number;
587
+ /** Frame size the ring was laid out for (≤ `seatWidth`/`seatHeight`). */
588
+ width: number;
589
+ height: number;
590
+ }
137
591
  interface CardTableProps {
138
- /** Number of seats (2–10). Default 6. */
592
+ /**
593
+ * Number of seats. Default 6.
594
+ *
595
+ * There is no upper limit. The seat ring shrinks the frames until `seatGap` is
596
+ * honoured, so 11–16 seats lay out fine (measured: 11 -> 126x139 frames,
597
+ * 16 -> 80x88). Past roughly 16 the frames hit the 50% shrink floor and start
598
+ * to overlap — that is the caller's call, not something to clamp silently.
599
+ */
139
600
  seatCount?: number;
140
601
  /** Optional per-seat content. Length should equal `seatCount`. */
141
602
  seats?: (ReactNode | null)[];
142
- /** Alternative to `seats`: render a seat by index. */
143
- renderSeat?: (index: number, isYou: boolean) => ReactNode;
144
- /** Which seat belongs to the viewer; rotates that seat to the bottom. */
603
+ /**
604
+ * Alternative to `seats`: render a seat by index. Receives the laid-out
605
+ * frame box as a third argument, so an auto-shrunk seat can be sized to
606
+ * match the ring: `<Seat width={box.width} height={box.height} />`.
607
+ */
608
+ renderSeat?: (index: number, isYou: boolean, box: SeatBox) => ReactNode;
609
+ /**
610
+ * Optional per-seat "bet" content, auto-positioned on the betting line
611
+ * (`betRadiusScale`) along the seat-to-center radius, so it faces the middle
612
+ * of the table. Length should equal `seatCount`; `null` entries are skipped.
613
+ */
614
+ bets?: (ReactNode | null)[];
615
+ /** Alternative to `bets`: render a bet by index (return `null` to skip). */
616
+ renderBet?: (index: number) => ReactNode;
617
+ /**
618
+ * Radius of the bet ring, as a fraction of the table radius. Default 0.72.
619
+ * The drawn betting line follows this value. Clamped to 0.2–1.
620
+ */
621
+ betRadiusScale?: number;
622
+ /**
623
+ * Angle (degrees) of seat 0. Default 90° = directly below the table centre.
624
+ * Seats advance by `360 / seatCount` from there, so 2–10 players spread evenly.
625
+ */
626
+ startAngle?: number;
627
+ /** Which seat is the viewer (adds no offset; only flags `isYou`). Default null. */
145
628
  yourSeatIndex?: number | null;
146
629
  /** Optional content in the middle of the table (cards, pot, anything). */
147
630
  center?: ReactNode;
@@ -153,16 +636,57 @@ interface CardTableProps {
153
636
  winnerSeats?: number[];
154
637
  /** Show confetti for winners. Default true. */
155
638
  confetti?: boolean;
156
- /** Seat frame size used for layout. */
639
+ /**
640
+ * Confetti colours. Defaults to a party palette. This is a prop rather than a
641
+ * theme token because the colours land in inline styles, which Tailwind's
642
+ * tree-shaking cannot see — a custom property nothing references never gets
643
+ * emitted.
644
+ */
645
+ confettiColors?: string[];
646
+ /** Preferred seat frame width. Default 160. */
157
647
  seatWidth?: number;
648
+ /** Preferred seat frame height. Default 176. */
158
649
  seatHeight?: number;
650
+ /**
651
+ * Minimum clearance to keep between neighbouring seat frames, in design-space
652
+ * px. Frames shrink uniformly (down to 50%) until it is honoured, which is what
653
+ * keeps 9–10 seat tables from looking cramped and what lets 11–16 seat tables
654
+ * lay out at all. `0` disables it.
655
+ * Only applies when `renderSeat` is used (it is the only form that can size
656
+ * the frames); the `seats` array keeps the size you were given.
657
+ * Default 24.
658
+ */
659
+ seatGap?: number;
660
+ /** Design-space width. Default 1200. */
661
+ designWidth?: number;
662
+ /** Design-space height. Default 900. */
663
+ designHeight?: number;
664
+ /**
665
+ * Rail inset as a fraction of the design box (default 164/1200).
666
+ * Proportional so a custom design box stays in proportion.
667
+ */
668
+ marginX?: number;
669
+ /** Rail inset as a fraction of the design box (default 196/900). */
670
+ marginY?: number;
671
+ /** Felt inset from the rail, in design-space px. Default 11. */
672
+ feltGap?: number;
673
+ /**
674
+ * How the design box relates to the container.
675
+ * - `"design"` (default): scale the fixed design box to fit — preserves the
676
+ * table's proportions but letterboxes non-matching containers.
677
+ * - `"container"`: derive the design width from the container's aspect ratio
678
+ * (clamped to 0.5–2.5) so the table fills the box instead of letterboxing.
679
+ * Use this for phone-portrait layouts. Pair with `designHeight` to set the
680
+ * scale.
681
+ */
682
+ fit?: "design" | "container";
159
683
  className?: string;
160
684
  }
161
685
  /**
162
- * A responsive, game-agnostic card table. It scales a fixed 1200x900 design
163
- * onto its container and arranges `seatCount` seats around the felt. Purely
164
- * presentational: supply seat content and an optional `center` slot.
686
+ * A responsive, game-agnostic card table. It scales a design box (1200x900 by
687
+ * default) onto its container and arranges `seatCount` seats around the felt.
688
+ * Purely presentational: supply seat content and an optional `center` slot.
165
689
  */
166
- declare function CardTable({ seatCount, seats, renderSeat, yourSeatIndex, center, ambient, yourTurn, winnerSeats, confetti, seatWidth, seatHeight, className, }: CardTableProps): react.JSX.Element;
690
+ declare function CardTable({ seatCount, seats, renderSeat, bets, renderBet, betRadiusScale, startAngle, yourSeatIndex, center, ambient, yourTurn, winnerSeats, confetti, confettiColors, seatWidth, seatHeight, seatGap, designWidth, designHeight, marginX, marginY, feltGap, fit, className, }: CardTableProps): react.JSX.Element;
167
691
 
168
- 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 };
692
+ export { BB, COUNT_UP_MS, CardBack, type CardBackProps, type CardSize, CardTable, type CardTableProps, Chip, type ChipProps, type ChipSize, ChipStack, type ChipStackEntry, type ChipStackProps, CommunityCards, type CommunityCardsProps, DEAL_STAGGER_MS, DEFAULT_BET_SCALE, DEFAULT_DENOMINATIONS, DEFAULT_FELT_GAP, DEFAULT_MARGIN_X, DEFAULT_MARGIN_Y, DEFAULT_SEAT_GAP, DEFAULT_SEAT_H, DEFAULT_SEAT_W, DESIGN_H, DESIGN_W, type Denomination, type EllipseBox, FLIP_STAGGER_MS, GLOW_GAP, HoleCards, type HoleCardsProps, type Layout, type LayoutOptions, type NextSeatOptions, type ParsedCard, PlayingCard, type PlayingCardProps, type Point, CardTable as PokerTable, type CardTableProps as PokerTableProps, Pot, type PotProps, RIM_GAP, type Rgba, SEAT_SHRINK_FLOOR, SIZES, Seat, type SeatBox, type SeatLayout, type SeatProps, type Size, chipBreakdown, chipCount, chipSpotColor, chipsTotal, cn, contrastRatio, describeCard, ellipseBox, ellipsePoint, fitSeatScale, flatten, formatBB, getRankName, getSuitName, getSuitSymbol, isKnownSuit, isRedSuit, layoutSeats, minSeatSeparation, mostVisible, nextSeatIndex, parseCard, parseColor, relativeLuminance, ringPoints, seatAngle, seatPoint, seatRingRadii, tableRadii };