@stargazeryan/poker-ui 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -12
- package/dist/index.cjs +746 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +547 -23
- package/dist/index.d.ts +547 -23
- package/dist/index.js +696 -168
- package/dist/index.js.map +1 -1
- package/package.json +27 -7
- package/src/styles.css +128 -95
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ an optional center slot, and a small set of built-in **game-feel animations**.
|
|
|
9
9
|
- 🎯 `CardTable` — responsive, scale-to-fit table with ambient felt, a your-turn edge glow, and winner confetti
|
|
10
10
|
- 💺 `Seat` — a neutral seat frame with slots + animated turn/winner states
|
|
11
11
|
- 🪙 `Pot` — an optional pot pill with a count-up animation
|
|
12
|
+
- 🎰 `ChipStack` / `Chip` — an amount broken into a real stack of chips (white 1, red 5, blue 10, green 25, black 100)
|
|
12
13
|
- 🔁 `nextSeatIndex()` — pure helper for "next seat on the ring"
|
|
13
14
|
|
|
14
15
|
> Formerly `PokerTable`; kept as a back-compat alias for `CardTable`.
|
|
@@ -37,6 +38,10 @@ theme CSS:
|
|
|
37
38
|
|
|
38
39
|
> Without the `@source` line, the components render unstyled. Adjust the path to
|
|
39
40
|
> wherever your `node_modules` lives.
|
|
41
|
+
>
|
|
42
|
+
> `npm run test:consumer` in this repo packs the package and builds a throwaway
|
|
43
|
+
> app that follows these three lines literally, then asserts the utilities,
|
|
44
|
+
> tokens and keyframes all made it into the output CSS.
|
|
40
45
|
|
|
41
46
|
## Usage
|
|
42
47
|
|
|
@@ -78,15 +83,138 @@ export function Table() {
|
|
|
78
83
|
|
|
79
84
|
| Component | Props |
|
|
80
85
|
| --- | --- |
|
|
81
|
-
| `CardTable` | `seatCount` (
|
|
82
|
-
| `Seat` | slots: `header`, `children`, `footer`, `badge`, `overlay`, `above`, `below`, `topLeft`, `topRight`, `topCenter`; states: `isActive`, `isWinner`, `isDimmed`; `animated` (default true), `winnerCrown`, `onClick`, `frameClassName` |
|
|
83
|
-
| `Pot` | `amount`, `label`, `format
|
|
86
|
+
| `CardTable` | `seatCount` (no upper limit), `seats` or `renderSeat(i, isYou, box)`, `bets` or `renderBet(i)` (placed on the betting line toward the center), `betRadiusScale` (default 0.72, and the drawn betting line follows it), `startAngle` (deg of seat 0, default 90° = directly below the center), `yourSeatIndex` (flags `isYou`; no offset), `center`, `ambient` (default true), `yourTurn`, `winnerSeats`, `confetti` (default true), `confettiColors`, `seatWidth`, `seatHeight`, `seatGap` (default 24), `designWidth`, `designHeight`, `marginX`, `marginY`, `feltGap`, `fit`, `className` |
|
|
87
|
+
| `Seat` | slots: `header`, `children`, `footer`, `badge`, `overlay`, `above`, `below`, `topLeft`, `topRight`, `topCenter`; states: `isActive`, `isWinner`, `isDimmed`; `animated` (default true), `winnerCrown`, `onClick`, `frameClassName`, `frameStyle` |
|
|
88
|
+
| `Pot` | `amount`, `label`, `format` (defaults to a plain number), `animate` (count-up, default true) |
|
|
89
|
+
| `ChipStack` | `amount` (chip units), `size`, `denominations`, `maxChips` (default 12), `label`, `className` |
|
|
90
|
+
| `Chip` | `value`, `color`, `colorClassName`, `size`, `label`, `className` |
|
|
84
91
|
| `PlayingCard` | `card` (`"As"`, `"10h"`, `null` = back), `size`: `xs \| sm \| md \| lg` |
|
|
85
|
-
| `HoleCards` | `cards`, `size`, `deal` (staggered deal-in, default true) |
|
|
92
|
+
| `HoleCards` | `cards`, `size`, `deal` (staggered deal-in, default true), `dealKey` (change it to replay the deal for an unchanged hand) |
|
|
93
|
+
| `CardBack` | `size`, `className` |
|
|
86
94
|
| `CommunityCards` | `cards`, `size`, `animated` (3D flip, default true), `slots` (fixed slot count) |
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
|
|
96
|
+
Everything the components do is also available as a pure function, one module per
|
|
97
|
+
concern:
|
|
98
|
+
|
|
99
|
+
| Module | Exports |
|
|
100
|
+
| --- | --- |
|
|
101
|
+
| `geometry` | `seatAngle`, `ellipsePoint`, `ringPoints` |
|
|
102
|
+
| `layout` | `layoutSeats`, `fitSeatScale`, `minSeatSeparation`, `seatPoint`, `tableRadii`, `seatRingRadii`, `ellipseBox`, `SEAT_SHRINK_FLOOR`, `RIM_GAP`, `GLOW_GAP`, `DESIGN_W`, `DESIGN_H`, `DEFAULT_MARGIN_X`, `DEFAULT_MARGIN_Y`, `DEFAULT_FELT_GAP`, `DEFAULT_BET_SCALE`, `DEFAULT_SEAT_GAP`, `DEFAULT_SEAT_W`, `DEFAULT_SEAT_H` |
|
|
103
|
+
| `cards` | `parseCard`, `describeCard`, `getRankName`, `getSuitName`, `getSuitSymbol`, `isRedSuit`, `isKnownSuit` |
|
|
104
|
+
| `chips` | `chipBreakdown`, `chipsTotal`, `chipCount`, `chipSpotColor`, `DEFAULT_DENOMINATIONS` |
|
|
105
|
+
| `color` | `parseColor`, `relativeLuminance`, `flatten`, `contrastRatio`, `mostVisible` |
|
|
106
|
+
| `sizes` | `SIZES` |
|
|
107
|
+
| `motion` | `COUNT_UP_MS`, `DEAL_STAGGER_MS`, `FLIP_STAGGER_MS` |
|
|
108
|
+
| misc | `cn`, `nextSeatIndex`, `formatBB`, `BB` |
|
|
109
|
+
|
|
110
|
+
Types: `SeatProps`, `CardTableProps`, `SeatBox`, `PotProps`, `PlayingCardProps`,
|
|
111
|
+
`CardBackProps`, `HoleCardsProps`, `CommunityCardsProps`, `ChipProps`,
|
|
112
|
+
`ChipStackProps`, `CardSize`, `ChipSize`, `Size`, `Point`, `Layout`,
|
|
113
|
+
`LayoutOptions`, `SeatLayout`, `EllipseBox`, `Rgba`, `ParsedCard`,
|
|
114
|
+
`Denomination`, `ChipStackEntry`.
|
|
115
|
+
|
|
116
|
+
### Amounts and units
|
|
117
|
+
|
|
118
|
+
Nothing here attaches a unit for you. `Pot` renders a plain, locale-grouped
|
|
119
|
+
number and `ChipStack` breaks an amount into chips; neither decides that your
|
|
120
|
+
numbers are big blinds, dollars or points, because that is a game rule and this
|
|
121
|
+
is generic UI.
|
|
122
|
+
|
|
123
|
+
`formatBB(amount, bb)` is an opt-in poker helper. Pass the big blind — the
|
|
124
|
+
exported `BB = 20` is that helper's own default, not something a component
|
|
125
|
+
applies behind your back:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<Pot amount={1234} /> {/* 1,234 */}
|
|
129
|
+
<Pot amount={1234} format={formatBB} /> {/* 61.7 BB */}
|
|
130
|
+
<Pot amount={1234} format={(n) => formatBB(n, 100)} /> {/* 12.3 BB */}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Chips
|
|
134
|
+
|
|
135
|
+
`ChipStack` turns an amount into a stack of chips, using the greedy breakdown a
|
|
136
|
+
dealer would use: **white 1, red 5, blue 10, green 25, black 100**. Blue is the
|
|
137
|
+
one that varies between houses — plenty of rooms use 20 — so pass
|
|
138
|
+
`denominations` to change the set (and to give the faces dollar labels).
|
|
139
|
+
|
|
140
|
+
Amounts are in **chip units** (the smallest chip is 1), not big blinds.
|
|
141
|
+
`chipBreakdown` is exported on its own if you want the split without the artwork,
|
|
142
|
+
and anything below the smallest chip is reported by `chipsTotal` rather than
|
|
143
|
+
rounded up. `maxChips` (default 12) keeps a large bet from becoming a hundred
|
|
144
|
+
black chips.
|
|
145
|
+
|
|
146
|
+
### Ellipse layout
|
|
147
|
+
|
|
148
|
+
The table, the seat ring and the betting line are all one parametric ellipse:
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
x(θ) = cx + rx·cos θ y(θ) = cy + ry·sin θ θ = startAngle + i·(360/n)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Screen coordinates (y grows downward), so θ = 90° is **directly below** the
|
|
155
|
+
center. **Seat 0 sits at `startAngle` (default 90°)**, and seats divide 360°
|
|
156
|
+
evenly, so 2–10 players always spread out. The betting line uses the same
|
|
157
|
+
formula with a smaller radius (`betRadiusScale`). To keep "you" at the bottom,
|
|
158
|
+
reorder your seat array so the viewer is index 0 (the package adds no offset).
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import { seatAngle, ellipsePoint, ringPoints } from "@stargazeryan/poker-ui";
|
|
162
|
+
|
|
163
|
+
seatAngle(0, 6); // 90 (below the center)
|
|
164
|
+
seatAngle(3, 6); // 270 (above the center)
|
|
165
|
+
ellipsePoint(300, 200, 120, 80, 90); // { x: 300, y: 280 }
|
|
166
|
+
ringPoints(4, 300, 200, 120, 80); // 4 points, first directly below
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Layout, seat size and responsiveness
|
|
170
|
+
|
|
171
|
+
Everything is laid out in a fixed **design box** (1200×900 by default) which is
|
|
172
|
+
then scaled to fit the container.
|
|
173
|
+
|
|
174
|
+
| Prop | Default | Meaning |
|
|
175
|
+
| --- | --- | --- |
|
|
176
|
+
| `designWidth` / `designHeight` | `1200` / `900` | the design box |
|
|
177
|
+
| `marginX` / `marginY` | `164/1200`, `196/900` | rail inset, as a **fraction** of the design box |
|
|
178
|
+
| `feltGap` | `11` | felt inset from the rail, design-space px |
|
|
179
|
+
| `seatWidth` / `seatHeight` | `160` / `176` | **preferred** seat frame size |
|
|
180
|
+
| `seatGap` | `24` | minimum clearance between neighbouring frames |
|
|
181
|
+
| `fit` | `"design"` | `"container"` derives the width from the container |
|
|
182
|
+
|
|
183
|
+
Margins are proportional, so a custom design box stays in proportion, and the
|
|
184
|
+
seat ring is clamped so frames are never clipped by the design box.
|
|
185
|
+
|
|
186
|
+
Two things to know:
|
|
187
|
+
|
|
188
|
+
- **`fit="container"` removes letterboxing.** With the default fit, a
|
|
189
|
+
phone-portrait container (e.g. 390×700) scales the 4:3 design box down and
|
|
190
|
+
wastes ~58% of the box — the table ends up tiny. `fit="container"` derives the
|
|
191
|
+
design width from the container's aspect ratio (clamped to 0.5–2.5) so the
|
|
192
|
+
table fills the box and the seats render much larger.
|
|
193
|
+
- **Seats shrink to keep their distance.** At 160×176, a 6-seat ring leaves
|
|
194
|
+
161px between neighbours but a 10-seat ring only 25.6px. `CardTable` measures
|
|
195
|
+
the ring and shrinks the frames uniformly (never below 50%) until `seatGap` is
|
|
196
|
+
honoured, so 9–10 seat tables never crowd. Every `Seat` inside the table is
|
|
197
|
+
told the laid-out size through context, so this works whether you pass
|
|
198
|
+
`renderSeat` or a `seats` array; `renderSeat` also receives the box as its third
|
|
199
|
+
argument if you want it. The same mechanism is why 11–16 seats lay out at all
|
|
200
|
+
(measured: 11 → 126×139 frames, 16 → 80×88); past roughly 16 the frames sit on
|
|
201
|
+
the 50% floor and overlap, so `seatCount` is not clamped above but it is not
|
|
202
|
+
magic either:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
<CardTable
|
|
206
|
+
seatCount={10}
|
|
207
|
+
seatGap={48} // roomier; 0 disables the auto-shrink
|
|
208
|
+
renderSeat={(i, isYou, box) => (
|
|
209
|
+
<Seat width={box.width} height={box.height} header={<b>{i}</b>}>
|
|
210
|
+
<HoleCards cards={hands[i]} size="sm" />
|
|
211
|
+
</Seat>
|
|
212
|
+
)}
|
|
213
|
+
/>
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
`seatGap={0}` disables it; the `seats` array form is not auto-shrunk (pass
|
|
217
|
+
matching `width`/`height` to your `Seat`s).
|
|
90
218
|
|
|
91
219
|
### Sizes
|
|
92
220
|
|
|
@@ -108,7 +236,12 @@ Built-in game-feel (all **on by default**, each toggleable):
|
|
|
108
236
|
| Your-turn edge glow | `CardTable` | omit `yourTurn` |
|
|
109
237
|
| Winner confetti | `CardTable` | `confetti={false}` |
|
|
110
238
|
|
|
111
|
-
All animations are disabled automatically when the user prefers reduced
|
|
239
|
+
All CSS animations are disabled automatically when the user prefers reduced
|
|
240
|
+
motion. (The `Pot` count-up is a `requestAnimationFrame` loop, so it is *not*
|
|
241
|
+
covered by that media query.)
|
|
242
|
+
|
|
243
|
+
Animation tokens live in `@theme`, so Tailwind only ships the keyframes your app
|
|
244
|
+
actually uses.
|
|
112
245
|
|
|
113
246
|
## "Whose turn is next?"
|
|
114
247
|
|
|
@@ -123,23 +256,83 @@ const next = nextSeatIndex(current, seatCount, { skip: (i) => players[i].out });
|
|
|
123
256
|
|
|
124
257
|
## Theming
|
|
125
258
|
|
|
126
|
-
Override
|
|
259
|
+
Override any token in your own `@theme` block. Every token below is referenced
|
|
260
|
+
by the components, so an override actually restyles them:
|
|
127
261
|
|
|
128
262
|
```css
|
|
129
263
|
@theme {
|
|
130
264
|
--color-felt: #0b3d2e;
|
|
131
|
-
--color-
|
|
265
|
+
--color-felt-light: #14563a;
|
|
266
|
+
--color-felt-dark: #062418;
|
|
267
|
+
|
|
268
|
+
--color-rail: #7a5230;
|
|
269
|
+
--color-rail-dark: #4d3119;
|
|
270
|
+
--color-rail-deep: #28190c;
|
|
271
|
+
|
|
272
|
+
--color-gold: #ffcc33; /* turn/winner glow, pot, focus ring */
|
|
273
|
+
--color-gold-light: #ffe08a; /* ambient glow, backdrop */
|
|
274
|
+
--color-gold-glow: #fff0c2; /* spotlight */
|
|
275
|
+
|
|
276
|
+
--color-card-back: #14315a;
|
|
277
|
+
--color-card-back-light: #1e4577;
|
|
278
|
+
--color-card-back-dark: #0b1d36;
|
|
279
|
+
--color-card-back-ink: #9fc0e8;
|
|
280
|
+
--color-card-back-emblem: #ffd98a;
|
|
281
|
+
|
|
282
|
+
--color-chip-blue: #1d4ed8; /* chip denominations */
|
|
132
283
|
}
|
|
133
284
|
```
|
|
134
285
|
|
|
135
|
-
|
|
286
|
+
Confetti has its own `confettiColors` prop rather than a token, because the
|
|
287
|
+
colours go into inline styles that Tailwind's tree-shaking cannot see.
|
|
288
|
+
|
|
289
|
+
`--color-chip-red|blue|green|black|white` is the chip palette, used by `Chip`
|
|
290
|
+
and `ChipStack` through `fill-*` utilities. Note that Tailwind v4 only emits a
|
|
291
|
+
theme variable that something references, which is why the package ships no
|
|
292
|
+
unused tokens: every one in `styles.css` is drawn with by a component, so
|
|
293
|
+
overriding any of them works.
|
|
294
|
+
|
|
295
|
+
Individual components also take `className` / `frameClassName`.
|
|
296
|
+
|
|
297
|
+
The [docs site](demo/) has a live token editor that sets these custom properties
|
|
298
|
+
on a wrapper, which is exactly what an override does document-wide.
|
|
299
|
+
|
|
300
|
+
## Docs site
|
|
301
|
+
|
|
302
|
+
The docs in `demo/` are the library's main documentation: a per-component site
|
|
303
|
+
with live demos, expandable source, API tables and a live token editor, plus a
|
|
304
|
+
[Known gaps](demo/src/docs/pages/guide.tsx) page listing what this library does
|
|
305
|
+
*not* do yet.
|
|
306
|
+
|
|
307
|
+
```bash
|
|
308
|
+
npm run docs # dev server (imports ../../src, HMR)
|
|
309
|
+
npm run docs:build # builds the package, then the docs against dist/
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`docs:build` compiles the package first and then builds the site against
|
|
313
|
+
`dist/index.js` rather than `src`, so the deployed site is running the artefact
|
|
314
|
+
consumers actually get. It imports the package by its published name in both
|
|
315
|
+
modes — never a relative path into `src`.
|
|
316
|
+
|
|
317
|
+
Tests cover the docs too: `tests/docs.test.tsx` renders every route and fails if
|
|
318
|
+
a page throws, and `tests/docs-api.test.ts` reads the real interfaces with the
|
|
319
|
+
TypeScript compiler and fails if an API table drifts from the code.
|
|
320
|
+
|
|
321
|
+
## Development
|
|
136
322
|
|
|
137
323
|
```bash
|
|
138
324
|
npm install --include=dev
|
|
139
|
-
|
|
140
|
-
npm run
|
|
325
|
+
|
|
326
|
+
npm run typecheck # tsc --noEmit
|
|
327
|
+
npm test # vitest: unit, component, docs routes, docs-vs-types
|
|
328
|
+
npm run build # tsup -> dist (ESM + CJS + d.ts)
|
|
329
|
+
|
|
330
|
+
npm run test:consumer # pack + install + build a real consumer app, assert the CSS
|
|
141
331
|
```
|
|
142
332
|
|
|
333
|
+
`npm run demo` and `npm run demo:build` remain as aliases for `docs` and
|
|
334
|
+
`docs:build`.
|
|
335
|
+
|
|
143
336
|
## License
|
|
144
337
|
|
|
145
338
|
MIT © 2026 Ryan
|