@stargazeryan/poker-ui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +97 -2
- package/dist/index.cjs +386 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +102 -0
- package/dist/index.d.ts +102 -0
- package/dist/index.js +350 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -4
- package/src/styles.css +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,98 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @stargazeryan/poker-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Themable **React + Tailwind CSS v4** components for poker tables and playing cards.
|
|
4
|
+
|
|
5
|
+
- 🃏 `PlayingCard` / `CardBack` / `HoleCards` / `CommunityCards`
|
|
6
|
+
- 🎴 `PokerTable` — a responsive, scale-to-fit oval table with six seats
|
|
7
|
+
- 💺 `Seat` — presentational, fully controlled by a view-model
|
|
8
|
+
- 🎨 Theme tokens via Tailwind v4 `@theme`
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @stargazeryan/poker-ui
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Peer requirements: `react` >= 18, `react-dom` >= 18, and Tailwind CSS v4.
|
|
17
|
+
|
|
18
|
+
## Setup (Tailwind v4) — important
|
|
19
|
+
|
|
20
|
+
This is a **compiled package**, so your app's Tailwind build cannot see the
|
|
21
|
+
class names used inside it. Add the package as a content source and import its
|
|
22
|
+
theme CSS:
|
|
23
|
+
|
|
24
|
+
```css
|
|
25
|
+
/* app.css */
|
|
26
|
+
@import "tailwindcss";
|
|
27
|
+
@import "@stargazeryan/poker-ui/styles.css";
|
|
28
|
+
|
|
29
|
+
@source "../node_modules/@stargazeryan/poker-ui";
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
> Without the `@source` line, the components render unstyled. Adjust the path to
|
|
33
|
+
> wherever your `node_modules` lives.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
import { PokerTable, PlayingCard, CardBack, type SeatView } from "@stargazeryan/poker-ui";
|
|
39
|
+
|
|
40
|
+
const seats: (SeatView | null)[] = [
|
|
41
|
+
{ nickname: "You", stack: 2000, holeCards: ["As", "Kh"], isYou: true, isDealer: true },
|
|
42
|
+
{ nickname: "Bot", stack: 1500, holeCards: [null, null] },
|
|
43
|
+
null, null, null, null,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
export function Table() {
|
|
47
|
+
return (
|
|
48
|
+
<div style={{ height: 640 }}>
|
|
49
|
+
<PokerTable
|
|
50
|
+
seats={seats}
|
|
51
|
+
yourSeatIndex={0}
|
|
52
|
+
communityCards={["Ah", "Kd", "7c", null, null]}
|
|
53
|
+
pot={240}
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Cards use a compact code: rank + suit, suit as `s | h | d | c` — e.g. `"As"`,
|
|
61
|
+
`"10h"`, `"kd"`. Pass `null` for a face-down card back.
|
|
62
|
+
|
|
63
|
+
## Components
|
|
64
|
+
|
|
65
|
+
| Component | Purpose |
|
|
66
|
+
| --- | --- |
|
|
67
|
+
| `PlayingCard` | One card (`card`, `size`: `xs \| sm \| md \| lg`) |
|
|
68
|
+
| `CardBack` | Face-down card |
|
|
69
|
+
| `HoleCards` | A row of hole cards |
|
|
70
|
+
| `CommunityCards` | Five community cards with a staggered deal animation |
|
|
71
|
+
| `Seat` | Presentational seat (`seat: SeatView \| null`) |
|
|
72
|
+
| `PokerTable` | Full table; `seats`, `yourSeatIndex`, `communityCards`, `pot`/`pots`, `seatSize`, `renderSeat` |
|
|
73
|
+
|
|
74
|
+
Also exported: `cn`, `formatBB`, `BB`, `parseCard`, `getSuitSymbol`,
|
|
75
|
+
and the types `SeatView`, `PokerTableProps`, `PotRow`, `CardSize`.
|
|
76
|
+
|
|
77
|
+
## Theming
|
|
78
|
+
|
|
79
|
+
Override the tokens in your own `@theme` block, or pass `className` props:
|
|
80
|
+
|
|
81
|
+
```css
|
|
82
|
+
@theme {
|
|
83
|
+
--color-felt: #0b3d2e;
|
|
84
|
+
--color-gold: #ffcc33;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Demo
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm install
|
|
92
|
+
npm run demo # Vite dev server
|
|
93
|
+
npm run demo:build # static build
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT © 2026 Ryan
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
"use strict";
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
BB: () => BB,
|
|
25
|
+
CardBack: () => CardBack,
|
|
26
|
+
CommunityCards: () => CommunityCards,
|
|
27
|
+
HoleCards: () => HoleCards,
|
|
28
|
+
PlayingCard: () => PlayingCard,
|
|
29
|
+
PokerTable: () => PokerTable,
|
|
30
|
+
Seat: () => Seat,
|
|
31
|
+
cn: () => cn,
|
|
32
|
+
formatBB: () => formatBB,
|
|
33
|
+
getSuitSymbol: () => getSuitSymbol,
|
|
34
|
+
parseCard: () => parseCard
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(index_exports);
|
|
37
|
+
|
|
38
|
+
// src/cn.ts
|
|
39
|
+
function cn(...parts) {
|
|
40
|
+
return parts.filter(Boolean).join(" ");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/format.ts
|
|
44
|
+
var BB = 20;
|
|
45
|
+
function formatBB(amount) {
|
|
46
|
+
const bb = amount / BB;
|
|
47
|
+
return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// src/components/PlayingCard.tsx
|
|
51
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
52
|
+
var SIZE_CLASSES = {
|
|
53
|
+
xs: "w-8 h-11",
|
|
54
|
+
sm: "w-10 h-14",
|
|
55
|
+
md: "w-16 h-22",
|
|
56
|
+
lg: "w-24 h-32"
|
|
57
|
+
};
|
|
58
|
+
var CORNER_RANK = {
|
|
59
|
+
xs: "text-[11px]",
|
|
60
|
+
sm: "text-sm",
|
|
61
|
+
md: "text-lg",
|
|
62
|
+
lg: "text-2xl"
|
|
63
|
+
};
|
|
64
|
+
var CORNER_SUIT = {
|
|
65
|
+
xs: "text-[9px]",
|
|
66
|
+
sm: "text-xs",
|
|
67
|
+
md: "text-sm",
|
|
68
|
+
lg: "text-base"
|
|
69
|
+
};
|
|
70
|
+
var CENTER_SUIT = {
|
|
71
|
+
xs: "text-base",
|
|
72
|
+
sm: "text-xl",
|
|
73
|
+
md: "text-3xl",
|
|
74
|
+
lg: "text-5xl"
|
|
75
|
+
};
|
|
76
|
+
var BACK_EMBLEM_SIZE = {
|
|
77
|
+
xs: 14,
|
|
78
|
+
sm: 18,
|
|
79
|
+
md: 28,
|
|
80
|
+
lg: 42
|
|
81
|
+
};
|
|
82
|
+
function parseCard(card) {
|
|
83
|
+
if (!card || card.length < 2) return { rank: "", suit: "" };
|
|
84
|
+
return { rank: card.slice(0, -1), suit: card.slice(-1) };
|
|
85
|
+
}
|
|
86
|
+
function getSuitSymbol(suit) {
|
|
87
|
+
switch (suit) {
|
|
88
|
+
case "s":
|
|
89
|
+
return "\u2660";
|
|
90
|
+
case "h":
|
|
91
|
+
return "\u2665";
|
|
92
|
+
case "d":
|
|
93
|
+
return "\u2666";
|
|
94
|
+
case "c":
|
|
95
|
+
return "\u2663";
|
|
96
|
+
default:
|
|
97
|
+
return suit;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function getSuitClass(suit) {
|
|
101
|
+
switch (suit) {
|
|
102
|
+
case "s":
|
|
103
|
+
case "c":
|
|
104
|
+
return "text-neutral-900";
|
|
105
|
+
case "h":
|
|
106
|
+
case "d":
|
|
107
|
+
return "text-red-600";
|
|
108
|
+
default:
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function PlayingCard({ card, size = "md", className }) {
|
|
113
|
+
if (!card) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardBack, { size, className });
|
|
114
|
+
const { rank, suit } = parseCard(card);
|
|
115
|
+
const suitSymbol = getSuitSymbol(suit);
|
|
116
|
+
const suitClass = getSuitClass(suit);
|
|
117
|
+
const showCenter = size === "md" || size === "lg";
|
|
118
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
119
|
+
"div",
|
|
120
|
+
{
|
|
121
|
+
className: cn(
|
|
122
|
+
SIZE_CLASSES[size],
|
|
123
|
+
"relative bg-gradient-to-br from-white via-white to-neutral-100",
|
|
124
|
+
"rounded-md border border-neutral-300/90 overflow-hidden",
|
|
125
|
+
"shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]",
|
|
126
|
+
className
|
|
127
|
+
),
|
|
128
|
+
style: { aspectRatio: "10/14" },
|
|
129
|
+
children: [
|
|
130
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
|
|
131
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
|
|
132
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
|
|
133
|
+
] }),
|
|
134
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
|
|
135
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
|
|
136
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
|
|
137
|
+
] }),
|
|
138
|
+
showCenter && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("absolute inset-0 flex items-center justify-center opacity-90", suitClass), children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: CENTER_SUIT[size], children: suitSymbol }) })
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
function CardBackEmblem({ size }) {
|
|
144
|
+
const s = BACK_EMBLEM_SIZE[size];
|
|
145
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("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", children: [
|
|
146
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("g", { fill: "#f0c04a", opacity: "0.92", children: [
|
|
147
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "11", rx: "4", ry: "7" }),
|
|
148
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "20", cy: "29", rx: "4", ry: "7" }),
|
|
149
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "11", cy: "20", rx: "7", ry: "4" }),
|
|
150
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ellipse", { cx: "29", cy: "20", rx: "7", ry: "4" })
|
|
151
|
+
] }),
|
|
152
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("circle", { cx: "20", cy: "20", r: "3.2", fill: "#5a0c18" })
|
|
153
|
+
] });
|
|
154
|
+
}
|
|
155
|
+
function CardBack({ size = "md", className }) {
|
|
156
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
157
|
+
"div",
|
|
158
|
+
{
|
|
159
|
+
className: cn(
|
|
160
|
+
SIZE_CLASSES[size],
|
|
161
|
+
"relative rounded-md p-[3px] overflow-hidden",
|
|
162
|
+
"bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]",
|
|
163
|
+
"border-2 border-[#c9a24b]/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]",
|
|
164
|
+
className
|
|
165
|
+
),
|
|
166
|
+
style: { aspectRatio: "10/14" },
|
|
167
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("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)]", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CardBackEmblem, { size }) })
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
function HoleCards({ cards, size = "lg", className }) {
|
|
172
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("flex gap-2", className), children: cards.map((card, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlayingCard, { card, size }, i)) });
|
|
173
|
+
}
|
|
174
|
+
function CommunityCards({ cards, size = "md", className }) {
|
|
175
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: cn("flex gap-1.5", className), children: cards.map((card, i) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "animate-card-deal", style: { animationDelay: `${i * 90}ms` }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlayingCard, { card, size }) }, i)) });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/components/Seat.tsx
|
|
179
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
180
|
+
function Seat({ seat, size = "sm", className }) {
|
|
181
|
+
if (!seat) {
|
|
182
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
183
|
+
"div",
|
|
184
|
+
{
|
|
185
|
+
className: cn(
|
|
186
|
+
"w-40 h-44 rounded-2xl border-2 border-dashed border-white/10",
|
|
187
|
+
"flex items-center justify-center text-white/20 text-xs",
|
|
188
|
+
className
|
|
189
|
+
),
|
|
190
|
+
children: "Empty seat"
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const showCards = !seat.folded && !seat.busted && !!seat.holeCards && seat.holeCards.length > 0;
|
|
195
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: cn("relative w-40 h-44", className), children: [
|
|
196
|
+
seat.actionLabel && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "animate-bubble-pop absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-neutral-900 shadow-lg", children: seat.actionLabel }),
|
|
197
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
|
|
198
|
+
"div",
|
|
199
|
+
{
|
|
200
|
+
className: cn(
|
|
201
|
+
"flex h-full w-full flex-col items-center justify-between rounded-2xl border bg-neutral-900/80 p-2 backdrop-blur",
|
|
202
|
+
seat.isTurn ? "border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]" : "border-neutral-700",
|
|
203
|
+
seat.isWinner && "border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]",
|
|
204
|
+
seat.folded && "opacity-50"
|
|
205
|
+
),
|
|
206
|
+
children: [
|
|
207
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex w-full items-center justify-between text-[11px]", children: [
|
|
208
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("span", { className: "truncate font-semibold text-neutral-100", children: [
|
|
209
|
+
seat.isWinner ? "\u{1F451} " : "",
|
|
210
|
+
seat.nickname,
|
|
211
|
+
seat.isYou ? " \u{1F448}" : ""
|
|
212
|
+
] }),
|
|
213
|
+
seat.isDealer && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "flex h-4 w-4 items-center justify-center rounded-full bg-white text-[9px] font-bold text-neutral-900", children: "D" })
|
|
214
|
+
] }),
|
|
215
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "flex flex-1 items-center justify-center", children: seat.busted ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "text-xs font-semibold text-red-400", children: "Busted" }) : showCards ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(HoleCards, { cards: seat.holeCards, size }) : /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex gap-2", children: [
|
|
216
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CardBack, { size }),
|
|
217
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(CardBack, { size })
|
|
218
|
+
] }) }),
|
|
219
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "flex w-full items-center justify-between text-[11px] text-neutral-300", children: [
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-mono", children: formatBB(seat.stack) }),
|
|
221
|
+
seat.allIn ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("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__ */ (0, import_jsx_runtime2.jsx)("span", { className: "font-mono text-amber-300", children: formatBB(seat.currentBet) }) : null
|
|
222
|
+
] })
|
|
223
|
+
]
|
|
224
|
+
}
|
|
225
|
+
)
|
|
226
|
+
] });
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// src/components/PokerTable.tsx
|
|
230
|
+
var import_react = require("react");
|
|
231
|
+
var import_jsx_runtime3 = require("react/jsx-runtime");
|
|
232
|
+
var SEAT_COUNT = 6;
|
|
233
|
+
var SEAT_W = 160;
|
|
234
|
+
var SEAT_H = 176;
|
|
235
|
+
var DESIGN_W = 1200;
|
|
236
|
+
var DESIGN_H = 900;
|
|
237
|
+
var MARGIN_X = 164;
|
|
238
|
+
var MARGIN_Y = 196;
|
|
239
|
+
var TABLE_RX = DESIGN_W / 2 - MARGIN_X;
|
|
240
|
+
var TABLE_RY = DESIGN_H / 2 - MARGIN_Y;
|
|
241
|
+
var FELT_GAP = 11;
|
|
242
|
+
var BET_SCALE = 0.72;
|
|
243
|
+
var BET_RX = TABLE_RX * BET_SCALE;
|
|
244
|
+
var BET_RY = TABLE_RY * BET_SCALE;
|
|
245
|
+
var SEAT_RING_X = TABLE_RX - FELT_GAP + SEAT_W / 2 + 6;
|
|
246
|
+
var SEAT_RING_Y = TABLE_RY - FELT_GAP + SEAT_H / 2 + 6;
|
|
247
|
+
var BASE_ANGLES = [-90, -30, 30, 90, 150, 210];
|
|
248
|
+
function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
|
|
249
|
+
const rad = angleDeg * Math.PI / 180;
|
|
250
|
+
return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
|
|
251
|
+
}
|
|
252
|
+
function calculateSeatLayouts(seats, yourSeatIndex) {
|
|
253
|
+
const cx = DESIGN_W / 2;
|
|
254
|
+
const cy = DESIGN_H / 2;
|
|
255
|
+
const rotationOffset = yourSeatIndex !== null ? BASE_ANGLES[3] - BASE_ANGLES[yourSeatIndex] : 0;
|
|
256
|
+
const layouts = [];
|
|
257
|
+
for (let i = 0; i < SEAT_COUNT; i++) {
|
|
258
|
+
const pt = getEllipsePoint(cx, cy, SEAT_RING_X, SEAT_RING_Y, BASE_ANGLES[i] + rotationOffset);
|
|
259
|
+
layouts.push({
|
|
260
|
+
x: pt.x - SEAT_W / 2,
|
|
261
|
+
y: pt.y - SEAT_H / 2,
|
|
262
|
+
logicalIndex: i,
|
|
263
|
+
isYou: i === yourSeatIndex,
|
|
264
|
+
seat: seats[i] ?? null
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
return layouts;
|
|
268
|
+
}
|
|
269
|
+
function PokerTable({
|
|
270
|
+
seats,
|
|
271
|
+
yourSeatIndex = null,
|
|
272
|
+
communityCards = [],
|
|
273
|
+
pot = 0,
|
|
274
|
+
pots = [],
|
|
275
|
+
isWaiting = false,
|
|
276
|
+
winnerSeats = [],
|
|
277
|
+
seatSize = "sm",
|
|
278
|
+
renderSeat,
|
|
279
|
+
className
|
|
280
|
+
}) {
|
|
281
|
+
const outerRef = (0, import_react.useRef)(null);
|
|
282
|
+
const [scale, setScale] = (0, import_react.useState)(1);
|
|
283
|
+
const layouts = (0, import_react.useMemo)(
|
|
284
|
+
() => calculateSeatLayouts(seats, yourSeatIndex),
|
|
285
|
+
[seats, yourSeatIndex]
|
|
286
|
+
);
|
|
287
|
+
(0, import_react.useLayoutEffect)(() => {
|
|
288
|
+
const el = outerRef.current;
|
|
289
|
+
if (!el) return;
|
|
290
|
+
const compute = () => {
|
|
291
|
+
const w = el.clientWidth;
|
|
292
|
+
const h = el.clientHeight;
|
|
293
|
+
if (!w || !h) return;
|
|
294
|
+
setScale(Math.min(w / DESIGN_W, h / DESIGN_H));
|
|
295
|
+
};
|
|
296
|
+
compute();
|
|
297
|
+
const ro = new ResizeObserver(compute);
|
|
298
|
+
ro.observe(el);
|
|
299
|
+
return () => ro.disconnect();
|
|
300
|
+
}, []);
|
|
301
|
+
const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
|
|
302
|
+
const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
|
|
303
|
+
const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
|
|
304
|
+
const betLineInset = {
|
|
305
|
+
inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
|
|
306
|
+
};
|
|
307
|
+
const potRows = pots.length > 0 ? pots : pot > 0 ? [{ label: "Pot", amount: pot }] : [];
|
|
308
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
309
|
+
"div",
|
|
310
|
+
{
|
|
311
|
+
ref: outerRef,
|
|
312
|
+
className: cn("relative flex h-full w-full items-center justify-center overflow-hidden min-h-0", className),
|
|
313
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
|
|
314
|
+
"div",
|
|
315
|
+
{
|
|
316
|
+
className: "relative",
|
|
317
|
+
style: { width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: "top left" },
|
|
318
|
+
children: [
|
|
319
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
320
|
+
"div",
|
|
321
|
+
{
|
|
322
|
+
className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl",
|
|
323
|
+
"aria-hidden": "true"
|
|
324
|
+
}
|
|
325
|
+
),
|
|
326
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
327
|
+
"div",
|
|
328
|
+
{
|
|
329
|
+
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)]",
|
|
330
|
+
style: tableFrameInset,
|
|
331
|
+
"aria-hidden": "true"
|
|
332
|
+
}
|
|
333
|
+
),
|
|
334
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
335
|
+
"div",
|
|
336
|
+
{
|
|
337
|
+
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)]",
|
|
338
|
+
style: feltInset,
|
|
339
|
+
"aria-hidden": "true"
|
|
340
|
+
}
|
|
341
|
+
),
|
|
342
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
|
|
343
|
+
"div",
|
|
344
|
+
{
|
|
345
|
+
className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
|
|
346
|
+
style: betLineInset,
|
|
347
|
+
"aria-hidden": "true"
|
|
348
|
+
}
|
|
349
|
+
),
|
|
350
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute flex items-center justify-center pointer-events-none", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none", children: "\u2660\u2665\u2666\u2663" }) }),
|
|
351
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "absolute inset-0 flex flex-col items-center justify-center pointer-events-none", children: [
|
|
352
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "mb-4 flex flex-col items-center gap-3", children: [
|
|
353
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(CommunityCards, { cards: communityCards, size: "lg", className: "filter drop-shadow-lg" }),
|
|
354
|
+
potRows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "flex flex-col items-center gap-1", children: potRows.map((p, i) => /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("div", { className: "flex items-center gap-2 font-mono text-sm text-amber-400", children: [
|
|
355
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("span", { className: "text-neutral-400", children: p.label }),
|
|
356
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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) })
|
|
357
|
+
] }, i)) })
|
|
358
|
+
] }),
|
|
359
|
+
isWaiting && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("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" })
|
|
360
|
+
] }),
|
|
361
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute inset-0 z-20 pointer-events-none", children: layouts.map(({ x, y, logicalIndex, isYou, seat }) => {
|
|
362
|
+
const merged = seat ? { ...seat, isYou, isWinner: winnerSeats.includes(logicalIndex) || seat.isWinner } : null;
|
|
363
|
+
return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute", style: { left: `${x}px`, top: `${y}px` }, children: renderSeat ? renderSeat(merged, logicalIndex) : /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Seat, { seat: merged, size: seatSize }) }, logicalIndex);
|
|
364
|
+
}) }),
|
|
365
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
|
|
366
|
+
]
|
|
367
|
+
}
|
|
368
|
+
) })
|
|
369
|
+
}
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
373
|
+
0 && (module.exports = {
|
|
374
|
+
BB,
|
|
375
|
+
CardBack,
|
|
376
|
+
CommunityCards,
|
|
377
|
+
HoleCards,
|
|
378
|
+
PlayingCard,
|
|
379
|
+
PokerTable,
|
|
380
|
+
Seat,
|
|
381
|
+
cn,
|
|
382
|
+
formatBB,
|
|
383
|
+
getSuitSymbol,
|
|
384
|
+
parseCard
|
|
385
|
+
});
|
|
386
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cn.ts","../src/format.ts","../src/components/PlayingCard.tsx","../src/components/Seat.tsx","../src/components/PokerTable.tsx"],"sourcesContent":["export { cn } from \"./cn\";\nexport { BB, formatBB } from \"./format\";\n\nexport {\n PlayingCard,\n CardBack,\n HoleCards,\n CommunityCards,\n parseCard,\n getSuitSymbol,\n type PlayingCardProps,\n type CardBackProps,\n type HoleCardsProps,\n type CommunityCardsProps,\n type CardSize,\n} from \"./components/PlayingCard\";\n\nexport { Seat, type SeatProps, type SeatView } from \"./components/Seat\";\n\nexport { PokerTable, type PokerTableProps, type PotRow } from \"./components/PokerTable\";\n","export function cn(...parts: Array<string | false | null | undefined>): string {\n return 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","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};\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 size?: CardSize;\n className?: string;\n}\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.\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 showCenter = size === \"md\" || size === \"lg\";\n\n return (\n <div\n className={cn(\n SIZE_CLASSES[size],\n \"relative bg-gradient-to-br from-white via-white to-neutral-100\",\n \"rounded-md border border-neutral-300/90 overflow-hidden\",\n \"shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]\",\n className,\n )}\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 {showCenter && (\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 )}\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}\n\n/** A row of a player's two hole cards. */\nexport function HoleCards({ cards, size = \"lg\", className }: HoleCardsProps) {\n return (\n <div className={cn(\"flex gap-2\", className)}>\n {cards.map((card, i) => (\n <PlayingCard key={i} card={card} size={size} />\n ))}\n </div>\n );\n}\n\nexport interface CommunityCardsProps {\n cards: (string | null)[];\n size?: CardSize;\n className?: string;\n}\n\n/** The five community cards, each dealt with a staggered flip animation. */\nexport function CommunityCards({ cards, size = \"md\", className }: CommunityCardsProps) {\n return (\n <div className={cn(\"flex gap-1.5\", className)}>\n {cards.map((card, i) => (\n <div key={i} className=\"animate-card-deal\" style={{ animationDelay: `${i * 90}ms` }}>\n <PlayingCard card={card} size={size} />\n </div>\n ))}\n </div>\n );\n}\n","import { cn } from \"../cn\";\nimport { formatBB } from \"../format\";\nimport { CardBack, HoleCards, type CardSize } from \"./PlayingCard\";\n\n/** Presentational view-model for one seat (no game logic). */\nexport interface SeatView {\n nickname: string;\n stack: number;\n holeCards?: (string | null)[] | null;\n isDealer?: boolean;\n isTurn?: boolean;\n folded?: boolean;\n busted?: boolean;\n allIn?: boolean;\n currentBet?: number;\n isYou?: boolean;\n isWinner?: boolean;\n /** Optional short label shown in a bubble (e.g. \"Raise 500\"). */\n actionLabel?: string | null;\n}\n\nexport interface SeatProps {\n seat: SeatView | null;\n size?: CardSize;\n className?: string;\n}\n\n/**\n * A presentational seat: nickname, stack, hole cards, dealer/turn/winner\n * markers. It is fully controlled — all state arrives through `seat`.\n */\nexport function Seat({ seat, size = \"sm\", className }: SeatProps) {\n if (!seat) {\n return (\n <div\n className={cn(\n \"w-40 h-44 rounded-2xl border-2 border-dashed border-white/10\",\n \"flex items-center justify-center text-white/20 text-xs\",\n className,\n )}\n >\n Empty seat\n </div>\n );\n }\n\n const showCards = !seat.folded && !seat.busted && !!seat.holeCards && seat.holeCards.length > 0;\n\n return (\n <div className={cn(\"relative w-40 h-44\", className)}>\n {seat.actionLabel && (\n <div className=\"animate-bubble-pop absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-neutral-900 shadow-lg\">\n {seat.actionLabel}\n </div>\n )}\n\n <div\n className={cn(\n \"flex h-full w-full flex-col items-center justify-between rounded-2xl border bg-neutral-900/80 p-2 backdrop-blur\",\n seat.isTurn ? \"border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]\" : \"border-neutral-700\",\n seat.isWinner && \"border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]\",\n seat.folded && \"opacity-50\",\n )}\n >\n <div className=\"flex w-full items-center justify-between text-[11px]\">\n <span className=\"truncate font-semibold text-neutral-100\">\n {seat.isWinner ? \"👑 \" : \"\"}\n {seat.nickname}\n {seat.isYou ? \" 👈\" : \"\"}\n </span>\n {seat.isDealer && (\n <span className=\"flex h-4 w-4 items-center justify-center rounded-full bg-white text-[9px] font-bold text-neutral-900\">\n D\n </span>\n )}\n </div>\n\n <div className=\"flex flex-1 items-center justify-center\">\n {seat.busted ? (\n <span className=\"text-xs font-semibold text-red-400\">Busted</span>\n ) : showCards ? (\n <HoleCards cards={seat.holeCards as (string | null)[]} size={size} />\n ) : (\n <div className=\"flex gap-2\">\n <CardBack size={size} />\n <CardBack size={size} />\n </div>\n )}\n </div>\n\n <div className=\"flex w-full items-center justify-between text-[11px] text-neutral-300\">\n <span className=\"font-mono\">{formatBB(seat.stack)}</span>\n {seat.allIn ? (\n <span className=\"rounded bg-red-600/80 px-1.5 text-[10px] font-bold text-white\">ALL-IN</span>\n ) : seat.currentBet && seat.currentBet > 0 ? (\n <span className=\"font-mono text-amber-300\">{formatBB(seat.currentBet)}</span>\n ) : null}\n </div>\n </div>\n </div>\n );\n}\n","import { useRef, useLayoutEffect, useState, useMemo } from \"react\";\nimport { cn } from \"../cn\";\nimport { formatBB } from \"../format\";\nimport { CommunityCards, type CardSize } from \"./PlayingCard\";\nimport { Seat, type SeatView } from \"./Seat\";\n\nexport interface PotRow {\n label: string;\n amount: number;\n}\n\nexport interface PokerTableProps {\n /** Six seats (some may be empty). */\n seats: (SeatView | null)[];\n /** Which seat belongs to the viewer; rotates that seat to the bottom. */\n yourSeatIndex?: number | null;\n communityCards?: (string | null)[];\n pot?: number;\n pots?: PotRow[];\n isWaiting?: boolean;\n winnerSeats?: number[];\n seatSize?: CardSize;\n /** Optional override for rendering a seat. */\n renderSeat?: (seat: SeatView | null, index: number) => React.ReactNode;\n className?: string;\n}\n\nconst SEAT_COUNT = 6;\nconst SEAT_W = 160;\nconst SEAT_H = 176;\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 SEAT_RING_X = TABLE_RX - FELT_GAP + SEAT_W / 2 + 6;\nconst SEAT_RING_Y = TABLE_RY - FELT_GAP + SEAT_H / 2 + 6;\n\nconst BASE_ANGLES = [-90, -30, 30, 90, 150, 210];\n\ninterface SeatLayout {\n x: number;\n y: number;\n logicalIndex: number;\n isYou: boolean;\n seat: SeatView | null;\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 seats: (SeatView | null)[],\n yourSeatIndex: number | null,\n): SeatLayout[] {\n const cx = DESIGN_W / 2;\n const cy = DESIGN_H / 2;\n const rotationOffset =\n yourSeatIndex !== null ? BASE_ANGLES[3] - BASE_ANGLES[yourSeatIndex] : 0;\n\n const layouts: SeatLayout[] = [];\n for (let i = 0; i < SEAT_COUNT; i++) {\n const pt = getEllipsePoint(cx, cy, SEAT_RING_X, SEAT_RING_Y, BASE_ANGLES[i] + rotationOffset);\n layouts.push({\n x: pt.x - SEAT_W / 2,\n y: pt.y - SEAT_H / 2,\n logicalIndex: i,\n isYou: i === yourSeatIndex,\n seat: seats[i] ?? null,\n });\n }\n return layouts;\n}\n\n/**\n * A responsive, themable poker table. It scales a fixed 1200x900 design onto\n * its container and arranges six seats around the felt. Purely presentational:\n * pass seat view-models and card codes in.\n */\nexport function PokerTable({\n seats,\n yourSeatIndex = null,\n communityCards = [],\n pot = 0,\n pots = [],\n isWaiting = false,\n winnerSeats = [],\n seatSize = \"sm\",\n renderSeat,\n className,\n}: PokerTableProps) {\n const outerRef = useRef<HTMLDivElement>(null);\n const [scale, setScale] = useState(1);\n\n const layouts = useMemo(\n () => calculateSeatLayouts(seats, yourSeatIndex),\n [seats, yourSeatIndex],\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 betLineInset = {\n inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`,\n };\n\n const potRows: PotRow[] = pots.length > 0 ? pots : pot > 0 ? [{ label: \"Pot\", amount: pot }] : [];\n\n return (\n <div\n ref={outerRef}\n className={cn(\"relative flex h-full w-full items-center justify-center overflow-hidden min-h-0\", 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 {/* ambient spotlight */}\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 {/* 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 {/* center: community cards + pots */}\n <div className=\"absolute inset-0 flex flex-col items-center justify-center pointer-events-none\">\n <div className=\"mb-4 flex flex-col items-center gap-3\">\n <CommunityCards cards={communityCards} size=\"lg\" className=\"filter drop-shadow-lg\" />\n {potRows.length > 0 && (\n <div className=\"flex flex-col items-center gap-1\">\n {potRows.map((p, i) => (\n <div key={i} className=\"flex items-center gap-2 font-mono text-sm text-amber-400\">\n <span className=\"text-neutral-400\">{p.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 {formatBB(p.amount)}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n {isWaiting && (\n <div className=\"rounded-full border border-neutral-700 bg-neutral-900/70 px-3 py-1 text-xs text-neutral-400\">\n Waiting to start\n </div>\n )}\n </div>\n\n {/* seats */}\n <div className=\"absolute inset-0 z-20 pointer-events-none\">\n {layouts.map(({ x, y, logicalIndex, isYou, seat }) => {\n const merged: SeatView | null = seat\n ? { ...seat, isYou, isWinner: winnerSeats.includes(logicalIndex) || seat.isWinner }\n : null;\n return (\n <div key={logicalIndex} className=\"absolute\" style={{ left: `${x}px`, top: `${y}px` }}>\n {renderSeat ? renderSeat(merged, logicalIndex) : <Seat seat={merged} size={seatSize} />}\n </div>\n );\n })}\n </div>\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":["import_jsx_runtime","import_jsx_runtime"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
declare function cn(...parts: Array<string | false | null | undefined>): string;
|
|
4
|
+
|
|
5
|
+
/** Big Blind value used for stack display. */
|
|
6
|
+
declare const BB = 20;
|
|
7
|
+
/** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
|
|
8
|
+
declare function formatBB(amount: number): string;
|
|
9
|
+
|
|
10
|
+
type CardSize = "xs" | "sm" | "md" | "lg";
|
|
11
|
+
/** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
|
|
12
|
+
declare function parseCard(card: string): {
|
|
13
|
+
rank: string;
|
|
14
|
+
suit: string;
|
|
15
|
+
};
|
|
16
|
+
declare function getSuitSymbol(suit: string): string;
|
|
17
|
+
interface PlayingCardProps {
|
|
18
|
+
/** Compact card code ("As", "10h") or null to render a face-down card back. */
|
|
19
|
+
card: string | null;
|
|
20
|
+
size?: CardSize;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A single playing card. Size is driven by the `size` prop (never viewport
|
|
25
|
+
* breakpoints) so cards scale predictably inside any container.
|
|
26
|
+
*/
|
|
27
|
+
declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
|
|
28
|
+
interface CardBackProps {
|
|
29
|
+
size?: CardSize;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The face-down card back (dark red + gold rosette). */
|
|
33
|
+
declare function CardBack({ size, className }: CardBackProps): react.JSX.Element;
|
|
34
|
+
interface HoleCardsProps {
|
|
35
|
+
cards: (string | null)[];
|
|
36
|
+
size?: CardSize;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
/** A row of a player's two hole cards. */
|
|
40
|
+
declare function HoleCards({ cards, size, className }: HoleCardsProps): react.JSX.Element;
|
|
41
|
+
interface CommunityCardsProps {
|
|
42
|
+
cards: (string | null)[];
|
|
43
|
+
size?: CardSize;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
/** The five community cards, each dealt with a staggered flip animation. */
|
|
47
|
+
declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
|
|
48
|
+
|
|
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
|
+
interface SeatProps {
|
|
66
|
+
seat: SeatView | null;
|
|
67
|
+
size?: CardSize;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A presentational seat: nickname, stack, hole cards, dealer/turn/winner
|
|
72
|
+
* markers. It is fully controlled — all state arrives through `seat`.
|
|
73
|
+
*/
|
|
74
|
+
declare function Seat({ seat, size, className }: SeatProps): react.JSX.Element;
|
|
75
|
+
|
|
76
|
+
interface PotRow {
|
|
77
|
+
label: string;
|
|
78
|
+
amount: number;
|
|
79
|
+
}
|
|
80
|
+
interface PokerTableProps {
|
|
81
|
+
/** Six seats (some may be empty). */
|
|
82
|
+
seats: (SeatView | null)[];
|
|
83
|
+
/** Which seat belongs to the viewer; rotates that seat to the bottom. */
|
|
84
|
+
yourSeatIndex?: number | null;
|
|
85
|
+
communityCards?: (string | null)[];
|
|
86
|
+
pot?: number;
|
|
87
|
+
pots?: PotRow[];
|
|
88
|
+
isWaiting?: boolean;
|
|
89
|
+
winnerSeats?: number[];
|
|
90
|
+
seatSize?: CardSize;
|
|
91
|
+
/** Optional override for rendering a seat. */
|
|
92
|
+
renderSeat?: (seat: SeatView | null, index: number) => React.ReactNode;
|
|
93
|
+
className?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A responsive, themable poker table. It scales a fixed 1200x900 design onto
|
|
97
|
+
* its container and arranges six seats around the felt. Purely presentational:
|
|
98
|
+
* pass seat view-models and card codes in.
|
|
99
|
+
*/
|
|
100
|
+
declare function PokerTable({ seats, yourSeatIndex, communityCards, pot, pots, isWaiting, winnerSeats, seatSize, renderSeat, className, }: PokerTableProps): react.JSX.Element;
|
|
101
|
+
|
|
102
|
+
export { BB, CardBack, type CardBackProps, type CardSize, CommunityCards, type CommunityCardsProps, HoleCards, type HoleCardsProps, PlayingCard, type PlayingCardProps, PokerTable, type PokerTableProps, type PotRow, Seat, type SeatProps, type SeatView, cn, formatBB, getSuitSymbol, parseCard };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
|
|
3
|
+
declare function cn(...parts: Array<string | false | null | undefined>): string;
|
|
4
|
+
|
|
5
|
+
/** Big Blind value used for stack display. */
|
|
6
|
+
declare const BB = 20;
|
|
7
|
+
/** Format a chip amount as a Big-Blind string, e.g. 1234 -> "61.7 BB". */
|
|
8
|
+
declare function formatBB(amount: number): string;
|
|
9
|
+
|
|
10
|
+
type CardSize = "xs" | "sm" | "md" | "lg";
|
|
11
|
+
/** Parse a compact card code like "As", "10h", "kd" into rank + suit. */
|
|
12
|
+
declare function parseCard(card: string): {
|
|
13
|
+
rank: string;
|
|
14
|
+
suit: string;
|
|
15
|
+
};
|
|
16
|
+
declare function getSuitSymbol(suit: string): string;
|
|
17
|
+
interface PlayingCardProps {
|
|
18
|
+
/** Compact card code ("As", "10h") or null to render a face-down card back. */
|
|
19
|
+
card: string | null;
|
|
20
|
+
size?: CardSize;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A single playing card. Size is driven by the `size` prop (never viewport
|
|
25
|
+
* breakpoints) so cards scale predictably inside any container.
|
|
26
|
+
*/
|
|
27
|
+
declare function PlayingCard({ card, size, className }: PlayingCardProps): react.JSX.Element;
|
|
28
|
+
interface CardBackProps {
|
|
29
|
+
size?: CardSize;
|
|
30
|
+
className?: string;
|
|
31
|
+
}
|
|
32
|
+
/** The face-down card back (dark red + gold rosette). */
|
|
33
|
+
declare function CardBack({ size, className }: CardBackProps): react.JSX.Element;
|
|
34
|
+
interface HoleCardsProps {
|
|
35
|
+
cards: (string | null)[];
|
|
36
|
+
size?: CardSize;
|
|
37
|
+
className?: string;
|
|
38
|
+
}
|
|
39
|
+
/** A row of a player's two hole cards. */
|
|
40
|
+
declare function HoleCards({ cards, size, className }: HoleCardsProps): react.JSX.Element;
|
|
41
|
+
interface CommunityCardsProps {
|
|
42
|
+
cards: (string | null)[];
|
|
43
|
+
size?: CardSize;
|
|
44
|
+
className?: string;
|
|
45
|
+
}
|
|
46
|
+
/** The five community cards, each dealt with a staggered flip animation. */
|
|
47
|
+
declare function CommunityCards({ cards, size, className }: CommunityCardsProps): react.JSX.Element;
|
|
48
|
+
|
|
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
|
+
interface SeatProps {
|
|
66
|
+
seat: SeatView | null;
|
|
67
|
+
size?: CardSize;
|
|
68
|
+
className?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A presentational seat: nickname, stack, hole cards, dealer/turn/winner
|
|
72
|
+
* markers. It is fully controlled — all state arrives through `seat`.
|
|
73
|
+
*/
|
|
74
|
+
declare function Seat({ seat, size, className }: SeatProps): react.JSX.Element;
|
|
75
|
+
|
|
76
|
+
interface PotRow {
|
|
77
|
+
label: string;
|
|
78
|
+
amount: number;
|
|
79
|
+
}
|
|
80
|
+
interface PokerTableProps {
|
|
81
|
+
/** Six seats (some may be empty). */
|
|
82
|
+
seats: (SeatView | null)[];
|
|
83
|
+
/** Which seat belongs to the viewer; rotates that seat to the bottom. */
|
|
84
|
+
yourSeatIndex?: number | null;
|
|
85
|
+
communityCards?: (string | null)[];
|
|
86
|
+
pot?: number;
|
|
87
|
+
pots?: PotRow[];
|
|
88
|
+
isWaiting?: boolean;
|
|
89
|
+
winnerSeats?: number[];
|
|
90
|
+
seatSize?: CardSize;
|
|
91
|
+
/** Optional override for rendering a seat. */
|
|
92
|
+
renderSeat?: (seat: SeatView | null, index: number) => React.ReactNode;
|
|
93
|
+
className?: string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A responsive, themable poker table. It scales a fixed 1200x900 design onto
|
|
97
|
+
* its container and arranges six seats around the felt. Purely presentational:
|
|
98
|
+
* pass seat view-models and card codes in.
|
|
99
|
+
*/
|
|
100
|
+
declare function PokerTable({ seats, yourSeatIndex, communityCards, pot, pots, isWaiting, winnerSeats, seatSize, renderSeat, className, }: PokerTableProps): react.JSX.Element;
|
|
101
|
+
|
|
102
|
+
export { BB, CardBack, type CardBackProps, type CardSize, CommunityCards, type CommunityCardsProps, HoleCards, type HoleCardsProps, PlayingCard, type PlayingCardProps, PokerTable, type PokerTableProps, type PotRow, Seat, type SeatProps, type SeatView, cn, formatBB, getSuitSymbol, parseCard };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/cn.ts
|
|
4
|
+
function cn(...parts) {
|
|
5
|
+
return parts.filter(Boolean).join(" ");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// src/format.ts
|
|
9
|
+
var BB = 20;
|
|
10
|
+
function formatBB(amount) {
|
|
11
|
+
const bb = amount / BB;
|
|
12
|
+
return `${Number.isInteger(bb) ? bb : bb.toFixed(1)} BB`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// src/components/PlayingCard.tsx
|
|
16
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
17
|
+
var SIZE_CLASSES = {
|
|
18
|
+
xs: "w-8 h-11",
|
|
19
|
+
sm: "w-10 h-14",
|
|
20
|
+
md: "w-16 h-22",
|
|
21
|
+
lg: "w-24 h-32"
|
|
22
|
+
};
|
|
23
|
+
var CORNER_RANK = {
|
|
24
|
+
xs: "text-[11px]",
|
|
25
|
+
sm: "text-sm",
|
|
26
|
+
md: "text-lg",
|
|
27
|
+
lg: "text-2xl"
|
|
28
|
+
};
|
|
29
|
+
var CORNER_SUIT = {
|
|
30
|
+
xs: "text-[9px]",
|
|
31
|
+
sm: "text-xs",
|
|
32
|
+
md: "text-sm",
|
|
33
|
+
lg: "text-base"
|
|
34
|
+
};
|
|
35
|
+
var CENTER_SUIT = {
|
|
36
|
+
xs: "text-base",
|
|
37
|
+
sm: "text-xl",
|
|
38
|
+
md: "text-3xl",
|
|
39
|
+
lg: "text-5xl"
|
|
40
|
+
};
|
|
41
|
+
var BACK_EMBLEM_SIZE = {
|
|
42
|
+
xs: 14,
|
|
43
|
+
sm: 18,
|
|
44
|
+
md: 28,
|
|
45
|
+
lg: 42
|
|
46
|
+
};
|
|
47
|
+
function parseCard(card) {
|
|
48
|
+
if (!card || card.length < 2) return { rank: "", suit: "" };
|
|
49
|
+
return { rank: card.slice(0, -1), suit: card.slice(-1) };
|
|
50
|
+
}
|
|
51
|
+
function getSuitSymbol(suit) {
|
|
52
|
+
switch (suit) {
|
|
53
|
+
case "s":
|
|
54
|
+
return "\u2660";
|
|
55
|
+
case "h":
|
|
56
|
+
return "\u2665";
|
|
57
|
+
case "d":
|
|
58
|
+
return "\u2666";
|
|
59
|
+
case "c":
|
|
60
|
+
return "\u2663";
|
|
61
|
+
default:
|
|
62
|
+
return suit;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function getSuitClass(suit) {
|
|
66
|
+
switch (suit) {
|
|
67
|
+
case "s":
|
|
68
|
+
case "c":
|
|
69
|
+
return "text-neutral-900";
|
|
70
|
+
case "h":
|
|
71
|
+
case "d":
|
|
72
|
+
return "text-red-600";
|
|
73
|
+
default:
|
|
74
|
+
return "";
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function PlayingCard({ card, size = "md", className }) {
|
|
78
|
+
if (!card) return /* @__PURE__ */ jsx(CardBack, { size, className });
|
|
79
|
+
const { rank, suit } = parseCard(card);
|
|
80
|
+
const suitSymbol = getSuitSymbol(suit);
|
|
81
|
+
const suitClass = getSuitClass(suit);
|
|
82
|
+
const showCenter = size === "md" || size === "lg";
|
|
83
|
+
return /* @__PURE__ */ jsxs(
|
|
84
|
+
"div",
|
|
85
|
+
{
|
|
86
|
+
className: cn(
|
|
87
|
+
SIZE_CLASSES[size],
|
|
88
|
+
"relative bg-gradient-to-br from-white via-white to-neutral-100",
|
|
89
|
+
"rounded-md border border-neutral-300/90 overflow-hidden",
|
|
90
|
+
"shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]",
|
|
91
|
+
className
|
|
92
|
+
),
|
|
93
|
+
style: { aspectRatio: "10/14" },
|
|
94
|
+
children: [
|
|
95
|
+
/* @__PURE__ */ jsxs("div", { className: cn("absolute top-[3px] left-[5px] flex flex-col items-center leading-none", suitClass), children: [
|
|
96
|
+
/* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
|
|
97
|
+
/* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
|
|
98
|
+
] }),
|
|
99
|
+
/* @__PURE__ */ jsxs("div", { className: cn("absolute bottom-[3px] right-[5px] flex flex-col items-center leading-none rotate-180", suitClass), children: [
|
|
100
|
+
/* @__PURE__ */ jsx("span", { className: cn(CORNER_RANK[size], "font-bold tracking-tight"), children: rank }),
|
|
101
|
+
/* @__PURE__ */ jsx("span", { className: cn(CORNER_SUIT[size], "-mt-[1px]"), children: suitSymbol })
|
|
102
|
+
] }),
|
|
103
|
+
showCenter && /* @__PURE__ */ jsx("div", { className: cn("absolute inset-0 flex items-center justify-center opacity-90", suitClass), children: /* @__PURE__ */ jsx("span", { className: CENTER_SUIT[size], children: suitSymbol }) })
|
|
104
|
+
]
|
|
105
|
+
}
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
function CardBackEmblem({ size }) {
|
|
109
|
+
const s = BACK_EMBLEM_SIZE[size];
|
|
110
|
+
return /* @__PURE__ */ jsxs("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", children: [
|
|
111
|
+
/* @__PURE__ */ jsxs("g", { fill: "#f0c04a", opacity: "0.92", children: [
|
|
112
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "11", rx: "4", ry: "7" }),
|
|
113
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "20", cy: "29", rx: "4", ry: "7" }),
|
|
114
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "11", cy: "20", rx: "7", ry: "4" }),
|
|
115
|
+
/* @__PURE__ */ jsx("ellipse", { cx: "29", cy: "20", rx: "7", ry: "4" })
|
|
116
|
+
] }),
|
|
117
|
+
/* @__PURE__ */ jsx("circle", { cx: "20", cy: "20", r: "3.2", fill: "#5a0c18" })
|
|
118
|
+
] });
|
|
119
|
+
}
|
|
120
|
+
function CardBack({ size = "md", className }) {
|
|
121
|
+
return /* @__PURE__ */ jsx(
|
|
122
|
+
"div",
|
|
123
|
+
{
|
|
124
|
+
className: cn(
|
|
125
|
+
SIZE_CLASSES[size],
|
|
126
|
+
"relative rounded-md p-[3px] overflow-hidden",
|
|
127
|
+
"bg-gradient-to-br from-[#7a1020] via-[#5a0c18] to-[#3f0810]",
|
|
128
|
+
"border-2 border-[#c9a24b]/70 shadow-[0_2px_6px_rgba(0,0,0,0.45)]",
|
|
129
|
+
className
|
|
130
|
+
),
|
|
131
|
+
style: { aspectRatio: "10/14" },
|
|
132
|
+
children: /* @__PURE__ */ jsx("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)]", children: /* @__PURE__ */ jsx(CardBackEmblem, { size }) })
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
function HoleCards({ cards, size = "lg", className }) {
|
|
137
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex gap-2", className), children: cards.map((card, i) => /* @__PURE__ */ jsx(PlayingCard, { card, size }, i)) });
|
|
138
|
+
}
|
|
139
|
+
function CommunityCards({ cards, size = "md", className }) {
|
|
140
|
+
return /* @__PURE__ */ jsx("div", { className: cn("flex gap-1.5", className), children: cards.map((card, i) => /* @__PURE__ */ jsx("div", { className: "animate-card-deal", style: { animationDelay: `${i * 90}ms` }, children: /* @__PURE__ */ jsx(PlayingCard, { card, size }) }, i)) });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// src/components/Seat.tsx
|
|
144
|
+
import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
145
|
+
function Seat({ seat, size = "sm", className }) {
|
|
146
|
+
if (!seat) {
|
|
147
|
+
return /* @__PURE__ */ jsx2(
|
|
148
|
+
"div",
|
|
149
|
+
{
|
|
150
|
+
className: cn(
|
|
151
|
+
"w-40 h-44 rounded-2xl border-2 border-dashed border-white/10",
|
|
152
|
+
"flex items-center justify-center text-white/20 text-xs",
|
|
153
|
+
className
|
|
154
|
+
),
|
|
155
|
+
children: "Empty seat"
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
const showCards = !seat.folded && !seat.busted && !!seat.holeCards && seat.holeCards.length > 0;
|
|
160
|
+
return /* @__PURE__ */ jsxs2("div", { className: cn("relative w-40 h-44", className), children: [
|
|
161
|
+
seat.actionLabel && /* @__PURE__ */ jsx2("div", { className: "animate-bubble-pop absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-neutral-900 shadow-lg", children: seat.actionLabel }),
|
|
162
|
+
/* @__PURE__ */ jsxs2(
|
|
163
|
+
"div",
|
|
164
|
+
{
|
|
165
|
+
className: cn(
|
|
166
|
+
"flex h-full w-full flex-col items-center justify-between rounded-2xl border bg-neutral-900/80 p-2 backdrop-blur",
|
|
167
|
+
seat.isTurn ? "border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]" : "border-neutral-700",
|
|
168
|
+
seat.isWinner && "border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]",
|
|
169
|
+
seat.folded && "opacity-50"
|
|
170
|
+
),
|
|
171
|
+
children: [
|
|
172
|
+
/* @__PURE__ */ jsxs2("div", { className: "flex w-full items-center justify-between text-[11px]", children: [
|
|
173
|
+
/* @__PURE__ */ jsxs2("span", { className: "truncate font-semibold text-neutral-100", children: [
|
|
174
|
+
seat.isWinner ? "\u{1F451} " : "",
|
|
175
|
+
seat.nickname,
|
|
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
|
+
] })
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
)
|
|
191
|
+
] });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// src/components/PokerTable.tsx
|
|
195
|
+
import { useRef, useLayoutEffect, useState, useMemo } from "react";
|
|
196
|
+
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
197
|
+
var SEAT_COUNT = 6;
|
|
198
|
+
var SEAT_W = 160;
|
|
199
|
+
var SEAT_H = 176;
|
|
200
|
+
var DESIGN_W = 1200;
|
|
201
|
+
var DESIGN_H = 900;
|
|
202
|
+
var MARGIN_X = 164;
|
|
203
|
+
var MARGIN_Y = 196;
|
|
204
|
+
var TABLE_RX = DESIGN_W / 2 - MARGIN_X;
|
|
205
|
+
var TABLE_RY = DESIGN_H / 2 - MARGIN_Y;
|
|
206
|
+
var FELT_GAP = 11;
|
|
207
|
+
var BET_SCALE = 0.72;
|
|
208
|
+
var BET_RX = TABLE_RX * BET_SCALE;
|
|
209
|
+
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
|
+
function getEllipsePoint(cx, cy, rx, ry, angleDeg) {
|
|
214
|
+
const rad = angleDeg * Math.PI / 180;
|
|
215
|
+
return { x: cx + rx * Math.cos(rad), y: cy + ry * Math.sin(rad) };
|
|
216
|
+
}
|
|
217
|
+
function calculateSeatLayouts(seats, yourSeatIndex) {
|
|
218
|
+
const cx = DESIGN_W / 2;
|
|
219
|
+
const cy = DESIGN_H / 2;
|
|
220
|
+
const rotationOffset = yourSeatIndex !== null ? BASE_ANGLES[3] - BASE_ANGLES[yourSeatIndex] : 0;
|
|
221
|
+
const layouts = [];
|
|
222
|
+
for (let i = 0; i < SEAT_COUNT; i++) {
|
|
223
|
+
const pt = getEllipsePoint(cx, cy, SEAT_RING_X, SEAT_RING_Y, BASE_ANGLES[i] + rotationOffset);
|
|
224
|
+
layouts.push({
|
|
225
|
+
x: pt.x - SEAT_W / 2,
|
|
226
|
+
y: pt.y - SEAT_H / 2,
|
|
227
|
+
logicalIndex: i,
|
|
228
|
+
isYou: i === yourSeatIndex,
|
|
229
|
+
seat: seats[i] ?? null
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
return layouts;
|
|
233
|
+
}
|
|
234
|
+
function PokerTable({
|
|
235
|
+
seats,
|
|
236
|
+
yourSeatIndex = null,
|
|
237
|
+
communityCards = [],
|
|
238
|
+
pot = 0,
|
|
239
|
+
pots = [],
|
|
240
|
+
isWaiting = false,
|
|
241
|
+
winnerSeats = [],
|
|
242
|
+
seatSize = "sm",
|
|
243
|
+
renderSeat,
|
|
244
|
+
className
|
|
245
|
+
}) {
|
|
246
|
+
const outerRef = useRef(null);
|
|
247
|
+
const [scale, setScale] = useState(1);
|
|
248
|
+
const layouts = useMemo(
|
|
249
|
+
() => calculateSeatLayouts(seats, yourSeatIndex),
|
|
250
|
+
[seats, yourSeatIndex]
|
|
251
|
+
);
|
|
252
|
+
useLayoutEffect(() => {
|
|
253
|
+
const el = outerRef.current;
|
|
254
|
+
if (!el) return;
|
|
255
|
+
const compute = () => {
|
|
256
|
+
const w = el.clientWidth;
|
|
257
|
+
const h = el.clientHeight;
|
|
258
|
+
if (!w || !h) return;
|
|
259
|
+
setScale(Math.min(w / DESIGN_W, h / DESIGN_H));
|
|
260
|
+
};
|
|
261
|
+
compute();
|
|
262
|
+
const ro = new ResizeObserver(compute);
|
|
263
|
+
ro.observe(el);
|
|
264
|
+
return () => ro.disconnect();
|
|
265
|
+
}, []);
|
|
266
|
+
const tableFrameInset = { inset: `${MARGIN_Y}px ${MARGIN_X}px` };
|
|
267
|
+
const feltInset = { inset: `${MARGIN_Y + FELT_GAP}px ${MARGIN_X + FELT_GAP}px` };
|
|
268
|
+
const rimInset = { inset: `${MARGIN_Y + 6}px ${MARGIN_X + 6}px` };
|
|
269
|
+
const betLineInset = {
|
|
270
|
+
inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`
|
|
271
|
+
};
|
|
272
|
+
const potRows = pots.length > 0 ? pots : pot > 0 ? [{ label: "Pot", amount: pot }] : [];
|
|
273
|
+
return /* @__PURE__ */ jsx3(
|
|
274
|
+
"div",
|
|
275
|
+
{
|
|
276
|
+
ref: outerRef,
|
|
277
|
+
className: cn("relative flex h-full w-full items-center justify-center overflow-hidden min-h-0", className),
|
|
278
|
+
children: /* @__PURE__ */ jsx3("div", { style: { width: DESIGN_W * scale, height: DESIGN_H * scale }, children: /* @__PURE__ */ jsxs3(
|
|
279
|
+
"div",
|
|
280
|
+
{
|
|
281
|
+
className: "relative",
|
|
282
|
+
style: { width: DESIGN_W, height: DESIGN_H, transform: `scale(${scale})`, transformOrigin: "top left" },
|
|
283
|
+
children: [
|
|
284
|
+
/* @__PURE__ */ jsx3(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
className: "absolute -inset-6 rounded-[50%] bg-[radial-gradient(closest-side,rgba(240,192,74,0.16),transparent_72%)] blur-3xl",
|
|
288
|
+
"aria-hidden": "true"
|
|
289
|
+
}
|
|
290
|
+
),
|
|
291
|
+
/* @__PURE__ */ jsx3(
|
|
292
|
+
"div",
|
|
293
|
+
{
|
|
294
|
+
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)]",
|
|
295
|
+
style: tableFrameInset,
|
|
296
|
+
"aria-hidden": "true"
|
|
297
|
+
}
|
|
298
|
+
),
|
|
299
|
+
/* @__PURE__ */ jsx3(
|
|
300
|
+
"div",
|
|
301
|
+
{
|
|
302
|
+
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)]",
|
|
303
|
+
style: feltInset,
|
|
304
|
+
"aria-hidden": "true"
|
|
305
|
+
}
|
|
306
|
+
),
|
|
307
|
+
/* @__PURE__ */ jsx3(
|
|
308
|
+
"div",
|
|
309
|
+
{
|
|
310
|
+
className: "absolute rounded-[50%] border-2 border-white/10 shadow-[inset_0_0_0_1px_rgba(0,0,0,0.25)]",
|
|
311
|
+
style: betLineInset,
|
|
312
|
+
"aria-hidden": "true"
|
|
313
|
+
}
|
|
314
|
+
),
|
|
315
|
+
/* @__PURE__ */ jsx3("div", { className: "absolute flex items-center justify-center pointer-events-none", style: feltInset, "aria-hidden": "true", children: /* @__PURE__ */ jsx3("span", { className: "text-white/[0.05] text-[110px] font-black tracking-[0.2em] select-none leading-none", children: "\u2660\u2665\u2666\u2663" }) }),
|
|
316
|
+
/* @__PURE__ */ jsxs3("div", { className: "absolute inset-0 flex flex-col items-center justify-center pointer-events-none", children: [
|
|
317
|
+
/* @__PURE__ */ jsxs3("div", { className: "mb-4 flex flex-col items-center gap-3", children: [
|
|
318
|
+
/* @__PURE__ */ jsx3(CommunityCards, { cards: communityCards, size: "lg", className: "filter drop-shadow-lg" }),
|
|
319
|
+
potRows.length > 0 && /* @__PURE__ */ jsx3("div", { className: "flex flex-col items-center gap-1", children: potRows.map((p, i) => /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 font-mono text-sm text-amber-400", children: [
|
|
320
|
+
/* @__PURE__ */ jsx3("span", { className: "text-neutral-400", children: p.label }),
|
|
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);
|
|
329
|
+
}) }),
|
|
330
|
+
/* @__PURE__ */ jsx3("div", { className: "absolute rounded-[50%] border border-white/10 pointer-events-none", style: rimInset, "aria-hidden": "true" })
|
|
331
|
+
]
|
|
332
|
+
}
|
|
333
|
+
) })
|
|
334
|
+
}
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
export {
|
|
338
|
+
BB,
|
|
339
|
+
CardBack,
|
|
340
|
+
CommunityCards,
|
|
341
|
+
HoleCards,
|
|
342
|
+
PlayingCard,
|
|
343
|
+
PokerTable,
|
|
344
|
+
Seat,
|
|
345
|
+
cn,
|
|
346
|
+
formatBB,
|
|
347
|
+
getSuitSymbol,
|
|
348
|
+
parseCard
|
|
349
|
+
};
|
|
350
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cn.ts","../src/format.ts","../src/components/PlayingCard.tsx","../src/components/Seat.tsx","../src/components/PokerTable.tsx"],"sourcesContent":["export function cn(...parts: Array<string | false | null | undefined>): string {\n return 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","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};\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 size?: CardSize;\n className?: string;\n}\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.\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 showCenter = size === \"md\" || size === \"lg\";\n\n return (\n <div\n className={cn(\n SIZE_CLASSES[size],\n \"relative bg-gradient-to-br from-white via-white to-neutral-100\",\n \"rounded-md border border-neutral-300/90 overflow-hidden\",\n \"shadow-[0_2px_6px_rgba(0,0,0,0.45),inset_0_1px_0_rgba(255,255,255,0.9)]\",\n className,\n )}\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 {showCenter && (\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 )}\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}\n\n/** A row of a player's two hole cards. */\nexport function HoleCards({ cards, size = \"lg\", className }: HoleCardsProps) {\n return (\n <div className={cn(\"flex gap-2\", className)}>\n {cards.map((card, i) => (\n <PlayingCard key={i} card={card} size={size} />\n ))}\n </div>\n );\n}\n\nexport interface CommunityCardsProps {\n cards: (string | null)[];\n size?: CardSize;\n className?: string;\n}\n\n/** The five community cards, each dealt with a staggered flip animation. */\nexport function CommunityCards({ cards, size = \"md\", className }: CommunityCardsProps) {\n return (\n <div className={cn(\"flex gap-1.5\", className)}>\n {cards.map((card, i) => (\n <div key={i} className=\"animate-card-deal\" style={{ animationDelay: `${i * 90}ms` }}>\n <PlayingCard card={card} size={size} />\n </div>\n ))}\n </div>\n );\n}\n","import { cn } from \"../cn\";\nimport { formatBB } from \"../format\";\nimport { CardBack, HoleCards, type CardSize } from \"./PlayingCard\";\n\n/** Presentational view-model for one seat (no game logic). */\nexport interface SeatView {\n nickname: string;\n stack: number;\n holeCards?: (string | null)[] | null;\n isDealer?: boolean;\n isTurn?: boolean;\n folded?: boolean;\n busted?: boolean;\n allIn?: boolean;\n currentBet?: number;\n isYou?: boolean;\n isWinner?: boolean;\n /** Optional short label shown in a bubble (e.g. \"Raise 500\"). */\n actionLabel?: string | null;\n}\n\nexport interface SeatProps {\n seat: SeatView | null;\n size?: CardSize;\n className?: string;\n}\n\n/**\n * A presentational seat: nickname, stack, hole cards, dealer/turn/winner\n * markers. It is fully controlled — all state arrives through `seat`.\n */\nexport function Seat({ seat, size = \"sm\", className }: SeatProps) {\n if (!seat) {\n return (\n <div\n className={cn(\n \"w-40 h-44 rounded-2xl border-2 border-dashed border-white/10\",\n \"flex items-center justify-center text-white/20 text-xs\",\n className,\n )}\n >\n Empty seat\n </div>\n );\n }\n\n const showCards = !seat.folded && !seat.busted && !!seat.holeCards && seat.holeCards.length > 0;\n\n return (\n <div className={cn(\"relative w-40 h-44\", className)}>\n {seat.actionLabel && (\n <div className=\"animate-bubble-pop absolute -top-6 left-1/2 -translate-x-1/2 whitespace-nowrap rounded-full bg-white px-2.5 py-1 text-[11px] font-semibold text-neutral-900 shadow-lg\">\n {seat.actionLabel}\n </div>\n )}\n\n <div\n className={cn(\n \"flex h-full w-full flex-col items-center justify-between rounded-2xl border bg-neutral-900/80 p-2 backdrop-blur\",\n seat.isTurn ? \"border-cyan-400 shadow-[0_0_18px_rgba(34,211,238,0.55)]\" : \"border-neutral-700\",\n seat.isWinner && \"border-amber-400 shadow-[0_0_18px_rgba(251,191,36,0.6)]\",\n seat.folded && \"opacity-50\",\n )}\n >\n <div className=\"flex w-full items-center justify-between text-[11px]\">\n <span className=\"truncate font-semibold text-neutral-100\">\n {seat.isWinner ? \"👑 \" : \"\"}\n {seat.nickname}\n {seat.isYou ? \" 👈\" : \"\"}\n </span>\n {seat.isDealer && (\n <span className=\"flex h-4 w-4 items-center justify-center rounded-full bg-white text-[9px] font-bold text-neutral-900\">\n D\n </span>\n )}\n </div>\n\n <div className=\"flex flex-1 items-center justify-center\">\n {seat.busted ? (\n <span className=\"text-xs font-semibold text-red-400\">Busted</span>\n ) : showCards ? (\n <HoleCards cards={seat.holeCards as (string | null)[]} size={size} />\n ) : (\n <div className=\"flex gap-2\">\n <CardBack size={size} />\n <CardBack size={size} />\n </div>\n )}\n </div>\n\n <div className=\"flex w-full items-center justify-between text-[11px] text-neutral-300\">\n <span className=\"font-mono\">{formatBB(seat.stack)}</span>\n {seat.allIn ? (\n <span className=\"rounded bg-red-600/80 px-1.5 text-[10px] font-bold text-white\">ALL-IN</span>\n ) : seat.currentBet && seat.currentBet > 0 ? (\n <span className=\"font-mono text-amber-300\">{formatBB(seat.currentBet)}</span>\n ) : null}\n </div>\n </div>\n </div>\n );\n}\n","import { useRef, useLayoutEffect, useState, useMemo } from \"react\";\nimport { cn } from \"../cn\";\nimport { formatBB } from \"../format\";\nimport { CommunityCards, type CardSize } from \"./PlayingCard\";\nimport { Seat, type SeatView } from \"./Seat\";\n\nexport interface PotRow {\n label: string;\n amount: number;\n}\n\nexport interface PokerTableProps {\n /** Six seats (some may be empty). */\n seats: (SeatView | null)[];\n /** Which seat belongs to the viewer; rotates that seat to the bottom. */\n yourSeatIndex?: number | null;\n communityCards?: (string | null)[];\n pot?: number;\n pots?: PotRow[];\n isWaiting?: boolean;\n winnerSeats?: number[];\n seatSize?: CardSize;\n /** Optional override for rendering a seat. */\n renderSeat?: (seat: SeatView | null, index: number) => React.ReactNode;\n className?: string;\n}\n\nconst SEAT_COUNT = 6;\nconst SEAT_W = 160;\nconst SEAT_H = 176;\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 SEAT_RING_X = TABLE_RX - FELT_GAP + SEAT_W / 2 + 6;\nconst SEAT_RING_Y = TABLE_RY - FELT_GAP + SEAT_H / 2 + 6;\n\nconst BASE_ANGLES = [-90, -30, 30, 90, 150, 210];\n\ninterface SeatLayout {\n x: number;\n y: number;\n logicalIndex: number;\n isYou: boolean;\n seat: SeatView | null;\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 seats: (SeatView | null)[],\n yourSeatIndex: number | null,\n): SeatLayout[] {\n const cx = DESIGN_W / 2;\n const cy = DESIGN_H / 2;\n const rotationOffset =\n yourSeatIndex !== null ? BASE_ANGLES[3] - BASE_ANGLES[yourSeatIndex] : 0;\n\n const layouts: SeatLayout[] = [];\n for (let i = 0; i < SEAT_COUNT; i++) {\n const pt = getEllipsePoint(cx, cy, SEAT_RING_X, SEAT_RING_Y, BASE_ANGLES[i] + rotationOffset);\n layouts.push({\n x: pt.x - SEAT_W / 2,\n y: pt.y - SEAT_H / 2,\n logicalIndex: i,\n isYou: i === yourSeatIndex,\n seat: seats[i] ?? null,\n });\n }\n return layouts;\n}\n\n/**\n * A responsive, themable poker table. It scales a fixed 1200x900 design onto\n * its container and arranges six seats around the felt. Purely presentational:\n * pass seat view-models and card codes in.\n */\nexport function PokerTable({\n seats,\n yourSeatIndex = null,\n communityCards = [],\n pot = 0,\n pots = [],\n isWaiting = false,\n winnerSeats = [],\n seatSize = \"sm\",\n renderSeat,\n className,\n}: PokerTableProps) {\n const outerRef = useRef<HTMLDivElement>(null);\n const [scale, setScale] = useState(1);\n\n const layouts = useMemo(\n () => calculateSeatLayouts(seats, yourSeatIndex),\n [seats, yourSeatIndex],\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 betLineInset = {\n inset: `${MARGIN_Y + (TABLE_RY - BET_RY)}px ${MARGIN_X + (TABLE_RX - BET_RX)}px`,\n };\n\n const potRows: PotRow[] = pots.length > 0 ? pots : pot > 0 ? [{ label: \"Pot\", amount: pot }] : [];\n\n return (\n <div\n ref={outerRef}\n className={cn(\"relative flex h-full w-full items-center justify-center overflow-hidden min-h-0\", 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 {/* ambient spotlight */}\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 {/* 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 {/* center: community cards + pots */}\n <div className=\"absolute inset-0 flex flex-col items-center justify-center pointer-events-none\">\n <div className=\"mb-4 flex flex-col items-center gap-3\">\n <CommunityCards cards={communityCards} size=\"lg\" className=\"filter drop-shadow-lg\" />\n {potRows.length > 0 && (\n <div className=\"flex flex-col items-center gap-1\">\n {potRows.map((p, i) => (\n <div key={i} className=\"flex items-center gap-2 font-mono text-sm text-amber-400\">\n <span className=\"text-neutral-400\">{p.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 {formatBB(p.amount)}\n </span>\n </div>\n ))}\n </div>\n )}\n </div>\n {isWaiting && (\n <div className=\"rounded-full border border-neutral-700 bg-neutral-900/70 px-3 py-1 text-xs text-neutral-400\">\n Waiting to start\n </div>\n )}\n </div>\n\n {/* seats */}\n <div className=\"absolute inset-0 z-20 pointer-events-none\">\n {layouts.map(({ x, y, logicalIndex, isYou, seat }) => {\n const merged: SeatView | null = seat\n ? { ...seat, isYou, isWinner: winnerSeats.includes(logicalIndex) || seat.isWinner }\n : null;\n return (\n <div key={logicalIndex} className=\"absolute\" style={{ left: `${x}px`, top: `${y}px` }}>\n {renderSeat ? renderSeat(merged, logicalIndex) : <Seat seat={merged} size={seatSize} />}\n </div>\n );\n })}\n </div>\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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stargazeryan/poker-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Themable React + Tailwind CSS v4 components for poker tables and playing cards.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Ryan",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"sideEffects": ["**/*.css"],
|
|
9
|
+
"files": ["dist", "src/styles.css", "README.md", "LICENSE"],
|
|
10
|
+
"main": "./dist/index.cjs",
|
|
11
|
+
"module": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" },
|
|
15
|
+
"./styles.css": "./src/styles.css"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": { "access": "public" },
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsup",
|
|
20
|
+
"typecheck": "tsc --noEmit",
|
|
21
|
+
"demo": "vite --config demo/vite.config.ts",
|
|
22
|
+
"demo:build": "vite build --config demo/vite.config.ts"
|
|
23
|
+
},
|
|
24
|
+
"keywords": ["poker", "playing-cards", "tailwindcss", "react", "component", "card", "table"],
|
|
25
|
+
"peerDependencies": { "react": ">=18", "react-dom": ">=18" },
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@tailwindcss/vite": "^4.3.3",
|
|
28
|
+
"@types/react": "^19.2.0",
|
|
29
|
+
"@types/react-dom": "^19.2.0",
|
|
30
|
+
"@vitejs/plugin-react": "^4.3.0",
|
|
31
|
+
"react": "^19.2.0",
|
|
32
|
+
"react-dom": "^19.2.0",
|
|
33
|
+
"tailwindcss": "^4.3.3",
|
|
34
|
+
"tsup": "^8.3.0",
|
|
35
|
+
"typescript": "^5.6.0",
|
|
36
|
+
"vite": "^6.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/styles.css
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Styles for @stargazeryan/poker-ui.
|
|
3
|
+
*
|
|
4
|
+
* IMPORTANT (Tailwind v4): because this is a compiled package, your app's
|
|
5
|
+
* Tailwind build cannot see the class names used inside it. Add this package
|
|
6
|
+
* as a content source so the utilities are generated:
|
|
7
|
+
*
|
|
8
|
+
* @import "tailwindcss";
|
|
9
|
+
* @source "../node_modules/@stargazeryan/poker-ui";
|
|
10
|
+
* @import "@stargazeryan/poker-ui/styles.css";
|
|
11
|
+
*
|
|
12
|
+
* The directives below add theme tokens and the two keyframe animations the
|
|
13
|
+
* components rely on.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@theme {
|
|
17
|
+
--color-felt: #0f4d2e;
|
|
18
|
+
--color-felt-light: #1a6b3d;
|
|
19
|
+
--color-wood: #8b5e3c;
|
|
20
|
+
--color-wood-dark: #5d3f1f;
|
|
21
|
+
--color-chip-red: #c0392b;
|
|
22
|
+
--color-chip-blue: #2980b9;
|
|
23
|
+
--color-chip-green: #27ae60;
|
|
24
|
+
--color-chip-black: #1a1a2e;
|
|
25
|
+
--color-chip-white: #ecf0f1;
|
|
26
|
+
--color-gold: #f39c12;
|
|
27
|
+
|
|
28
|
+
--animate-card-deal: card-deal 0.35s ease-out both;
|
|
29
|
+
--animate-bubble-pop: bubble-pop 0.28s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@keyframes card-deal {
|
|
33
|
+
0% {
|
|
34
|
+
opacity: 0;
|
|
35
|
+
transform: translateY(-14px) rotateY(90deg) scale(0.8);
|
|
36
|
+
}
|
|
37
|
+
100% {
|
|
38
|
+
opacity: 1;
|
|
39
|
+
transform: translateY(0) rotateY(0) scale(1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@keyframes bubble-pop {
|
|
44
|
+
0% {
|
|
45
|
+
opacity: 0;
|
|
46
|
+
transform: translateY(8px) scale(0.6);
|
|
47
|
+
}
|
|
48
|
+
100% {
|
|
49
|
+
opacity: 1;
|
|
50
|
+
transform: translateY(0) scale(1);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@media (prefers-reduced-motion: reduce) {
|
|
55
|
+
*,
|
|
56
|
+
*::before,
|
|
57
|
+
*::after {
|
|
58
|
+
animation-duration: 0.01ms !important;
|
|
59
|
+
animation-iteration-count: 1 !important;
|
|
60
|
+
transition-duration: 0.01ms !important;
|
|
61
|
+
}
|
|
62
|
+
}
|