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