@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 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` (2–10), `seats` or `renderSeat(i, isYou)`, `yourSeatIndex`, `center`, `ambient` (default true), `yourTurn`, `winnerSeats`, `confetti` (default true), `seatWidth`, `seatHeight` |
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`, `animate` (count-up, default true) |
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
- Also exported: `nextSeatIndex`, `cn`, `formatBB`, `BB`, `parseCard`,
89
- `getSuitSymbol`, and the types `SeatProps`, `CardTableProps`, `PotProps`, `CardSize`.
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 motion.
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 the tokens in your own `@theme` block, or pass `className` / `frameClassName`:
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-gold: #ffcc33;
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
- ## Demo
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
- npm run demo # Vite dev server
140
- npm run demo:build # static build
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