@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/README.md +205 -12
- package/dist/index.cjs +746 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -23
- package/dist/index.d.ts +547 -23
- package/dist/index.js +696 -168
- package/dist/index.js.map +1 -1
- package/package.json +27 -7
- package/src/styles.css +128 -95
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cn.ts","../src/format.ts","../src/seat.ts","../src/components/PlayingCard.tsx","../src/components/Seat.tsx","../src/components/Pot.tsx","../src/components/CardTable.tsx"],"sourcesContent":["import { twMerge } from \"tailwind-merge\";\n\n/** Join class names and resolve conflicting Tailwind utilities (last wins). */\nexport function cn(...parts: Array<string | false | null | undefined>): string {\n return twMerge(parts.filter(Boolean).join(\" \"));\n}\n","/** Big Blind value used for stack display. */\nexport const BB = 20;\n\n/** Format a chip amount as a Big-Blind string, e.g. 1234 -> \"61.7 BB\". */\nexport function formatBB(amount: number): string {\n const bb = amount / BB;\n return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;\n}\n","/**\n * Seat-ring helpers. Pure functions — they contain no game-specific rules.\n */\n\nexport interface NextSeatOptions {\n /** +1 for clockwise, -1 for counter-clockwise. Default +1. */\n dir?: 1 | -1;\n /** Return true to skip a seat (e.g. folded / empty / out). */\n skip?: (index: number) => boolean;\n}\n\n/**\n * Return the index of the next seat on the ring after `current`, skipping any\n * seat matched by `opts.skip`. Wraps around. Returns `current` if every other\n * seat is skipped.\n */\nexport function nextSeatIndex(current: number, seatCount: number, opts: NextSeatOptions = {}): number {\n const n = Math.max(1, Math.floor(seatCount));\n const dir = opts.dir ?? 1;\n const skip = opts.skip;\n const start = ((Math.floor(current) % n) + n) % n;\n\n let i = start;\n for (let step = 0; step < n - 1; step++) {\n i = (i + dir + n) % n;\n if (!skip || !skip(i)) return i;\n }\n return start;\n}\n","import { cn } from \"../cn\";\n\nexport type CardSize = \"xs\" | \"sm\" | \"md\" | \"lg\";\n\nconst SIZE_CLASSES: Record<CardSize, string> = {\n xs: \"w-8 h-11\",\n sm: \"w-10 h-14\",\n md: \"w-16 h-22\",\n lg: \"w-24 h-32\",\n};\n\nconst CORNER_RANK: Record<CardSize, string> = {\n xs: \"text-[11px]\",\n sm: \"text-sm\",\n md: \"text-lg\",\n lg: \"text-2xl\",\n};\nconst CORNER_SUIT: Record<CardSize, string> = {\n xs: \"text-[9px]\",\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n};\nconst CENTER_SUIT: Record<CardSize, string> = {\n xs: \"text-base\",\n sm: \"text-xl\",\n md: \"text-3xl\",\n lg: \"text-5xl\",\n};\n// Small cards (xs/sm) show a single, larger index centered in the card.\nconst SMALL_RANK: Partial<Record<CardSize, string>> = {\n xs: \"text-base\",\n sm: \"text-xl\",\n};\nconst SMALL_SUIT: Partial<Record<CardSize, string>> = {\n xs: \"text-lg\",\n sm: \"text-2xl\",\n};\nconst BACK_EMBLEM_SIZE: Record<CardSize, number> = {\n xs: 14,\n sm: 18,\n md: 28,\n lg: 42,\n};\n\n/** Parse a compact card code like \"As\", \"10h\", \"kd\" into rank + suit. */\nexport function parseCard(card: string): { rank: string; suit: string } {\n if (!card || card.length < 2) return { rank: \"\", suit: \"\" };\n return { rank: card.slice(0, -1), suit: card.slice(-1) };\n}\n\nexport function getSuitSymbol(suit: string): string {\n switch (suit) {\n case \"s\": return \"♠\";\n case \"h\": return \"♥\";\n case \"d\": return \"♦\";\n case \"c\": return \"♣\";\n default: return suit;\n }\n}\n\nfunction getSuitClass(suit: string): string {\n switch (suit) {\n case \"s\":\n case \"c\":\n return \"text-neutral-900\";\n case \"h\":\n case \"d\":\n return \"text-red-600\";\n default:\n return \"\";\n }\n}\n\nexport interface PlayingCardProps {\n /** Compact card code (\"As\", \"10h\") or null to render a face-down card back. */\n card: string | null;\n /** xs / sm show a single, larger, centered index; md / lg show corner + center. */\n size?: CardSize;\n className?: string;\n}\n\nconst CARD_FACE = \"relative bg-gradient-to-br from-white via-white to-neutral-100 rounded-md border border-neutral-300/90 overflow-hidden shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]\";\n\n/**\n * A single playing card. Size is driven by the `size` prop (never viewport\n * breakpoints) so cards scale predictably inside any container. Small sizes\n * (xs / sm) show only a larger upright index, centered in the card.\n */\nexport function PlayingCard({ card, size = \"md\", className }: PlayingCardProps) {\n if (!card) return <CardBack size={size} className={className} />;\n\n const { rank, suit } = parseCard(card);\n const suitSymbol = getSuitSymbol(suit);\n const suitClass = getSuitClass(suit);\n const large = size === \"md\" || size === \"lg\";\n\n // Small cards: rank centered in the top half, suit centered in the bottom half.\n if (!large) {\n return (\n <div className={cn(SIZE_CLASSES[size], CARD_FACE, className)} style={{ aspectRatio: \"10/14\" }}>\n <div className={cn(\"absolute inset-0 flex flex-col leading-none\", suitClass)}>\n <div className=\"flex flex-1 items-center justify-center\">\n <span className={cn(SMALL_RANK[size], \"font-bold tracking-tight\")}>{rank}</span>\n </div>\n <div className=\"flex flex-1 items-center justify-center\">\n <span className={SMALL_SUIT[size]}>{suitSymbol}</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div className={cn(SIZE_CLASSES[size], CARD_FACE, className)} style={{ aspectRatio: \"10/14\" }}>\n <div className={cn(\"absolute top-[3px] left-[5px] flex flex-col items-center leading-none\", suitClass)}>\n <span className={cn(CORNER_RANK[size], \"font-bold tracking-tight\")}>{rank}</span>\n <span className={cn(CORNER_SUIT[size], \"-mt-[1px]\")}>{suitSymbol}</span>\n </div>\n <div className={cn(\"absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180\", suitClass)}>\n <span className={cn(CORNER_RANK[size], \"font-bold tracking-tight\")}>{rank}</span>\n <span className={cn(CORNER_SUIT[size], \"-mt-[1px]\")}>{suitSymbol}</span>\n </div>\n <div className={cn(\"absolute inset-0 flex items-center justify-center opacity-90\", suitClass)}>\n <span className={CENTER_SUIT[size]}>{suitSymbol}</span>\n </div>\n </div>\n );\n}\n\nexport interface CardBackProps {\n size?: CardSize;\n className?: string;\n}\n\nfunction CardBackEmblem({ size }: { size: CardSize }) {\n const s = BACK_EMBLEM_SIZE[size];\n return (\n <svg width={s} height={s} viewBox=\"0 0 40 40\" className=\"drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]\" aria-hidden=\"true\">\n <g fill=\"#f0c04a\" opacity=\"0.92\">\n <ellipse cx=\"20\" cy=\"11\" rx=\"4\" ry=\"7\" />\n <ellipse cx=\"20\" cy=\"29\" rx=\"4\" ry=\"7\" />\n <ellipse cx=\"11\" cy=\"20\" rx=\"7\" ry=\"4\" />\n <ellipse cx=\"29\" cy=\"20\" rx=\"7\" ry=\"4\" />\n </g>\n <circle cx=\"20\" cy=\"20\" r=\"3.2\" fill=\"#5a0c18\" />\n </svg>\n );\n}\n\n/** The face-down card back (dark red + gold rosette). */\nexport function CardBack({ size = \"md\", className }: CardBackProps) {\n return (\n <div\n className={cn(\n SIZE_CLASSES[size],\n \"relative rounded-md p-[3px] overflow-hidden\",\n \"bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]\",\n \"border-2 border-[#c9a24b]/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]\",\n className,\n )}\n style={{ aspectRatio: \"10/14\" }}\n >\n <div className=\"h-full w-full rounded-[4px] border border-white/20 flex items-center justify-center bg-[repeating-linear-gradient(45deg,rgba(255,255,255,0.09)_0_6px,transparent_6px_12px),repeating-linear-gradient(-45deg,rgba(255,255,255,0.06)_0_6px,transparent_6px_12px)]\">\n <CardBackEmblem size={size} />\n </div>\n </div>\n );\n}\n\nexport interface HoleCardsProps {\n cards: (string | null)[];\n size?: CardSize;\n className?: string;\n /** Animate the cards dealing in, one after another. Default true. */\n deal?: boolean;\n}\n\n/** A row of a player's hole cards. */\nexport function HoleCards({ cards, size = \"lg\", className, deal = true }: HoleCardsProps) {\n return (\n <div className={cn(\"flex gap-2\", className)}>\n {cards.map((card, i) => (\n <div key={i} className={cn(deal && \"animate-deal-in\")} style={deal ? { animationDelay: `${i * 100}ms` } : undefined}>\n <PlayingCard card={card} size={size} />\n </div>\n ))}\n </div>\n );\n}\n\nexport interface CommunityCardsProps {\n cards: (string | null)[];\n size?: CardSize;\n className?: string;\n /** Animate each card with a 3D flip. Default true. */\n animated?: boolean;\n /** Reserve a fixed number of slots so the row width never changes. */\n slots?: number;\n}\n\n/** A row of community cards, each flipping in with a staggered delay. */\nexport function CommunityCards({ cards, size = \"md\", className, animated = true, slots }: CommunityCardsProps) {\n const count = slots ?? cards.length;\n const list = Array.from({ length: count }, (_, i) => cards[i] ?? null);\n return (\n <div className={cn(\"flex gap-1.5\", animated && \"[perspective:800px]\", className)}>\n {list.map((card, i) => (\n <div key={i} className={SIZE_CLASSES[size]} style={{ aspectRatio: \"10/14\" }}>\n {card ? (\n <div\n className={cn(\"h-full w-full\", animated && \"animate-card-flip\")}\n style={animated ? { animationDelay: `${i * 70}ms` } : undefined}\n >\n <PlayingCard card={card} size={size} />\n </div>\n ) : null}\n </div>\n ))}\n </div>\n );\n}\n","import type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"../cn\";\n\nexport interface SeatProps {\n /** Turn/active highlight (generic). Animated by default. */\n isActive?: boolean;\n /** Winner highlight (generic). Animated by default. */\n isWinner?: boolean;\n /** Dimmed, e.g. folded or out (generic). */\n isDimmed?: boolean;\n /** Enable the turn/winner glow animations. Default true. */\n animated?: boolean;\n /** Render a default 👑 in the top-left corner when `isWinner`. Default false. */\n winnerCrown?: boolean;\n /** Optional click handler (e.g. \"sit here\"). */\n onClick?: () => void;\n\n /** Slot: top row inside the frame — name / badges. */\n header?: ReactNode;\n /** Slot: center content — 0..N cards, avatar, anything. */\n children?: ReactNode;\n /** Slot: bottom row inside the frame — stack / status. */\n footer?: ReactNode;\n /** Slot: floating element at the frame's top-right (legacy alias of topRight). */\n badge?: ReactNode;\n /** Slot: overlay centered over the frame. */\n overlay?: ReactNode;\n\n /** Slot: floating content above the frame, centered (e.g. an action bubble). */\n above?: ReactNode;\n /** Slot: content below the frame, centered (e.g. countdown / bet pill). */\n below?: ReactNode;\n /** Slot: floating content at the frame's top-left corner (e.g. a crown). */\n topLeft?: ReactNode;\n /** Slot: floating content at the frame's top-right corner (e.g. a kick button). */\n topRight?: ReactNode;\n /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */\n topCenter?: ReactNode;\n\n width?: number;\n height?: number;\n className?: string;\n /** Extra classes for the inner frame, to override visuals. */\n frameClassName?: string;\n /** Inline styles for the inner frame. */\n frameStyle?: CSSProperties;\n}\n\n/**\n * A neutral seat: a frame with generic visual states and named slots. Put\n * whatever you want into `header` / `children` / `footer`, and use\n * `above` / `below` / `topLeft` / `topRight` / `topCenter` / `overlay` for\n * floating bits. It knows nothing about any particular game.\n */\nexport function Seat({\n isActive = false,\n isWinner = false,\n isDimmed = false,\n animated = true,\n winnerCrown = false,\n onClick,\n header,\n children,\n footer,\n badge,\n overlay,\n above,\n below,\n topLeft,\n topRight,\n topCenter,\n width = 160,\n height = 176,\n className,\n frameClassName,\n frameStyle,\n}: SeatProps) {\n const activeClass = animated\n ? \"border-amber-400 animate-turn-glow\"\n : \"border-amber-400 shadow-[0_0_18px_rgba(243,156,18,0.5)]\";\n const winnerClass = animated\n ? \"border-amber-400 animate-winner-glow\"\n : \"border-amber-400 shadow-[0_0_0_3px_rgba(243,156,18,0.9)]\";\n\n const effTopLeft =\n topLeft !== undefined\n ? topLeft\n : isWinner && winnerCrown\n ? <span className=\"text-3xl drop-shadow-[0_2px_4px_rgba(0,0,0,0.6)] animate-bounce\">👑</span>\n : undefined;\n\n return (\n <div className={cn(\"relative flex flex-col items-center\", className)} style={{ width }}>\n {topCenter !== undefined && (\n <div className=\"absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1\">{topCenter}</div>\n )}\n {effTopLeft !== undefined && <div className=\"absolute -top-3 -left-2 z-40\">{effTopLeft}</div>}\n {topRight !== undefined && <div className=\"absolute -top-2 -right-2 z-40\">{topRight}</div>}\n {above !== undefined && (\n <div className=\"pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2\">{above}</div>\n )}\n\n <div\n role={onClick ? \"button\" : undefined}\n tabIndex={onClick ? 0 : undefined}\n onClick={onClick}\n onKeyDown={\n onClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onClick();\n }\n : undefined\n }\n style={{ width, height, ...frameStyle }}\n className={cn(\n \"relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition\",\n \"border-neutral-700\",\n isActive && activeClass,\n isWinner && winnerClass,\n isDimmed && \"opacity-50\",\n onClick && \"cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-amber-400\",\n frameClassName,\n )}\n >\n {header !== undefined && (\n <div className=\"flex w-full shrink-0 items-center justify-between gap-1 text-[11px]\">{header}</div>\n )}\n <div className=\"flex min-h-0 flex-1 items-center justify-center\">{children}</div>\n {footer !== undefined && (\n <div className=\"flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300\">{footer}</div>\n )}\n\n {overlay !== undefined && (\n <div className=\"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 text-center\">{overlay}</div>\n )}\n </div>\n\n {badge !== undefined && <div className=\"absolute top-0 right-0 z-40\">{badge}</div>}\n {below !== undefined && <div className=\"mt-1 flex flex-col items-center gap-1\">{below}</div>}\n </div>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../cn\";\nimport { formatBB } from \"../format\";\n\nexport interface PotProps {\n amount: number;\n /** Optional label, e.g. \"Main\" / \"Side\". */\n label?: string;\n /** Formatter for the amount; defaults to Big-Blind formatting. */\n format?: (amount: number) => string;\n /** Animate the amount counting up when it changes. Default true. */\n animate?: boolean;\n className?: string;\n}\n\n/** Smoothly count a number toward its target (presentational only). */\nfunction CountUp({ value, format }: { value: number; format: (n: number) => string }) {\n const [shown, setShown] = useState(value);\n const fromRef = useRef(value);\n\n useEffect(() => {\n const from = fromRef.current;\n const to = value;\n if (from === to) return;\n let raf = 0;\n const start = performance.now();\n const dur = 500;\n const tick = (now: number) => {\n const t = Math.min(1, (now - start) / dur);\n const eased = 1 - Math.pow(1 - t, 3);\n setShown(Math.round(from + (to - from) * eased));\n if (t < 1) raf = requestAnimationFrame(tick);\n else fromRef.current = to;\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [value]);\n\n return <>{format(shown)}</>;\n}\n\n/** A small, standalone pot pill. Optional — the table does not require it. */\nexport function Pot({ amount, label, format = formatBB, animate = true, className }: PotProps) {\n return (\n <div className={cn(\"flex items-center gap-2 font-mono text-sm text-amber-400\", className)}>\n {label && <span className=\"text-neutral-400\">{label}</span>}\n <span className=\"whitespace-nowrap rounded-full border border-amber-400/50 bg-neutral-900/80 px-3 py-1 shadow-inner\">\n {animate ? <CountUp value={amount} format={format} /> : format(amount)}\n </span>\n </div>\n );\n}\n","import { useRef, useLayoutEffect, useState, useMemo, type ReactNode } from \"react\";\nimport { cn } from \"../cn\";\n\nexport interface CardTableProps {\n /** Number of seats (2–10). Default 6. */\n seatCount?: number;\n /** Optional per-seat content. Length should equal `seatCount`. */\n seats?: (ReactNode | null)[];\n /** Alternative to `seats`: render a seat by index. */\n renderSeat?: (index: number, isYou: boolean) => ReactNode;\n /** Which seat belongs to the viewer; rotates that seat to the bottom. */\n yourSeatIndex?: number | null;\n /** Optional content in the middle of the table (cards, pot, anything). */\n center?: ReactNode;\n /** Ambient spotlight + felt shimmer. Default true. */\n ambient?: boolean;\n /** Highlight the table edge (e.g. when it is your turn). Default false. */\n yourTurn?: boolean;\n /** Winning seat indices; when non-empty, confetti falls. */\n winnerSeats?: number[];\n /** Show confetti for winners. Default true. */\n confetti?: boolean;\n /** Seat frame size used for layout. */\n seatWidth?: number;\n seatHeight?: number;\n className?: string;\n}\n\nconst DESIGN_W = 1200;\nconst DESIGN_H = 900;\nconst MARGIN_X = 164;\nconst MARGIN_Y = 196;\nconst TABLE_RX = DESIGN_W / 2 - MARGIN_X;\nconst TABLE_RY = DESIGN_H / 2 - MARGIN_Y;\nconst FELT_GAP = 11;\nconst BET_SCALE = 0.72;\nconst BET_RX = TABLE_RX * BET_SCALE;\nconst BET_RY = TABLE_RY * BET_SCALE;\nconst CONFETTI_COLORS = [\"#f39c12\", \"#e74c3c\", \"#2ecc71\", \"#3498db\", \"#f1c40f\", \"#9b59b6\"];\n\ninterface SeatLayout {\n x: number;\n y: number;\n index: number;\n isYou: boolean;\n}\n\nfunction getEllipsePoint(cx: number, cy: number, rx: number, ry: number, angleDeg: number) {\n const rad = (angleDeg * Math.PI) / 180;\n return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };\n}\n\nfunction calculateSeatLayouts(\n seatCount: number,\n yourSeatIndex: number | null,\n seatWidth: number,\n seatHeight: number,\n): SeatLayout[] {\n const cx = DESIGN_W / 2;\n const cy = DESIGN_H / 2;\n const ringX = TABLE_RX - FELT_GAP + seatWidth / 2 + 6;\n const ringY = TABLE_RY - FELT_GAP + seatHeight / 2 + 6;\n\n const step = 360 / seatCount;\n const angleOf = (i: number) => -90 + i * step;\n const rotationOffset = yourSeatIndex !== null ? 90 - angleOf(yourSeatIndex) : 0;\n\n const layouts: SeatLayout[] = [];\n for (let i = 0; i < seatCount; i++) {\n const pt = getEllipsePoint(cx, cy, ringX, ringY, angleOf(i) + rotationOffset);\n layouts.push({\n x: pt.x - seatWidth / 2,\n y: pt.y - seatHeight / 2,\n index: i,\n isYou: i === yourSeatIndex,\n });\n }\n return layouts;\n}\n\n/**\n * A responsive, game-agnostic card table. It scales a fixed 1200x900 design\n * onto its container and arranges `seatCount` seats around the felt. Purely\n * presentational: supply seat content and an optional `center` slot.\n */\nexport function CardTable({\n seatCount = 6,\n seats,\n renderSeat,\n yourSeatIndex = null,\n center,\n ambient = true,\n yourTurn = false,\n winnerSeats = [],\n confetti = true,\n seatWidth = 160,\n seatHeight = 176,\n className,\n}: CardTableProps) {\n const count = Math.max(2, Math.min(10, Math.floor(seatCount)));\n const outerRef = useRef<HTMLDivElement>(null);\n const [scale, setScale] = useState(1);\n\n const layouts = useMemo(\n () => calculateSeatLayouts(count, yourSeatIndex, seatWidth, seatHeight),\n [count, yourSeatIndex, seatWidth, seatHeight],\n );\n\n useLayoutEffect(() => {\n const el = outerRef.current;\n if (!el) return;\n const compute = () => {\n const w = el.clientWidth;\n const h = el.clientHeight;\n if (!w || !h) return;\n setScale(Math.min(w / DESIGN_W, h / DESIGN_H));\n };\n compute();\n const ro = new ResizeObserver(compute);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };\n const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };\n const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };\n const ringInset = { inset: `${MARGIN_Y + 2}px ${MARGIN_X + 2}px` };\n const betLineInset = {\n inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`,\n };\n\n return (\n <div\n ref={outerRef}\n className={cn(\"relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden\", className)}\n >\n <div style={{ width: DESIGN_W * scale, height: DESIGN_H * scale }}>\n <div\n className=\"relative\"\n style={{ width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: \"top left\" }}\n >\n <div\n className=\"absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl\"\n aria-hidden=\"true\"\n />\n\n {/* wooden rail */}\n <div\n 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)]\"\n style={tableFrameInset}\n aria-hidden=\"true\"\n />\n\n {/* felt */}\n <div\n 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)]\"\n style={feltInset}\n aria-hidden=\"true\"\n />\n\n {/* ambient: breathing spotlight + felt shimmer */}\n {ambient && (\n <>\n <div\n className=\"absolute animate-spotlight rounded-[50%] bg-[radial-gradient(closest-side,rgba(255,214,120,0.14),transparent_70%)] blur-2xl\"\n style={feltInset}\n aria-hidden=\"true\"\n />\n <div className=\"absolute overflow-hidden rounded-[50%]\" style={feltInset} aria-hidden=\"true\">\n <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%]\" />\n </div>\n </>\n )}\n\n {/* betting line */}\n <div\n className=\"absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]\"\n style={betLineInset}\n aria-hidden=\"true\"\n />\n\n {/* center suit watermark */}\n <div className=\"absolute flex items-center justify-center pointer-events-none\" style={feltInset} aria-hidden=\"true\">\n <span className=\"text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none\">\n ♠♥♦♣\n </span>\n </div>\n\n {/* your-turn edge glow */}\n {yourTurn && (\n <div\n className=\"pointer-events-none absolute z-10 animate-turn-glow rounded-[50%]\"\n style={ringInset}\n aria-hidden=\"true\"\n />\n )}\n\n {/* center slot (optional) */}\n {center !== undefined && (\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\">\n {center}\n </div>\n )}\n\n {/* seats */}\n <div className=\"absolute inset-0 z-20\">\n {layouts.map(({ x, y, index, isYou }) => {\n const content = seats ? (seats[index] ?? null) : renderSeat ? renderSeat(index, isYou) : null;\n if (content === null) return null;\n return (\n <div key={index} className=\"absolute\" style={{ left: `${x}px`, top: `${y}px` }}>\n {content}\n </div>\n );\n })}\n </div>\n\n {/* winner confetti */}\n {confetti && winnerSeats.length > 0 && (\n <div className=\"pointer-events-none absolute inset-0 z-30 overflow-hidden\" aria-hidden=\"true\">\n {Array.from({ length: 44 }).map((_, i) => {\n const w = 5 + (i % 4) * 2;\n const h = 9 + (i % 3) * 4;\n const dur = 1.7 + (i % 5) * 0.28;\n const delay = (i % 12) * 0.14;\n return (\n <span\n key={i}\n className=\"absolute top-[26%] animate-confetti rounded-[2px]\"\n style={{\n left: `${(i * 37) % 96}%`,\n width: w,\n height: h,\n backgroundColor: CONFETTI_COLORS[i % CONFETTI_COLORS.length],\n animationDuration: `${dur}s`,\n animationDelay: `${delay}s`,\n animationIterationCount: \"infinite\",\n }}\n />\n );\n })}\n </div>\n )}\n\n {/* outer rim highlight */}\n <div className=\"absolute rounded-[50%] border border-white/10 pointer-events-none\" style={rimInset} aria-hidden=\"true\" />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","useRef","useState","Fragment","jsx","jsxs","useRef","useState"]}
|
|
1
|
+
{"version":3,"sources":["../src/cn.ts","../src/format.ts","../src/color.ts","../src/sizes.ts","../src/motion.ts","../src/cards.ts","../src/seat.ts","../src/geometry.ts","../src/layout.ts","../src/components/PlayingCard.tsx","../src/chips.ts","../src/components/Chip.tsx","../src/components/seatSize.ts","../src/components/Seat.tsx","../src/components/Pot.tsx","../src/components/CardTable.tsx"],"sourcesContent":["import { twMerge } from \"tailwind-merge\";\n\n/** Join class names and resolve conflicting Tailwind utilities (last wins). */\nexport function cn(...parts: Array<string | false | null | undefined>): string {\n return twMerge(parts.filter(Boolean).join(\" \"));\n}\n","/**\n * Default big blind that `formatBB` uses when you do not pass one.\n *\n * Kept for back-compat. Nothing in the package applies it on your behalf: the\n * components render plain numbers unless you hand them a formatter, because a\n * big blind is a poker rule and this is not a poker library.\n */\nexport const BB = 20;\n\n/**\n * Format an amount as big blinds, e.g. 1234 -> \"61.7 BB\".\n *\n * This is a poker *helper*, not a default. Pass your own big blind — a\n * fixed 20 is wrong for most tables:\n *\n * ```ts\n * <Pot amount={1234} format={(n) => formatBB(n, 100)} />\n * ```\n */\nexport function formatBB(amount: number, bb: number = BB): string {\n const value = amount / bb;\n return `${Number.isInteger(value) ? value : value.toFixed(1)} BB`;\n}\n","/**\n * Colour maths. Nothing here knows about cards, chips or games — this is the\n * generic layer that contrast decisions are built on.\n *\n * It exists because the package needs to answer \"would this be visible on top of\n * that?\" in more than one place (edge spots on a chip, and anything else that\n * grows a light/dark ink choice later), and that question should not live inside\n * a component or a game-specific module.\n */\n\nexport interface Rgba {\n r: number;\n g: number;\n b: number;\n /** 0–1. */\n a: number;\n}\n\n/**\n * Parse `#rgb`, `#rrggbb`, `rgb()` or `rgba()` into channels.\n *\n * Returns `null` for anything else — a `var(...)`, a named colour, a value with\n * a color-mix() in it. Callers are expected to have a sensible fallback rather\n * than guess, which is why this does not throw.\n */\nexport function parseColor(color: string): Rgba | null {\n const input = color.trim();\n const hex = input.match(/^#([0-9a-f]{3,8})$/i);\n if (hex) {\n const raw = hex[1];\n if (raw.length === 3 || raw.length === 4) {\n const [r, g, b, a] = raw.split(\"\").map((c) => parseInt(c + c, 16));\n return { r, g, b, a: raw.length === 4 ? a / 255 : 1 };\n }\n if (raw.length === 6 || raw.length === 8) {\n return {\n r: parseInt(raw.slice(0, 2), 16),\n g: parseInt(raw.slice(2, 4), 16),\n b: parseInt(raw.slice(4, 6), 16),\n a: raw.length === 8 ? parseInt(raw.slice(6, 8), 16) / 255 : 1,\n };\n }\n return null;\n }\n\n const fn = input.match(/^rgba?\\(([^)]+)\\)$/i);\n if (!fn) return null;\n const parts = fn[1].split(/[,\\s/]+/).filter(Boolean).map(Number);\n if (parts.length < 3 || parts.slice(0, 3).some((n) => !Number.isFinite(n))) return null;\n return { r: parts[0], g: parts[1], b: parts[2], a: Number.isFinite(parts[3]) ? parts[3] : 1 };\n}\n\n/** WCAG relative luminance (0 = black, 1 = white). `null` if unparseable. */\nexport function relativeLuminance(color: string): number | null {\n const parsed = parseColor(color);\n if (!parsed) return null;\n const channel = (c: number) => {\n const s = Math.min(255, Math.max(0, c)) / 255;\n return s <= 0.04045 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;\n };\n return 0.2126 * channel(parsed.r) + 0.7152 * channel(parsed.g) + 0.0722 * channel(parsed.b);\n}\n\n/** Composite a possibly translucent colour over an opaque one. */\nexport function flatten(foreground: string, background: string): Rgba | null {\n const fg = parseColor(foreground);\n const bg = parseColor(background);\n if (!fg || !bg) return null;\n const mix = (f: number, b: number) => fg.a * f + (1 - fg.a) * b;\n return { r: mix(fg.r, bg.r), g: mix(fg.g, bg.g), b: mix(fg.b, bg.b), a: 1 };\n}\n\n/**\n * WCAG contrast ratio between two colours, 1–21. `foreground` may be\n * translucent, in which case it is composited over `background` first — which is\n * the question you actually have when you are choosing an ink colour.\n *\n * `null` if either colour is unparseable.\n */\nexport function contrastRatio(foreground: string, background: string): number | null {\n const composited = flatten(foreground, background);\n if (!composited) return null;\n const a = relativeLuminance(`rgb(${composited.r}, ${composited.g}, ${composited.b})`);\n const b = relativeLuminance(background);\n if (a === null || b === null) return null;\n const [hi, lo] = a > b ? [a, b] : [b, a];\n return (hi + 0.05) / (lo + 0.05);\n}\n\n/**\n * Pick whichever candidate is more visible on `background`.\n *\n * Derived from measured contrast rather than a hand-tuned luminance threshold,\n * so a mid-tone background gets the genuinely better of the two instead of\n * whichever side of an arbitrary cut-off it fell on. Falls back to the first\n * candidate when the background cannot be measured.\n */\nexport function mostVisible(candidates: readonly string[], background: string): string {\n let best = candidates[0];\n let bestRatio = -1;\n for (const candidate of candidates) {\n const ratio = contrastRatio(candidate, background);\n if (ratio !== null && ratio > bestRatio) {\n best = candidate;\n bestRatio = ratio;\n }\n }\n return best;\n}\n","/**\n * The size vocabulary, in one place.\n *\n * Cards and chips keep their own pixel tables — a `sm` card and a `sm` chip are\n * different objects and pretending otherwise would be worse — but they share one\n * list of names. That way adding an `xl` to the vocabulary is a single edit and\n * a component cannot silently support a size the type does not mention.\n */\n\nexport const SIZES = [\"xs\", \"sm\", \"md\", \"lg\"] as const;\n\n/** Any size in the vocabulary. */\nexport type Size = (typeof SIZES)[number];\n\n/** `PlayingCard` / `HoleCards` / `CommunityCards`: the full range. */\nexport type CardSize = Size;\n\n/** `Chip` / `ChipStack`: a chip has no `xs`. */\nexport type ChipSize = Exclude<Size, \"xs\">;\n","/**\n * Animation timing, in one place.\n *\n * These were magic numbers inside three components. They are kept short on\n * purpose: the on-by-default animations are game feel, and a consumer who wants\n * a different tempo turns them off rather than tuning them.\n */\n\n/** How long the pot takes to count from one value to the next, in ms. */\nexport const COUNT_UP_MS = 500;\n\n/** Stagger between hole cards dealing in, in ms. */\nexport const DEAL_STAGGER_MS = 100;\n\n/** Stagger between community cards flipping in, in ms. */\nexport const FLIP_STAGGER_MS = 70;\n","/**\n * Card codes and how to say them out loud. Pure functions, no React.\n *\n * This lives outside the component because two things need it: the artwork\n * (`PlayingCard` / `CardBack`, for the accessible name) and any consumer\n * building a hand list, a log line or a replay. A single vocabulary is what\n * stops every call site inventing its own way of saying \"As\".\n */\n\nexport interface ParsedCard {\n rank: string;\n suit: string;\n}\n\nconst RANK_NAMES: Record<string, string> = {\n A: \"Ace\",\n K: \"King\",\n Q: \"Queen\",\n J: \"Jack\",\n \"10\": \"Ten\",\n \"9\": \"Nine\",\n \"8\": \"Eight\",\n \"7\": \"Seven\",\n \"6\": \"Six\",\n \"5\": \"Five\",\n \"4\": \"Four\",\n \"3\": \"Three\",\n \"2\": \"Two\",\n};\n\nconst SUIT_NAMES: Record<string, string> = {\n s: \"spades\",\n h: \"hearts\",\n d: \"diamonds\",\n c: \"clubs\",\n};\n\nconst SUIT_SYMBOLS: Record<string, string> = {\n s: \"\\u2660\",\n h: \"\\u2665\",\n d: \"\\u2666\",\n c: \"\\u2663\",\n};\n\n/**\n * Split a compact card code like `\"As\"` or `\"10h\"` into rank and suit.\n *\n * Deliberately permissive: it takes the last character as the suit and trusts\n * the rest, so `\"xyz\"` parses as rank `\"xy\"` / suit `\"z\"`. Validation is a known\n * gap — use `describeCard`, which falls back to the raw code rather than\n * pretending to know what it read.\n */\nexport function parseCard(card: string): ParsedCard {\n if (!card || card.length < 2) return { rank: \"\", suit: \"\" };\n return { rank: card.slice(0, -1), suit: card.slice(-1).toLowerCase() };\n}\n\n/** The glyph for a suit letter. Unknown suits are passed through unchanged. */\nexport function getSuitSymbol(suit: string): string {\n return SUIT_SYMBOLS[suit.toLowerCase()] ?? suit;\n}\n\n/** `\"A\"` → `\"Ace\"`, `\"10\"` → `\"Ten\"`. Unknown ranks pass through. */\nexport function getRankName(rank: string): string {\n return RANK_NAMES[rank.toUpperCase()] ?? rank;\n}\n\n/** `\"s\"` → `\"spades\"`. Unknown suits pass through. */\nexport function getSuitName(suit: string): string {\n return SUIT_NAMES[suit.toLowerCase()] ?? suit;\n}\n\n/** Hearts and diamonds are the red ones. */\nexport function isRedSuit(suit: string): boolean {\n const s = suit.toLowerCase();\n return s === \"h\" || s === \"d\";\n}\n\n/** Whether a suit letter is one this package knows how to draw. */\nexport function isKnownSuit(suit: string): boolean {\n return suit.toLowerCase() in SUIT_SYMBOLS;\n}\n\n/**\n * A spoken description: `\"As\"` → `\"Ace of spades\"`, `null` → `\"face-down card\"`.\n *\n * Returns the raw code when the rank or suit is not recognised, rather than\n * announcing something the card is not.\n */\nexport function describeCard(card: string | null): string {\n if (card === null || card === \"\") return \"face-down card\";\n const { rank, suit } = parseCard(card);\n const rankName = RANK_NAMES[rank.toUpperCase()];\n const suitName = SUIT_NAMES[suit];\n if (!rankName || !suitName) return card;\n return `${rankName} of ${suitName}`;\n}\n","/**\n * Seat-ring helpers. Pure functions — they contain no game-specific rules.\n */\n\nexport interface NextSeatOptions {\n /** +1 for clockwise, -1 for counter-clockwise. Default +1. */\n dir?: 1 | -1;\n /** Return true to skip a seat (e.g. folded / empty / out). */\n skip?: (index: number) => boolean;\n}\n\n/**\n * Return the index of the next seat on the ring after `current`, skipping any\n * seat matched by `opts.skip`. Wraps around. Returns `current` if every other\n * seat is skipped.\n */\nexport function nextSeatIndex(current: number, seatCount: number, opts: NextSeatOptions = {}): number {\n // `seatCount` comes from game state, so guard against NaN / Infinity / <= 0\n // rather than propagating NaN out of the modulo.\n const floored = Math.floor(seatCount);\n const n = Number.isFinite(floored) && floored > 0 ? floored : 1;\n const dir = opts.dir ?? 1;\n const skip = opts.skip;\n const start = (((Math.floor(current) || 0) % n) + n) % n;\n\n let i = start;\n for (let step = 0; step < n - 1; step++) {\n i = (i + dir + n) % n;\n if (!skip || !skip(i)) return i;\n }\n return start;\n}\n","/**\n * Pure ellipse geometry for the card table. No game rules, no React.\n *\n * Everything is derived from one parametric ellipse:\n *\n * x(θ) = cx + rx·cos θ\n * y(θ) = cy + ry·sin θ\n *\n * with θ in degrees. Screen coordinates (y grows downward), so θ = 90° is the\n * point directly BELOW the centre.\n *\n * Seat 0 sits at `startAngle` (default 90° = directly below the centre) and\n * every following seat advances by `360 / seatCount`, so any count from 2 to 10\n * spreads evenly. The betting line uses the same formula with a smaller radius.\n */\n\nexport interface Point {\n x: number;\n y: number;\n}\n\n/** Normalise a seat count to a positive integer. */\nfunction normCount(count: number): number {\n const n = Math.floor(count);\n return Number.isFinite(n) && n > 0 ? n : 1;\n}\n\n/**\n * Ring angle (degrees) of seat `index`, evenly divided around 360°.\n * Seat 0 is at `startAngle` (default 90° = directly below the table centre).\n */\nexport function seatAngle(index: number, seatCount: number, startAngle = 90): number {\n const n = normCount(seatCount);\n return startAngle + (index * 360) / n;\n}\n\n/** Point on the ellipse at `angleDeg` (degrees; 90° = below the centre). */\nexport function ellipsePoint(cx: number, cy: number, rx: number, ry: number, angleDeg: number): Point {\n const rad = (angleDeg * Math.PI) / 180;\n return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };\n}\n\n/**\n * `count` evenly spaced points on an ellipse, starting at `startAngle`\n * (default 90° = directly below the centre). Handy for seat rings and betting\n * lines alike: pass the ring radii, get the coordinates.\n */\nexport function ringPoints(\n count: number,\n cx: number,\n cy: number,\n rx: number,\n ry: number,\n startAngle = 90,\n): Point[] {\n const n = normCount(count);\n return Array.from({ length: n }, (_, i) => ellipsePoint(cx, cy, rx, ry, seatAngle(i, n, startAngle)));\n}\n","/**\n * Pure layout math for the card table. No React, no game rules.\n *\n * Everything is derived from one parametric ellipse (see `geometry.ts`). The\n * table, the felt, the rim, the seat ring and the betting line are ellipses\n * sharing a centre and an aspect ratio, and they are all computed **here**.\n *\n * That matters: the betting line used to be computed inside `CardTable` from a\n * module constant while the chips were positioned from the `betRadiusScale`\n * prop, and the two drifted apart. One ellipse, one place that computes it.\n */\n\nimport { ellipsePoint, seatAngle } from \"./geometry\";\n\n/** Default design space. */\nexport const DESIGN_W = 1200;\nexport const DESIGN_H = 900;\n/** Rail inset as a fraction of the design box (164/1200, 196/900). */\nexport const DEFAULT_MARGIN_X = 164 / DESIGN_W;\nexport const DEFAULT_MARGIN_Y = 196 / DESIGN_H;\n/** Gap between the rail and the felt, in design-space px. */\nexport const DEFAULT_FELT_GAP = 11;\n/** Default radius of the bet ring, as a fraction of the table radius. */\nexport const DEFAULT_BET_SCALE = 0.72;\n/** Default minimum clearance between neighbouring seat frames. */\nexport const DEFAULT_SEAT_GAP = 24;\n/** Default preferred seat frame size. */\nexport const DEFAULT_SEAT_W = 160;\nexport const DEFAULT_SEAT_H = 176;\n/** How far inside the rail the rim highlight sits, in design-space px. */\nexport const RIM_GAP = 6;\n/** How far inside the rail the your-turn glow sits, in design-space px. */\nexport const GLOW_GAP = 2;\n\nexport interface LayoutOptions {\n /** Number of seats. */\n count: number;\n /** Design-space width / height. */\n designWidth: number;\n designHeight: number;\n /** Rail inset as a fraction of designWidth / designHeight (0..0.5). */\n marginX: number;\n marginY: number;\n /** Felt inset from the rail, in design-space px. */\n feltGap: number;\n /** Preferred (maximum) seat frame size. */\n seatWidth: number;\n seatHeight: number;\n /**\n * Minimum clearance in design-space px to keep between neighbouring seat\n * frames. Frames are shrunk uniformly (never below `SEAT_SHRINK_FLOOR`) to\n * honour it. `0` disables the shrink entirely.\n */\n seatGap: number;\n /** Angle of seat 0, in degrees. */\n startAngle: number;\n /** Where the betting line sits, as a fraction of the table radii. Default 0.72. */\n betRadiusScale?: number;\n}\n\nexport interface SeatLayout {\n index: number;\n /** Centre of the seat frame, in design space. */\n x: number;\n y: number;\n /** Frame size the layout was computed for (≤ the preferred size when shrunk). */\n width: number;\n height: number;\n}\n\n/**\n * How far an ellipse sits inside the design box, in design-space px. Spread it\n * onto an absolutely-positioned element and the ellipse lands where the rest of\n * the layout expects.\n */\nexport interface EllipseBox {\n top: number;\n right: number;\n bottom: number;\n left: number;\n}\n\nexport interface Layout {\n /** Seat centre used for every ring, in design space. */\n cx: number;\n cy: number;\n /** Radii of the outer table ellipse. */\n tableRx: number;\n tableRy: number;\n /** Radii of the seat ring. */\n ringRx: number;\n ringRy: number;\n /** Radii of the betting line. */\n betRx: number;\n betRy: number;\n /** Seat frame size actually used. */\n seatW: number;\n seatH: number;\n seats: SeatLayout[];\n /** Every ellipse the table draws, ready to position. */\n boxes: {\n table: EllipseBox;\n felt: EllipseBox;\n rim: EllipseBox;\n glow: EllipseBox;\n bettingLine: EllipseBox;\n };\n}\n\n/** Smallest fraction of the preferred seat size the auto-shrink will go to. */\nexport const SEAT_SHRINK_FLOOR = 0.5;\n\n/** Radii of the table ellipse for a given design box and rail inset. */\nexport function tableRadii(\n designWidth: number,\n designHeight: number,\n marginX: number,\n marginY: number,\n): { rx: number; ry: number } {\n return {\n rx: Math.max(0, designWidth / 2 - marginX * designWidth),\n ry: Math.max(0, designHeight / 2 - marginY * designHeight),\n };\n}\n\n/**\n * Radii of the seat ring. Seats ride the ring at `+ seatSize / 2 + 6`, so the\n * frames straddle the rail, but never past the design box (seats at the extreme\n * left / right / top / bottom used to be clipped in narrow layouts).\n */\nexport function seatRingRadii(\n designWidth: number,\n designHeight: number,\n tableRx: number,\n tableRy: number,\n seatW: number,\n seatH: number,\n feltGap: number,\n): { rx: number; ry: number } {\n const rx = Math.min(tableRx - feltGap + seatW / 2 + 6, designWidth / 2 - seatW / 2);\n const ry = Math.min(tableRy - feltGap + seatH / 2 + 6, designHeight / 2 - seatH / 2);\n return { rx: Math.max(0, rx), ry: Math.max(0, ry) };\n}\n\n/** Centre point of seat `index` on the seat ring, in design space. */\nexport function seatPoint(\n index: number,\n count: number,\n cx: number,\n cy: number,\n ringRx: number,\n ringRy: number,\n startAngle = 90,\n): { x: number; y: number } {\n return ellipsePoint(cx, cy, ringRx, ringRy, seatAngle(index, count, startAngle));\n}\n\n/** Turn radii into the inset box of an absolutely-positioned ellipse. */\nexport function ellipseBox(designWidth: number, designHeight: number, rx: number, ry: number): EllipseBox {\n const top = designHeight / 2 - ry;\n const left = designWidth / 2 - rx;\n return { top, right: left, bottom: top, left };\n}\n\n/** Axis-aligned separation of two seat frames: > 0 is a gap, < 0 a penetration. */\nfunction boxSeparation(a: SeatLayout, b: SeatLayout): number {\n const dx = Math.max(b.x - a.x - (a.width + b.width) / 2, a.x - b.x - (a.width + b.width) / 2);\n const dy = Math.max(b.y - a.y - (a.height + b.height) / 2, a.y - b.y - (a.height + b.height) / 2);\n if (dx > 0 && dy > 0) return Math.hypot(dx, dy);\n if (dx > 0) return dx;\n if (dy > 0) return dy;\n return Math.max(dx, dy);\n}\n\n/**\n * Smallest gap between any two seat frames, over every pair. Positive means no\n * frames overlap. Returns `Infinity` for fewer than two seats.\n */\nexport function minSeatSeparation(seats: SeatLayout[]): number {\n let worst = Infinity;\n for (let i = 0; i < seats.length; i++) {\n for (let j = i + 1; j < seats.length; j++) {\n const s = boxSeparation(seats[i], seats[j]);\n if (s < worst) worst = s;\n }\n }\n return worst;\n}\n\nfunction positionsFor(\n count: number,\n cx: number,\n cy: number,\n ringRx: number,\n ringRy: number,\n seatW: number,\n seatH: number,\n startAngle: number,\n): SeatLayout[] {\n return Array.from({ length: count }, (_, index) => {\n const p = seatPoint(index, count, cx, cy, ringRx, ringRy, startAngle);\n return { index, x: p.x, y: p.y, width: seatW, height: seatH };\n });\n}\n\n/**\n * Pick the largest uniform scale (1 → `SEAT_SHRINK_FLOOR`, in 1% steps) at which\n * every pair of seat frames clears `seatGap`. Returns the scale to apply.\n *\n * A 10-seat table at the default 160x176 frames only leaves ~26px between\n * neighbours, which reads as crowded; shrinking them a little spreads the ring\n * out. It is also what makes 11–16 seats possible at all. Wide tables (2–8\n * seats) are unaffected — they pass on the first step.\n */\nexport function fitSeatScale(opts: LayoutOptions): number {\n if (opts.seatGap <= 0) return 1;\n const { rx, ry } = tableRadii(opts.designWidth, opts.designHeight, opts.marginX, opts.marginY);\n const cx = opts.designWidth / 2;\n const cy = opts.designHeight / 2;\n\n for (let step = 0; step <= 50; step++) {\n const scale = 1 - step * 0.01;\n const w = Math.max(1, Math.round(opts.seatWidth * scale));\n const h = Math.max(1, Math.round(opts.seatHeight * scale));\n const ring = seatRingRadii(opts.designWidth, opts.designHeight, rx, ry, w, h, opts.feltGap);\n const seats = positionsFor(opts.count, cx, cy, ring.rx, ring.ry, w, h, opts.startAngle);\n if (minSeatSeparation(seats) >= opts.seatGap) return scale;\n if (scale <= SEAT_SHRINK_FLOOR) break;\n }\n return SEAT_SHRINK_FLOOR;\n}\n\n/**\n * Everything the table draws for one configuration: the table ellipse, the felt,\n * the rim, the glow ring, the betting line, the seat ring and every seat centre.\n */\nexport function layoutSeats(opts: LayoutOptions): Layout {\n const { rx, ry } = tableRadii(opts.designWidth, opts.designHeight, opts.marginX, opts.marginY);\n const cx = opts.designWidth / 2;\n const cy = opts.designHeight / 2;\n\n const scale = fitSeatScale(opts);\n const seatW = Math.max(1, Math.round(opts.seatWidth * scale));\n const seatH = Math.max(1, Math.round(opts.seatHeight * scale));\n\n const ring = seatRingRadii(opts.designWidth, opts.designHeight, rx, ry, seatW, seatH, opts.feltGap);\n const betScale = Math.min(1, Math.max(0.2, opts.betRadiusScale ?? DEFAULT_BET_SCALE));\n const betRx = rx * betScale;\n const betRy = ry * betScale;\n\n const box = (bx: number, by: number) => ellipseBox(opts.designWidth, opts.designHeight, bx, by);\n\n return {\n cx,\n cy,\n tableRx: rx,\n tableRy: ry,\n ringRx: ring.rx,\n ringRy: ring.ry,\n betRx,\n betRy,\n seatW,\n seatH,\n seats: positionsFor(opts.count, cx, cy, ring.rx, ring.ry, seatW, seatH, opts.startAngle),\n boxes: {\n table: box(rx, ry),\n felt: box(rx - opts.feltGap, ry - opts.feltGap),\n rim: box(rx - RIM_GAP, ry - RIM_GAP),\n glow: box(rx - GLOW_GAP, ry - GLOW_GAP),\n bettingLine: box(betRx, betRy),\n },\n };\n}\n","import { cn } from \"../cn\";\nimport { describeCard, getSuitSymbol, isKnownSuit, isRedSuit, parseCard } from \"../cards\";\nimport { DEAL_STAGGER_MS, FLIP_STAGGER_MS } from \"../motion\";\nimport { type CardSize } from \"../sizes\";\n\n/*\n * Card helpers are re-exported from here so imports from this module keep\n * working; they live in `cards.ts` now, next to the naming they belong with.\n */\nexport { describeCard, getRankName, getSuitName, getSuitSymbol, isKnownSuit, isRedSuit, parseCard } from \"../cards\";\nexport type { ParsedCard } from \"../cards\";\nexport type { CardSize } from \"../sizes\";\n\nconst SIZE_CLASSES: Record<CardSize, string> = {\n xs: \"w-8 h-11\",\n sm: \"w-10 h-14\",\n md: \"w-16 h-22\",\n lg: \"w-24 h-32\",\n};\n\nconst CORNER_RANK: Record<CardSize, string> = {\n xs: \"text-[11px]\",\n sm: \"text-sm\",\n md: \"text-lg\",\n lg: \"text-2xl\",\n};\nconst CORNER_SUIT: Record<CardSize, string> = {\n xs: \"text-[9px]\",\n sm: \"text-xs\",\n md: \"text-sm\",\n lg: \"text-base\",\n};\nconst CENTER_SUIT: Record<CardSize, string> = {\n xs: \"text-base\",\n sm: \"text-xl\",\n md: \"text-3xl\",\n lg: \"text-5xl\",\n};\n// Small cards (xs/sm) show a single, larger index centered in the card.\nconst SMALL_RANK: Partial<Record<CardSize, string>> = {\n xs: \"text-base\",\n sm: \"text-xl\",\n};\nconst SMALL_SUIT: Partial<Record<CardSize, string>> = {\n xs: \"text-lg\",\n sm: \"text-2xl\",\n};\nconst BACK_EMBLEM_SIZE: Record<CardSize, number> = {\n xs: 14,\n sm: 18,\n md: 28,\n lg: 42,\n};\n\nfunction getSuitClass(suit: string): string {\n if (isRedSuit(suit)) return \"text-red-600\";\n return isKnownSuit(suit) ? \"text-neutral-900\" : \"\";\n}\n\nexport interface PlayingCardProps {\n /** Compact card code (\"As\", \"10h\") or null to render a face-down card back. */\n card: string | null;\n /** xs / sm show a single, larger, centered index; md / lg show corner + center. */\n size?: CardSize;\n /**\n * Accessible name. Defaults to `describeCard(card)` — \"Ace of spades\" rather\n * than the fragments \"A\" and \"♠\" a screen reader would otherwise read out.\n */\n label?: string;\n className?: string;\n}\n\nconst CARD_FACE = \"relative bg-gradient-to-br from-white via-white to-neutral-100 rounded-md border border-neutral-300/90 overflow-hidden shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]\";\n\n/**\n * A single playing card. Size is driven by the `size` prop (never viewport\n * breakpoints) so cards scale predictably inside any container. Small sizes\n * (xs / sm) show only a larger upright index, centered in the card.\n */\nexport function PlayingCard({ card, size = \"md\", label, className }: PlayingCardProps) {\n if (!card) return <CardBack size={size} className={className} label={label} />;\n\n const name = label ?? describeCard(card);\n\n const { rank, suit } = parseCard(card);\n const suitSymbol = getSuitSymbol(suit);\n const suitClass = getSuitClass(suit);\n const large = size === \"md\" || size === \"lg\";\n\n // Small cards: rank centered in the top half, suit centered in the bottom half.\n if (!large) {\n return (\n <div\n role=\"img\"\n aria-label={name}\n className={cn(SIZE_CLASSES[size], CARD_FACE, className)}\n style={{ aspectRatio: \"10/14\" }}\n >\n <div className={cn(\"absolute inset-0 flex flex-col leading-none\", suitClass)}>\n <div className=\"flex flex-1 items-center justify-center\">\n <span className={cn(SMALL_RANK[size], \"font-bold tracking-tight\")}>{rank}</span>\n </div>\n <div className=\"flex flex-1 items-center justify-center\">\n <span className={SMALL_SUIT[size]}>{suitSymbol}</span>\n </div>\n </div>\n </div>\n );\n }\n\n return (\n <div\n role=\"img\"\n aria-label={name}\n className={cn(SIZE_CLASSES[size], CARD_FACE, className)}\n style={{ aspectRatio: \"10/14\" }}\n >\n <div className={cn(\"absolute top-[3px] left-[5px] flex flex-col items-center leading-none\", suitClass)}>\n <span className={cn(CORNER_RANK[size], \"font-bold tracking-tight\")}>{rank}</span>\n <span className={cn(CORNER_SUIT[size], \"-mt-[1px]\")}>{suitSymbol}</span>\n </div>\n <div className={cn(\"absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180\", suitClass)}>\n <span className={cn(CORNER_RANK[size], \"font-bold tracking-tight\")}>{rank}</span>\n <span className={cn(CORNER_SUIT[size], \"-mt-[1px]\")}>{suitSymbol}</span>\n </div>\n <div className={cn(\"absolute inset-0 flex items-center justify-center opacity-90\", suitClass)}>\n <span className={CENTER_SUIT[size]}>{suitSymbol}</span>\n </div>\n </div>\n );\n}\n\nexport interface CardBackProps {\n size?: CardSize;\n /** Accessible name. Defaults to \"face-down card\". */\n label?: string;\n className?: string;\n}\n\nfunction CardBackEmblem({ size }: { size: CardSize }) {\n const s = BACK_EMBLEM_SIZE[size];\n return (\n <svg width={s} height={s} viewBox=\"0 0 40 40\" className=\"drop-shadow-[0_1px_2px_rgba(0,0,0,0.6)]\" aria-hidden=\"true\">\n <g fill=\"var(--color-card-back-emblem, #f0c04a)\" opacity=\"0.92\">\n <ellipse cx=\"20\" cy=\"11\" rx=\"4\" ry=\"7\" />\n <ellipse cx=\"20\" cy=\"29\" rx=\"4\" ry=\"7\" />\n <ellipse cx=\"11\" cy=\"20\" rx=\"7\" ry=\"4\" />\n <ellipse cx=\"29\" cy=\"20\" rx=\"7\" ry=\"4\" />\n </g>\n <circle cx=\"20\" cy=\"20\" r=\"3.2\" fill=\"var(--color-card-back, #5a0c18)\" />\n </svg>\n );\n}\n\n/** The face-down card back (dark red + gold rosette). */\nexport function CardBack({ size = \"md\", label = \"face-down card\", className }: CardBackProps) {\n return (\n <div\n role=\"img\"\n aria-label={label}\n className={cn(\n SIZE_CLASSES[size],\n \"relative rounded-md p-[3px] overflow-hidden\",\n \"bg-gradient-to-br from-card-back-light via-card-back to-card-back-dark\",\n \"border-2 border-card-back-ink/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]\",\n className,\n )}\n style={{ aspectRatio: \"10/14\" }}\n >\n <div className=\"h-full w-full rounded-[4px] border border-white/20 flex items-center justify-center bg-[repeating-linear-gradient(45deg,rgba(255,255,255,0.09)_0_6px,transparent_6px_12px),repeating-linear-gradient(-45deg,rgba(255,255,255,0.06)_0_6px,transparent_6px_12px)]\">\n <CardBackEmblem size={size} />\n </div>\n </div>\n );\n}\n\nexport interface HoleCardsProps {\n cards: (string | null)[];\n size?: CardSize;\n className?: string;\n /** Animate the cards dealing in, one after another. Default true. */\n deal?: boolean;\n /**\n * Change this to replay the deal animation for an unchanged hand (e.g. a hand\n * id or a round number). Without it, a card whose value changed re-deals on\n * its own, but a new hand holding the same two cards looks identical.\n */\n dealKey?: string | number;\n}\n\n/** A row of a player's hole cards. */\nexport function HoleCards({ cards, size = \"lg\", className, deal = true, dealKey }: HoleCardsProps) {\n return (\n <div className={cn(\"flex gap-2\", className)}>\n {cards.map((card, i) => (\n /*\n * Keying on the card rather than the index is what makes the deal\n * animation replay: React reuses an element whose key is unchanged, and\n * a reused element never restarts its CSS animation.\n */\n <div\n key={dealKey !== undefined ? `${dealKey}:${i}` : `${i}:${card ?? \"back\"}`}\n className={cn(deal && \"animate-deal-in\")}\n style={deal ? { animationDelay: `${i * DEAL_STAGGER_MS}ms` } : undefined}\n >\n <PlayingCard card={card} size={size} />\n </div>\n ))}\n </div>\n );\n}\n\nexport interface CommunityCardsProps {\n cards: (string | null)[];\n size?: CardSize;\n className?: string;\n /** Animate each card with a 3D flip. Default true. */\n animated?: boolean;\n /** Reserve a fixed number of slots so the row width never changes. */\n slots?: number;\n}\n\n/** A row of community cards, each flipping in with a staggered delay. */\nexport function CommunityCards({ cards, size = \"md\", className, animated = true, slots }: CommunityCardsProps) {\n const count = slots ?? cards.length;\n const list = Array.from({ length: count }, (_, i) => cards[i] ?? null);\n return (\n <div className={cn(\"flex gap-1.5\", animated && \"[perspective:800px]\", className)}>\n {list.map((card, i) => (\n <div key={i} className={SIZE_CLASSES[size]} style={{ aspectRatio: \"10/14\" }}>\n {card ? (\n <div\n className={cn(\"h-full w-full\", animated && \"animate-card-flip\")}\n style={animated ? { animationDelay: `${i * FLIP_STAGGER_MS}ms` } : undefined}\n >\n <PlayingCard card={card} size={size} />\n </div>\n ) : null}\n </div>\n ))}\n </div>\n );\n}\n","/**\n * Chip denominations and the breakdown of an amount into chips. Pure functions,\n * no React — the same split as `geometry.ts` and `layout.ts`.\n *\n * Amounts are in **chip units** (the smallest chip is 1), not in big blinds.\n */\n\nimport { mostVisible } from \"./color\";\n\nexport interface Denomination {\n /** Face value of the chip. */\n value: number;\n /**\n * Printed on the chip face. Defaults to the value, which is what you want for\n * a 5 or a 100; use it for a \"$5\" or a \"5K\" if your table reads differently.\n */\n label?: string;\n /**\n * CSS colour for the body. Used directly as the SVG `fill`, so it can be any\n * CSS colour — including `var(--color-chip-red)`.\n */\n color: string;\n /**\n * Colour of the eight edge spots. Defaults to white, or to a dark spot on a\n * light body — see `chipSpotColor`.\n */\n spotColor?: string;\n /**\n * Optional `fill-*` utility applied on top of `color`. The default set uses\n * these so the `--color-chip-*` theme tokens are actually emitted by Tailwind\n * (it only outputs a theme variable something references) and can therefore be\n * overridden from a consumer's `@theme` block like every other token.\n *\n * A class wins over the `fill` attribute, so `color` acts as the fallback.\n */\n className?: string;\n}\n\n/**\n * The classic low-stakes set: white 1, red 5, blue 10, green 25, black 100.\n *\n * Blue is the one that varies between houses — plenty of rooms use blue for\n * $10 and plenty for $20. Ten is the default here; override\n * `denominations` to use twenty.\n */\nexport const DEFAULT_DENOMINATIONS: Denomination[] = [\n { value: 100, color: \"#1a1a2e\", className: \"fill-chip-black\" },\n { value: 25, color: \"#27ae60\", className: \"fill-chip-green\" },\n { value: 10, color: \"#2980b9\", className: \"fill-chip-blue\" },\n { value: 5, color: \"#c0392b\", className: \"fill-chip-red\" },\n { value: 1, color: \"#ecf0f1\", className: \"fill-chip-white\" },\n];\n\n/** The two inks a chip's edge spots can be drawn in. */\nconst SPOT_CANDIDATES = [\"rgba(255,255,255,0.9)\", \"rgba(0,0,0,0.45)\"] as const;\n\n/**\n * Spot colour for a chip body: whichever of the two inks is measurably more\n * visible on it.\n *\n * Derived from contrast rather than a luminance threshold, because the obvious\n * implementation — always white spots — puts the white chip\n * (`--color-chip-white`, #ecf0f1) at a 1.13:1 contrast ratio against its own\n * body, so it renders as a blank oval with no edge at all. Colours we cannot\n * measure keep white spots.\n */\nexport function chipSpotColor(bodyColor: string): string {\n return mostVisible(SPOT_CANDIDATES, bodyColor);\n}\n\nexport interface ChipStackEntry extends Denomination {\n /** How many chips of this denomination the amount needs. */\n count: number;\n}\n\n/**\n * Break an amount into chips, largest denomination first (the greedy breakdown\n * a dealer actually uses).\n *\n * Anything below the smallest denomination is left out rather than rounded up —\n * `chipsTotal()` tells you what the chips add up to, so you can spot the\n * remainder. Non-integer and negative amounts are floored/clamped to 0.\n */\nexport function chipBreakdown(\n amount: number,\n denominations: Denomination[] = DEFAULT_DENOMINATIONS,\n): ChipStackEntry[] {\n const usable = denominations.filter((d) => Number.isFinite(d.value) && d.value >= 1);\n const sorted = [...usable].sort((a, b) => b.value - a.value);\n\n let remaining = Math.max(0, Math.floor(Number.isFinite(amount) ? amount : 0));\n const entries: ChipStackEntry[] = [];\n\n for (const denomination of sorted) {\n const count = Math.floor(remaining / denomination.value);\n if (count > 0) {\n entries.push({ ...denomination, count });\n remaining -= count * denomination.value;\n }\n }\n return entries;\n}\n\n/** What a set of chip entries adds up to. Compare with the amount to see the remainder. */\nexport function chipsTotal(entries: ChipStackEntry[]): number {\n return entries.reduce((sum, e) => sum + e.value * e.count, 0);\n}\n\n/** Total number of physical chips in a breakdown. */\nexport function chipCount(entries: ChipStackEntry[]): number {\n return entries.reduce((sum, e) => sum + e.count, 0);\n}\n","import { useId, type ReactNode } from \"react\";\nimport { cn } from \"../cn\";\nimport { chipBreakdown, chipCount, chipSpotColor, DEFAULT_DENOMINATIONS, type Denomination } from \"../chips\";\nimport { type ChipSize } from \"../sizes\";\n\n/*\n * Shares the package's size vocabulary, so a size added to `sizes.ts` is a\n * compile error here until it is given a width.\n */\nexport type { ChipSize } from \"../sizes\";\n\n/** Chip width in px. Height is 0.625x that, so a chip sits on a table like a disc. */\nconst CHIP_WIDTH: Record<ChipSize, number> = { sm: 22, md: 30, lg: 42 };\n\n/** Eight edge spots, evenly spaced around the rim. */\nconst SPOT_ANGLES = [0, 45, 90, 135, 180, 225, 270, 315];\n\nexport interface ChipProps {\n /** Printed on the centre of the face. */\n value?: number | string;\n /** CSS colour for the body. */\n color?: string;\n /** `fill-*` utility applied over `color` — see `Denomination.className`. */\n colorClassName?: string;\n /** Colour of the eight edge spots. Defaults to `chipSpotColor(color)`. */\n spotColor?: string;\n /** Default `\"md\"`. */\n size?: ChipSize;\n /** Accessible name. Defaults to `\"<value> chip\"`. */\n label?: string;\n className?: string;\n}\n\n/**\n * A single chip, drawn as SVG: a coloured disc with a darker edge for\n * thickness, eight white edge spots, a rim highlight and a white centre carrying\n * the value.\n *\n * The colours come from `--color-chip-*` via `fill-*` utilities, so the chip\n * palette is themeable like every other token.\n */\nexport function Chip({\n value,\n color = \"#2980b9\",\n colorClassName,\n spotColor,\n size = \"md\",\n label,\n className,\n}: ChipProps) {\n // useId() contains colons, which are legal in an id but fiddly inside url(#…)\n const clipId = `chip-clip-${useId().replace(/:/g, \"\")}`;\n const width = CHIP_WIDTH[size];\n const spots = spotColor ?? chipSpotColor(color);\n\n return (\n <svg\n width={width}\n height={width * 0.625}\n viewBox=\"0 0 48 30\"\n role=\"img\"\n aria-label={label ?? (value !== undefined ? `${value} chip` : \"chip\")}\n className={className}\n >\n <defs>\n <clipPath id={clipId}>\n <ellipse cx=\"24\" cy=\"15\" rx=\"22\" ry=\"13\" />\n </clipPath>\n </defs>\n\n {/*\n * Thickness: the same disc pushed down, darkened. The top face is drawn\n * over it, so only the exposed crescent of the dark copy shows and the\n * chip reads as a solid object rather than a flat oval.\n */}\n <ellipse cx=\"24\" cy=\"18\" rx=\"22\" ry=\"13\" className={colorClassName} fill={color} />\n <ellipse cx=\"24\" cy=\"18\" rx=\"22\" ry=\"13\" fill=\"rgba(0,0,0,0.32)\" />\n\n {/* top face */}\n <ellipse cx=\"24\" cy=\"15\" rx=\"22\" ry=\"13\" className={colorClassName} fill={color} />\n\n <g clipPath={`url(#${clipId})`}>\n {SPOT_ANGLES.map((angle) => {\n const rad = (angle * Math.PI) / 180;\n return (\n <rect\n key={angle}\n x={-1.7}\n y={-3}\n width={3.4}\n height={6}\n rx={1.3}\n fill={spots}\n transform={`translate(${24 + 19.3 * Math.cos(rad)} ${15 + 10.7 * Math.sin(rad)}) rotate(${angle})`}\n />\n );\n })}\n </g>\n\n {/* rim highlight */}\n <ellipse cx=\"24\" cy=\"15\" rx=\"21\" ry=\"12.1\" fill=\"none\" stroke=\"rgba(255,255,255,0.3)\" strokeWidth=\"1.2\" />\n\n {/* centre */}\n <ellipse cx=\"24\" cy=\"15\" rx=\"13.4\" ry=\"7.9\" fill=\"rgba(255,255,255,0.94)\" />\n <ellipse cx=\"24\" cy=\"15\" rx=\"13.4\" ry=\"7.9\" fill=\"none\" stroke=\"rgba(0,0,0,0.18)\" strokeWidth=\"0.8\" />\n\n {value !== undefined && (\n <text\n x=\"24\"\n y=\"18.3\"\n textAnchor=\"middle\"\n fontSize=\"9.5\"\n fontWeight=\"700\"\n fill=\"#171717\"\n fontFamily=\"ui-monospace, SFMono-Regular, Menlo, monospace\"\n >\n {value}\n </text>\n )}\n </svg>\n );\n}\n\nexport interface ChipStackProps {\n /** Amount in chip units — see `chipBreakdown`. */\n amount: number;\n /** Default `\"md\"`. */\n size?: ChipSize;\n /** Default `DEFAULT_DENOMINATIONS` (white 1, red 5, blue 10, green 25, black 100). */\n denominations?: Denomination[];\n /**\n * Most chips to draw, biggest denominations first. A 10,000 bet would\n * otherwise be 100 black chips. The count of the chips left off is shown\n * underneath. Default 12.\n */\n maxChips?: number;\n /** Content under the stack — usually the formatted amount. */\n label?: ReactNode;\n className?: string;\n}\n\n/**\n * The chips for an amount: a real stack, largest denomination on top.\n *\n * The accessible name is the full breakdown (\"240 in 4 chips: 2x100, 1x25,\n * 1x10\"), so a screen reader gets the value rather than four anonymous images.\n */\nexport function ChipStack({\n amount,\n size = \"md\",\n denominations = DEFAULT_DENOMINATIONS,\n maxChips = 12,\n label,\n className,\n}: ChipStackProps) {\n const entries = chipBreakdown(amount, denominations);\n const total = chipCount(entries);\n\n const drawn: typeof entries = [];\n for (const entry of entries) {\n for (let i = 0; i < entry.count && drawn.length < maxChips; i++) drawn.push(entry);\n if (drawn.length >= maxChips) break;\n }\n const hidden = total - drawn.length;\n\n const offset = Math.max(3, Math.round(CHIP_WIDTH[size] * 0.17));\n const breakdown = entries.map((e) => `${e.count}x${e.label ?? e.value}`).join(\", \");\n\n return (\n <div\n className={cn(\"inline-flex flex-col items-center gap-1\", className)}\n role=\"img\"\n aria-label={`${amount} in ${total} ${total === 1 ? \"chip\" : \"chips\"}${breakdown ? `: ${breakdown}` : \"\"}`}\n >\n {drawn.length > 0 && (\n // Presentation only — the stack already carries the accessible name.\n <div aria-hidden=\"true\" className=\"flex flex-col items-center drop-shadow-[0_3px_4px_rgba(0,0,0,0.5)]\">\n {drawn.map((entry, i) => (\n // Later chips are tucked under the ones above them, so z-index has\n // to descend as the stack grows.\n <div key={i} className=\"relative\" style={{ marginTop: i === 0 ? 0 : -offset, zIndex: drawn.length - i }}>\n <Chip\n value={entry.label ?? entry.value}\n color={entry.color}\n colorClassName={entry.className}\n spotColor={entry.spotColor}\n size={size}\n />\n </div>\n ))}\n </div>\n )}\n\n {hidden > 0 && (\n <span className=\"font-mono text-[10px] leading-none text-neutral-400\">+{hidden}</span>\n )}\n {label !== undefined && <span className=\"leading-none\">{label}</span>}\n </div>\n );\n}\n","import { createContext, useContext } from \"react\";\n\n/** The seat frame size a table laid its ring out for. */\nexport interface SeatSize {\n width: number;\n height: number;\n}\n\n/**\n * Provided by `CardTable`, consumed by `Seat`.\n *\n * Without it the frame size has two sources: `CardTable`'s `seatWidth` /\n * `seatHeight` (used to position the ring) and `Seat`'s own `width` / `height`\n * (used to draw the frame). They share a default and nothing else, so shrinking\n * a seat to fit the ring only worked when the caller threaded the size through by\n * hand. A `Seat` rendered outside any table gets `null` and falls back to its own\n * defaults.\n */\nexport const SeatSizeContext = createContext<SeatSize | null>(null);\n\n/** The frame size the surrounding table laid out, or `null` outside a table. */\nexport function useSeatSize(): SeatSize | null {\n return useContext(SeatSizeContext);\n}\n","import type { CSSProperties, ReactNode } from \"react\";\nimport { cn } from \"../cn\";\nimport { DEFAULT_SEAT_H, DEFAULT_SEAT_W } from \"../layout\";\nimport { useSeatSize } from \"./seatSize\";\n\nexport interface SeatProps {\n /** Turn/active highlight (generic). Animated by default. */\n isActive?: boolean;\n /** Winner highlight (generic). Animated by default. */\n isWinner?: boolean;\n /** Dimmed, e.g. folded or out (generic). */\n isDimmed?: boolean;\n /** Enable the turn/winner glow animations. Default true. */\n animated?: boolean;\n /** Render a default 👑 in the top-left corner when `isWinner`. Default false. */\n winnerCrown?: boolean;\n /** Optional click handler (e.g. \"sit here\"). */\n onClick?: () => void;\n\n /** Slot: top row inside the frame — name / badges. */\n header?: ReactNode;\n /** Slot: center content — 0..N cards, avatar, anything. */\n children?: ReactNode;\n /** Slot: bottom row inside the frame — stack / status. */\n footer?: ReactNode;\n /** Slot: floating element at the frame's top-right (legacy alias of topRight). */\n badge?: ReactNode;\n /** Slot: overlay centered over the frame. */\n overlay?: ReactNode;\n\n /** Slot: floating content above the frame, centered (e.g. an action bubble). */\n above?: ReactNode;\n /** Slot: content below the frame, centered (e.g. countdown / bet pill). */\n below?: ReactNode;\n /** Slot: floating content at the frame's top-left corner (e.g. a crown). */\n topLeft?: ReactNode;\n /** Slot: floating content at the frame's top-right corner (e.g. a kick button). */\n topRight?: ReactNode;\n /** Slot: floating content centered above the top edge (e.g. D/SB/BB badges). */\n topCenter?: ReactNode;\n\n /**\n * Frame width. Defaults to the size the surrounding `CardTable` laid out for\n * this seat, or 160 outside a table — the table is the one that knows how much\n * room the ring has.\n */\n width?: number;\n /** Frame height. Defaults to the laid-out size, or 176 outside a table. */\n height?: number;\n className?: string;\n /** Extra classes for the inner frame, to override visuals. */\n frameClassName?: string;\n /** Inline styles for the inner frame. */\n frameStyle?: CSSProperties;\n}\n\n/**\n * A neutral seat: a frame with generic visual states and named slots. Put\n * whatever you want into `header` / `children` / `footer`, and use\n * `above` / `below` / `topLeft` / `topRight` / `topCenter` / `overlay` for\n * floating bits. It knows nothing about any particular game.\n */\nexport function Seat({\n isActive = false,\n isWinner = false,\n isDimmed = false,\n animated = true,\n winnerCrown = false,\n onClick,\n header,\n children,\n footer,\n badge,\n overlay,\n above,\n below,\n topLeft,\n topRight,\n topCenter,\n width,\n height,\n className,\n frameClassName,\n frameStyle,\n}: SeatProps) {\n const laidOut = useSeatSize();\n const frameWidth = width ?? laidOut?.width ?? DEFAULT_SEAT_W;\n const frameHeight = height ?? laidOut?.height ?? DEFAULT_SEAT_H;\n\n const activeClass = animated\n ? \"border-gold animate-turn-glow\"\n : \"border-gold shadow-[0_0_18px_color-mix(in_oklab,var(--color-gold)_50%,transparent)]\";\n const winnerClass = animated\n ? \"border-gold animate-winner-glow\"\n : \"border-gold shadow-[0_0_0_3px_color-mix(in_oklab,var(--color-gold)_90%,transparent)]\";\n\n const effTopLeft =\n topLeft !== undefined\n ? topLeft\n : isWinner && winnerCrown\n ? <span className=\"text-3xl drop-shadow-[0_2px_4px_rgba(0,0,0,0.6)] animate-bounce\">👑</span>\n : undefined;\n\n return (\n <div className={cn(\"relative flex flex-col items-center\", className)} style={{ width: frameWidth }}>\n {topCenter !== undefined && (\n <div className=\"absolute -top-2 left-1/2 z-20 flex -translate-x-1/2 gap-1\">{topCenter}</div>\n )}\n {effTopLeft !== undefined && <div className=\"absolute -top-3 -left-2 z-40\">{effTopLeft}</div>}\n {topRight !== undefined && <div className=\"absolute -top-2 -right-2 z-40\">{topRight}</div>}\n {above !== undefined && (\n <div className=\"pointer-events-none absolute -top-12 left-1/2 z-30 -translate-x-1/2\">{above}</div>\n )}\n\n <div\n role={onClick ? \"button\" : undefined}\n tabIndex={onClick ? 0 : undefined}\n onClick={onClick}\n onKeyDown={\n onClick\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") onClick();\n }\n : undefined\n }\n style={{ width: frameWidth, height: frameHeight, ...frameStyle }}\n className={cn(\n \"relative flex flex-col items-center justify-between gap-1 rounded-2xl border bg-neutral-900/80 p-2 text-neutral-100 transition\",\n \"border-neutral-700\",\n isActive && activeClass,\n isWinner && winnerClass,\n isDimmed && \"opacity-50\",\n onClick && \"cursor-pointer hover:border-neutral-500 focus:outline-none focus-visible:ring-2 focus-visible:ring-gold\",\n frameClassName,\n )}\n >\n {header !== undefined && (\n <div className=\"flex w-full shrink-0 items-center justify-between gap-1 text-[11px]\">{header}</div>\n )}\n <div className=\"flex min-h-0 flex-1 items-center justify-center\">{children}</div>\n {footer !== undefined && (\n <div className=\"flex w-full shrink-0 items-center justify-between text-[11px] text-neutral-300\">{footer}</div>\n )}\n\n {overlay !== undefined && (\n <div className=\"pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 text-center\">{overlay}</div>\n )}\n </div>\n\n {badge !== undefined && <div className=\"absolute top-0 right-0 z-40\">{badge}</div>}\n {below !== undefined && <div className=\"mt-1 flex flex-col items-center gap-1\">{below}</div>}\n </div>\n );\n}\n","import { useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../cn\";\nimport { COUNT_UP_MS } from \"../motion\";\n\n/**\n * The default rendering of an amount: the number, grouped by the locale, with no\n * unit attached.\n *\n * This library does not know whether your numbers are chips, dollars, big blinds\n * or points, so it does not invent a unit. `formatBB` is available as an opt-in\n * poker helper — `<Pot amount={n} format={(v) => formatBB(v, 100)} />`.\n */\nconst defaultFormat = (amount: number): string => amount.toLocaleString();\n\n/** Whether the user has asked for less motion. Guarded: jsdom and SSR have no matchMedia. */\nfunction prefersReducedMotion(): boolean {\n if (typeof window === \"undefined\" || typeof window.matchMedia !== \"function\") return false;\n return window.matchMedia(\"(prefers-reduced-motion: reduce)\").matches;\n}\n\nexport interface PotProps {\n amount: number;\n /** Optional label, e.g. \"Main\" / \"Side\". */\n label?: string;\n /**\n * Formatter for the amount. Defaults to a plain, locale-grouped number —\n * pass one if your amounts have a unit.\n */\n format?: (amount: number) => string;\n /** Animate the amount counting up when it changes. Default true. */\n animate?: boolean;\n className?: string;\n}\n\n/** Smoothly count a number toward its target (presentational only). */\nfunction CountUp({ value, format }: { value: number; format: (n: number) => string }) {\n const [shown, setShown] = useState(value);\n // Mirrors `shown`, so an interrupted animation resumes from wherever the\n // number actually is instead of from the value it started at.\n const shownRef = useRef(value);\n\n useEffect(() => {\n const from = shownRef.current;\n const to = value;\n if (from === to) return;\n // The media query in styles.css cannot reach a rAF loop, so the JS animation\n // has to check for itself. Jump straight to the value instead.\n if (prefersReducedMotion()) {\n shownRef.current = to;\n setShown(to);\n return;\n }\n let raf = 0;\n const dur = COUNT_UP_MS;\n // Time the animation off the rAF timestamps themselves and never against\n // `performance.now()`: that is a second clock with a different origin, and\n // its skew against the rAF clock is unbounded. Mixing them made `t`\n // negative, which pushed `eased` outside [0, 1] and sent the counter off\n // backwards (reproducibly to -15698 in jsdom, where the two clocks start\n // ~390ms apart).\n let origin: number | null = null;\n const tick = (now: number) => {\n if (origin === null) origin = now;\n const t = Math.min(1, Math.max(0, (now - origin) / dur));\n const eased = 1 - Math.pow(1 - t, 3);\n const next = Math.round(from + (to - from) * eased);\n shownRef.current = next;\n setShown(next);\n if (t < 1) raf = requestAnimationFrame(tick);\n };\n raf = requestAnimationFrame(tick);\n return () => cancelAnimationFrame(raf);\n }, [value]);\n\n return <>{format(shown)}</>;\n}\n\n/** A small, standalone pot pill. Optional — the table does not require it. */\nexport function Pot({ amount, label, format = defaultFormat, animate = true, className }: PotProps) {\n return (\n <div className={cn(\"flex items-center gap-2 font-mono text-sm text-gold\", className)}>\n {label && <span className=\"text-neutral-400\">{label}</span>}\n <span className=\"whitespace-nowrap rounded-full border border-gold/50 bg-neutral-900/80 px-3 py-1 shadow-inner\">\n {animate ? <CountUp value={amount} format={format} /> : format(amount)}\n </span>\n </div>\n );\n}\n","import { useRef, useLayoutEffect, useState, useMemo, type ReactNode } from \"react\";\nimport { cn } from \"../cn\";\nimport { ellipsePoint, seatAngle } from \"../geometry\";\nimport {\n DEFAULT_BET_SCALE,\n DEFAULT_FELT_GAP,\n DEFAULT_MARGIN_X,\n DEFAULT_MARGIN_Y,\n DEFAULT_SEAT_GAP,\n DEFAULT_SEAT_H,\n DEFAULT_SEAT_W,\n DESIGN_H,\n DESIGN_W,\n layoutSeats,\n type EllipseBox,\n} from \"../layout\";\nimport { SeatSizeContext } from \"./seatSize\";\n\n/*\n * The design constants live in `layout.ts` with the rest of the geometry. They\n * are re-exported here so imports from this module keep working.\n */\nexport {\n DEFAULT_BET_SCALE,\n DEFAULT_FELT_GAP,\n DEFAULT_MARGIN_X,\n DEFAULT_MARGIN_Y,\n DEFAULT_SEAT_GAP,\n DESIGN_H,\n DESIGN_W,\n} from \"../layout\";\n\n/** Geometry CardTable laid a seat frame out for, handed to `renderSeat`. */\nexport interface SeatBox {\n index: number;\n isYou: boolean;\n /** Centre of the frame, in design space. */\n x: number;\n y: number;\n /** Frame size the ring was laid out for (≤ `seatWidth`/`seatHeight`). */\n width: number;\n height: number;\n}\n\nexport interface CardTableProps {\n /**\n * Number of seats. Default 6.\n *\n * There is no upper limit. The seat ring shrinks the frames until `seatGap` is\n * honoured, so 11–16 seats lay out fine (measured: 11 -> 126x139 frames,\n * 16 -> 80x88). Past roughly 16 the frames hit the 50% shrink floor and start\n * to overlap — that is the caller's call, not something to clamp silently.\n */\n seatCount?: number;\n /** Optional per-seat content. Length should equal `seatCount`. */\n seats?: (ReactNode | null)[];\n /**\n * Alternative to `seats`: render a seat by index. Receives the laid-out\n * frame box as a third argument, so an auto-shrunk seat can be sized to\n * match the ring: `<Seat width={box.width} height={box.height} />`.\n */\n renderSeat?: (index: number, isYou: boolean, box: SeatBox) => ReactNode;\n /**\n * Optional per-seat \"bet\" content, auto-positioned on the betting line\n * (`betRadiusScale`) along the seat-to-center radius, so it faces the middle\n * of the table. Length should equal `seatCount`; `null` entries are skipped.\n */\n bets?: (ReactNode | null)[];\n /** Alternative to `bets`: render a bet by index (return `null` to skip). */\n renderBet?: (index: number) => ReactNode;\n /**\n * Radius of the bet ring, as a fraction of the table radius. Default 0.72.\n * The drawn betting line follows this value. Clamped to 0.2–1.\n */\n betRadiusScale?: number;\n /**\n * Angle (degrees) of seat 0. Default 90° = directly below the table centre.\n * Seats advance by `360 / seatCount` from there, so 2–10 players spread evenly.\n */\n startAngle?: number;\n /** Which seat is the viewer (adds no offset; only flags `isYou`). Default null. */\n yourSeatIndex?: number | null;\n /** Optional content in the middle of the table (cards, pot, anything). */\n center?: ReactNode;\n /** Ambient spotlight + felt shimmer. Default true. */\n ambient?: boolean;\n /** Highlight the table edge (e.g. when it is your turn). Default false. */\n yourTurn?: boolean;\n /** Winning seat indices; when non-empty, confetti falls. */\n winnerSeats?: number[];\n /** Show confetti for winners. Default true. */\n confetti?: boolean;\n /**\n * Confetti colours. Defaults to a party palette. This is a prop rather than a\n * theme token because the colours land in inline styles, which Tailwind's\n * tree-shaking cannot see — a custom property nothing references never gets\n * emitted.\n */\n confettiColors?: string[];\n /** Preferred seat frame width. Default 160. */\n seatWidth?: number;\n /** Preferred seat frame height. Default 176. */\n seatHeight?: number;\n /**\n * Minimum clearance to keep between neighbouring seat frames, in design-space\n * px. Frames shrink uniformly (down to 50%) until it is honoured, which is what\n * keeps 9–10 seat tables from looking cramped and what lets 11–16 seat tables\n * lay out at all. `0` disables it.\n * Only applies when `renderSeat` is used (it is the only form that can size\n * the frames); the `seats` array keeps the size you were given.\n * Default 24.\n */\n seatGap?: number;\n /** Design-space width. Default 1200. */\n designWidth?: number;\n /** Design-space height. Default 900. */\n designHeight?: number;\n /**\n * Rail inset as a fraction of the design box (default 164/1200).\n * Proportional so a custom design box stays in proportion.\n */\n marginX?: number;\n /** Rail inset as a fraction of the design box (default 196/900). */\n marginY?: number;\n /** Felt inset from the rail, in design-space px. Default 11. */\n feltGap?: number;\n /**\n * How the design box relates to the container.\n * - `\"design\"` (default): scale the fixed design box to fit — preserves the\n * table's proportions but letterboxes non-matching containers.\n * - `\"container\"`: derive the design width from the container's aspect ratio\n * (clamped to 0.5–2.5) so the table fills the box instead of letterboxing.\n * Use this for phone-portrait layouts. Pair with `designHeight` to set the\n * scale.\n */\n fit?: \"design\" | \"container\";\n className?: string;\n}\n\n/** Aspect range `fit=\"container\"` will not go outside of. */\nconst MIN_FIT_ASPECT = 0.5;\nconst MAX_FIT_ASPECT = 2.5;\n\nconst DEFAULT_CONFETTI_COLORS = [\"#f39c12\", \"#e74c3c\", \"#2ecc71\", \"#3498db\", \"#f1c40f\", \"#9b59b6\"];\n\n/**\n * A responsive, game-agnostic card table. It scales a design box (1200x900 by\n * default) onto its container and arranges `seatCount` seats around the felt.\n * Purely presentational: supply seat content and an optional `center` slot.\n */\nexport function CardTable({\n seatCount = 6,\n seats,\n renderSeat,\n bets,\n renderBet,\n betRadiusScale = DEFAULT_BET_SCALE,\n startAngle = 90,\n yourSeatIndex = null,\n center,\n ambient = true,\n yourTurn = false,\n winnerSeats = [],\n confetti = true,\n confettiColors = DEFAULT_CONFETTI_COLORS,\n seatWidth = DEFAULT_SEAT_W,\n seatHeight = DEFAULT_SEAT_H,\n seatGap = DEFAULT_SEAT_GAP,\n designWidth = DESIGN_W,\n designHeight = DESIGN_H,\n marginX = DEFAULT_MARGIN_X,\n marginY = DEFAULT_MARGIN_Y,\n feltGap = DEFAULT_FELT_GAP,\n fit = \"design\",\n className,\n}: CardTableProps) {\n const requested = Math.floor(seatCount);\n const count = Number.isFinite(requested) ? Math.max(1, requested) : 1;\n const outerRef = useRef<HTMLDivElement>(null);\n const [box, setBox] = useState({ w: 0, h: 0 });\n\n useLayoutEffect(() => {\n const el = outerRef.current;\n if (!el) return;\n const compute = () => {\n const w = el.clientWidth;\n const h = el.clientHeight;\n if (!w || !h) return;\n setBox({ w, h });\n };\n compute();\n const ro = new ResizeObserver(compute);\n ro.observe(el);\n return () => ro.disconnect();\n }, []);\n\n const geo = useMemo(() => {\n const dh = designHeight;\n const dw =\n fit === \"container\" && box.w > 0 && box.h > 0\n ? dh * Math.min(MAX_FIT_ASPECT, Math.max(MIN_FIT_ASPECT, box.w / box.h))\n : designWidth;\n\n const layout = layoutSeats({\n count,\n designWidth: dw,\n designHeight: dh,\n marginX,\n marginY,\n feltGap,\n seatWidth,\n seatHeight,\n // Always applied. Every Seat inside the table is told its frame size\n // through context, so the shrink is no longer limited to `renderSeat`.\n seatGap,\n startAngle,\n betRadiusScale,\n });\n\n const scale = box.w > 0 && box.h > 0 ? Math.min(box.w / dw, box.h / dh) : 1;\n return { designW: dw, designH: dh, scale, layout };\n }, [\n count,\n designWidth,\n designHeight,\n marginX,\n marginY,\n feltGap,\n seatWidth,\n seatHeight,\n seatGap,\n startAngle,\n fit,\n betRadiusScale,\n box.w,\n box.h,\n ]);\n\n const { designW, designH, scale, layout } = geo;\n const { cx, cy, seatW, seatH, betRx, betRy, boxes } = layout;\n const hasBets = bets !== undefined || renderBet !== undefined;\n\n /*\n * Every ellipse comes from `layoutSeats` as an inset box, so this component\n * computes no geometry of its own. The betting line in particular used to be\n * derived here from a module constant while the chips were positioned from the\n * `betRadiusScale` prop, which is how the two drifted apart.\n *\n * Explicit top/right/bottom/left rather than the `inset` shorthand: it is what\n * every browser and CSSOM implementation (incl. jsdom) agrees on.\n */\n const px = (b: EllipseBox) => ({\n top: `${b.top}px`,\n right: `${b.right}px`,\n bottom: `${b.bottom}px`,\n left: `${b.left}px`,\n });\n const tableFrameInset = px(boxes.table);\n const feltInset = px(boxes.felt);\n const rimInset = px(boxes.rim);\n const ringInset = px(boxes.glow);\n const betLineInset = px(boxes.bettingLine);\n\n return (\n <div\n ref={outerRef}\n className={cn(\"relative flex h-full w-full min-h-0 items-center justify-center overflow-hidden\", className)}\n >\n <div style={{ width: designW * scale, height: designH * scale }}>\n <div\n className=\"relative\"\n style={{ width: designW, height: designH, transform: `scale(${scale})`, transformOrigin: \"top left\" }}\n >\n <div\n className=\"absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--color-gold-light)_16%,transparent),transparent_72%)] blur-3xl\"\n aria-hidden=\"true\"\n />\n\n {/* wooden rail */}\n <div\n className=\"absolute rounded-[50%] bg-gradient-to-b from-rail via-rail-dark to-rail-deep 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)]\"\n style={tableFrameInset}\n aria-hidden=\"true\"\n />\n\n {/* felt */}\n <div\n className=\"absolute rounded-[50%] bg-[radial-gradient(ellipse_at_50%_32%,var(--color-felt-light),var(--color-felt)_55%,var(--color-felt-dark))] shadow-[inset_0_0_90px_rgba(0,0,0,0.55),inset_0_3px_10px_rgba(255,255,255,0.06)]\"\n style={feltInset}\n aria-hidden=\"true\"\n />\n\n {/* ambient: breathing spotlight + felt shimmer */}\n {ambient && (\n <>\n <div\n className=\"absolute animate-spotlight rounded-[50%] bg-[radial-gradient(closest-side,color-mix(in_oklab,var(--color-gold-glow)_14%,transparent),transparent_70%)] blur-2xl\"\n style={feltInset}\n aria-hidden=\"true\"\n />\n <div className=\"absolute overflow-hidden rounded-[50%]\" style={feltInset} aria-hidden=\"true\">\n <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%]\" />\n </div>\n </>\n )}\n\n {/* betting line */}\n <div\n data-part=\"betting-line\"\n className=\"absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]\"\n style={betLineInset}\n aria-hidden=\"true\"\n />\n\n {/* center suit watermark */}\n <div className=\"absolute flex items-center justify-center pointer-events-none\" style={feltInset} aria-hidden=\"true\">\n <span className=\"text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none\">\n ♠♥♦♣\n </span>\n </div>\n\n {/* your-turn edge glow */}\n {yourTurn && (\n <div\n className=\"pointer-events-none absolute z-10 animate-turn-glow rounded-[50%]\"\n style={ringInset}\n aria-hidden=\"true\"\n />\n )}\n\n {/* center slot (optional) */}\n {center !== undefined && (\n <div className=\"pointer-events-none absolute inset-0 flex flex-col items-center justify-center\">\n {center}\n </div>\n )}\n\n {/* seats */}\n <div className=\"absolute inset-0 z-20\">\n {/*\n * The context is what gives every `Seat` its frame size: one source\n * for it (the ring this table computed) instead of CardTable's\n * seatWidth and Seat's own width being two unrelated numbers that\n * happen to share a default.\n */}\n <SeatSizeContext.Provider value={{ width: seatW, height: seatH }}>\n {layout.seats.map((seat) => {\n const { index, x, y } = seat;\n const isYou = index === yourSeatIndex;\n const box: SeatBox = { index, isYou, x, y, width: seatW, height: seatH };\n const content = seats ? (seats[index] ?? null) : renderSeat ? renderSeat(index, isYou, box) : null;\n if (content === null) return null;\n return (\n <div\n key={index}\n data-part=\"seat\"\n data-seat={index}\n className=\"absolute\"\n style={{ left: `${x - seatW / 2}px`, top: `${y - seatH / 2}px` }}\n >\n {content}\n </div>\n );\n })}\n </SeatSizeContext.Provider>\n </div>\n\n {/* bets on the betting line (positioned toward the table center) */}\n {hasBets && (\n <div className=\"pointer-events-none absolute inset-0 z-20\">\n {layout.seats.map(({ index }) => {\n const content = bets ? (bets[index] ?? null) : renderBet ? renderBet(index) : null;\n if (content === null || content === undefined) return null;\n const pt = ellipsePoint(cx, cy, betRx, betRy, seatAngle(index, count, startAngle));\n return (\n <div\n key={index}\n data-seat-bet={index}\n className=\"absolute\"\n style={{ left: `${pt.x}px`, top: `${pt.y}px`, transform: \"translate(-50%, -50%)\" }}\n >\n {content}\n </div>\n );\n })}\n </div>\n )}\n\n {/* winner confetti */}\n {confetti && winnerSeats.length > 0 && (\n <div className=\"pointer-events-none absolute inset-0 z-30 overflow-hidden\" aria-hidden=\"true\">\n {Array.from({ length: 44 }).map((_, i) => {\n const w = 5 + (i % 4) * 2;\n const h = 9 + (i % 3) * 4;\n const dur = 1.7 + (i % 5) * 0.28;\n const delay = (i % 12) * 0.14;\n return (\n <span\n key={i}\n className=\"absolute top-[26%] animate-confetti rounded-[2px]\"\n style={{\n left: `${(i * 37) % 96}%`,\n width: w,\n height: h,\n backgroundColor: confettiColors[i % confettiColors.length],\n animationDuration: `${dur}s`,\n animationDelay: `${delay}s`,\n animationIterationCount: \"infinite\",\n }}\n />\n );\n })}\n </div>\n )}\n\n {/* outer rim highlight */}\n <div className=\"absolute rounded-[50%] border border-white/10 pointer-events-none\" style={rimInset} aria-hidden=\"true\" />\n </div>\n </div>\n </div>\n );\n}\n"],"mappings":"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","names":["jsx","jsxs","jsx","jsxs","jsx","jsxs","useRef","useState","Fragment","jsx","jsxs","useRef","useState","layout","scale","box"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stargazeryan/poker-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Themable React + Tailwind CSS v4 components for poker tables and playing cards.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ryan",
|
|
@@ -26,9 +26,11 @@
|
|
|
26
26
|
".": {
|
|
27
27
|
"types": "./dist/index.d.ts",
|
|
28
28
|
"import": "./dist/index.js",
|
|
29
|
-
"require": "./dist/index.cjs"
|
|
29
|
+
"require": "./dist/index.cjs",
|
|
30
|
+
"default": "./dist/index.js"
|
|
30
31
|
},
|
|
31
|
-
"./styles.css": "./src/styles.css"
|
|
32
|
+
"./styles.css": "./src/styles.css",
|
|
33
|
+
"./package.json": "./package.json"
|
|
32
34
|
},
|
|
33
35
|
"publishConfig": {
|
|
34
36
|
"access": "public"
|
|
@@ -36,8 +38,15 @@
|
|
|
36
38
|
"scripts": {
|
|
37
39
|
"build": "tsup",
|
|
38
40
|
"typecheck": "tsc --noEmit",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"test:consumer": "node scripts/check-consumer.mjs",
|
|
44
|
+
"docs": "vite --config demo/vite.config.ts",
|
|
45
|
+
"docs:build": "npm run build && node scripts/build-docs.mjs dist",
|
|
46
|
+
"demo": "npm run docs",
|
|
47
|
+
"demo:build": "npm run docs:build",
|
|
48
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
49
|
+
"check:release": "node scripts/check-release.mjs"
|
|
41
50
|
},
|
|
42
51
|
"keywords": [
|
|
43
52
|
"poker",
|
|
@@ -50,21 +59,32 @@
|
|
|
50
59
|
],
|
|
51
60
|
"peerDependencies": {
|
|
52
61
|
"react": ">=18",
|
|
53
|
-
"react-dom": ">=18"
|
|
62
|
+
"react-dom": ">=18",
|
|
63
|
+
"tailwindcss": ">=4"
|
|
54
64
|
},
|
|
55
65
|
"devDependencies": {
|
|
56
66
|
"@tailwindcss/vite": "^4.3.3",
|
|
67
|
+
"@testing-library/dom": "^10.4.2",
|
|
68
|
+
"@testing-library/react": "^16.3.3",
|
|
69
|
+
"@types/node": "^22.20.5",
|
|
57
70
|
"@types/react": "^19.2.0",
|
|
58
71
|
"@types/react-dom": "^19.2.0",
|
|
59
72
|
"@vitejs/plugin-react": "^4.3.0",
|
|
73
|
+
"@vitest/coverage-v8": "^3.2.7",
|
|
74
|
+
"jsdom": "^26.1.0",
|
|
75
|
+
"prism-react-renderer": "^2.4.1",
|
|
60
76
|
"react": "^19.2.0",
|
|
61
77
|
"react-dom": "^19.2.0",
|
|
62
78
|
"tailwindcss": "^4.3.3",
|
|
63
79
|
"tsup": "^8.3.0",
|
|
64
80
|
"typescript": "^5.6.0",
|
|
65
|
-
"vite": "^6.0.0"
|
|
81
|
+
"vite": "^6.0.0",
|
|
82
|
+
"vitest": "^3.2.7"
|
|
66
83
|
},
|
|
67
84
|
"dependencies": {
|
|
68
85
|
"tailwind-merge": "^3.7.0"
|
|
86
|
+
},
|
|
87
|
+
"engines": {
|
|
88
|
+
"node": ">=18"
|
|
69
89
|
}
|
|
70
90
|
}
|