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