poker-irongrid 1.4.0 → 2.0.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
@@ -217,6 +217,21 @@ The component includes comprehensive filtering options:
217
217
  - Visual distinction between colored and uncolored hands
218
218
  - Color statistics display
219
219
 
220
+ ### Design (2.0)
221
+
222
+ - **Grid:** small tiles with rounded corners and 2px gaps (`--irongrid-cell-gap`, `--irongrid-cell-radius`),
223
+ two-character labels (AK above the diagonal is suited, below is offsuit). Hands in the range are filled with
224
+ their color (lighter when only some combos are in); hands outside it get a faint suited / offsuit / pair tint
225
+ (`--irongrid-cell-suited-bg`, `--irongrid-cell-offsuit-bg`, `--irongrid-cell-paired-bg`) and grey text.
226
+ Selected colored hands get a ring (`--irongrid-cell-ring-color`). With `subgrid`, the 3x3 blocks are set apart.
227
+ - **Loading a range** (range select) fills a fresh grid with the first color; nothing is selected.
228
+ - **Controls** (`showControls`): color chips showing each color's share of the range (%), combos (#) or hero's
229
+ equity (Eq), and × to clear; the share selected with All / Invert; a **Made × Draw** table (made hand by draw:
230
+ none, straight, flush, combo, backdoor flush) or **Made × Equity** (hero's equity against the hands, in 20%
231
+ bands); rank filters; and a suit matrix with Suited / Offsuit. Draws must use a hole card (poker-extval with
232
+ `holeCards`). Clicking any table, rank or suit cell selects its hands (ctrl/cmd adds, shift removes); a color
233
+ chip then colors the selection.
234
+
220
235
  ### Equity Evaluation
221
236
 
222
237
  When `evaluator={true}`, hero's equity is calculated live (asynchronously) whenever the hero cards,
@@ -226,6 +226,52 @@ export class PokerGridManager {
226
226
  * @returns {Promise<Object>} Matching hands
227
227
  */
228
228
  getHandsByDraw(targetDraw: string, hands?: any, boardCards?: string[], boardLength?: number): Promise<any>;
229
+ /** The color of a hand's values (its hsl entry), or null. */
230
+ colorOf(values: any): any;
231
+ /**
232
+ * Combos in the range: those with a color or selected (dead combos are expected to be filtered out already).
233
+ * @param {Object} hands - Hands object (visible hands)
234
+ * @returns {string[]}
235
+ */
236
+ getRangeCombos(hands?: any): string[];
237
+ /**
238
+ * Select combos with the usual modifiers: plain replaces the selection, ctrl/cmd adds, shift removes.
239
+ * @param {Object} hands - Hands object
240
+ * @param {string[]} combos - Combos to select
241
+ * @param {{ctrl?: boolean, shift?: boolean}} modifiers
242
+ * @returns {Object} Updated hands object
243
+ */
244
+ selectCombos(hands: any, combos: string[], modifiers?: {
245
+ ctrl?: boolean;
246
+ shift?: boolean;
247
+ }): any;
248
+ /**
249
+ * Invert the selection within the range.
250
+ * @param {Object} hands - Hands object
251
+ * @returns {Object} Updated hands object
252
+ */
253
+ invertRangeSelection(hands: any): any;
254
+ /**
255
+ * Classify combos on a board for the Made x Draw table: made hand (row) and draw (column), from
256
+ * poker-extval with the hole cards, so draws must use a hole card. Cached per board.
257
+ * Made: highCard, onePair, twoPair, trips, straight, flush, fullHouse, quads, straightFlush.
258
+ * Draw (flop/turn only): combo (flush + straight draw), flush, straight, backdoor (backdoor flush draw,
259
+ * flop only), none.
260
+ * @param {string[]} combos - Combos
261
+ * @param {string[]} boardCards - Board (3-5 cards)
262
+ * @returns {Map<string, {made: string, draw: string}>}
263
+ */
264
+ classifyCombos(combos: string[], boardCards?: string[]): Map<string, {
265
+ made: string;
266
+ draw: string;
267
+ }>;
268
+ classifyCacheBoard: any;
269
+ classifyCache: Map<any, any>;
270
+ /** Uncached classifyCombos for one combo, or null if the cards clash. */
271
+ classifyCombo(combo: any, boardCards: any): {
272
+ made: string;
273
+ draw: string;
274
+ };
229
275
  /**
230
276
  * Apply color to hands.
231
277
  * @param {Object} hands - Current hands object
@@ -244,19 +290,18 @@ export class PokerGridManager {
244
290
  /**
245
291
  * Hero's equity against the villain hands on the grid, overall and per color, from one
246
292
  * poker-bitvaljs compareRangeHands call (each villain combo's result against hero's hand).
247
- * Villain hands are the combos that are selected or colored by hand (colors in `autoColors`, set by
248
- * applyEquityBands, don't count); hands holding a dead, board or hero card are left out.
293
+ * Villain hands are the combos in the range (colored or selected); hands holding a dead, board or hero
294
+ * card are left out.
249
295
  * @param {Object} hands - Hands object
250
296
  * @param {string[]} heroCards - Hero cards (two)
251
297
  * @param {string[]} boardCards - Board cards (0-5)
252
298
  * @param {string[]} deadCards - Dead cards (may include hero and board cards)
253
299
  * @param {string[]} colors - Colors array
254
- * @param {Map<string, string>|null} autoColors - Colors applyEquityBands set (combo -> color)
255
300
  * @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object, byCombo: Object}|null>}
256
301
  * Equities in percent (null when hero meets no villain combo); byCombo is hero's equity against each
257
302
  * villain combo. Null without two hero cards.
258
303
  */
259
- calculateEquity(hands?: any, heroCards?: string[], boardCards?: string[], deadCards?: string[], colors?: string[], autoColors?: Map<string, string> | null): Promise<{
304
+ calculateEquity(hands?: any, heroCards?: string[], boardCards?: string[], deadCards?: string[], colors?: string[]): Promise<{
260
305
  hero: number | null;
261
306
  villain: number | null;
262
307
  combos: number;
@@ -267,16 +312,17 @@ export class PokerGridManager {
267
312
  * Color villain combos by hero's equity against them: one equal-width band per color, colors[0] for
268
313
  * hero's lowest equity (villain's strongest hands) up to the last color for hero's highest. With 6
269
314
  * colors the bands are 0-16.7%, 16.7-33.3%, ... 83.3-100%.
270
- * Only combos without a color, or still holding the color this method gave them, are recolored, so
271
- * hand-applied colors stay. Colors this method set are removed from combos that are no longer in
272
- * `byCombo` (deselected, dead, or no equity).
315
+ * Only combos without a color, still holding the color this method gave them, or holding their base
316
+ * color (e.g. from loading a range) are recolored, so hand-applied colors stay. Combos this method colored
317
+ * that are no longer in `byCombo` (dead, or no equity) go back to their base color, or lose the color.
273
318
  * @param {Object} hands - Hands object
274
319
  * @param {Object} byCombo - Hero's equity (%) against each villain combo, from calculateEquity
275
320
  * @param {string[]} colors - Colors, one per band
276
321
  * @param {Map<string, string>} autoColors - Colors set by earlier calls (combo -> color)
322
+ * @param {Map<string, string>} baseColors - Colors combos had before banding (combo -> color)
277
323
  * @returns {{hands: Object, autoColors: Map<string, string>, changed: boolean}}
278
324
  */
279
- applyEquityBands(hands: any, byCombo: any, colors: string[], autoColors?: Map<string, string>): {
325
+ applyEquityBands(hands: any, byCombo: any, colors: string[], autoColors?: Map<string, string>, baseColors?: Map<string, string>): {
280
326
  hands: any;
281
327
  autoColors: Map<string, string>;
282
328
  changed: boolean;
@@ -286,6 +332,17 @@ export class PokerGridManager {
286
332
  * @returns {Promise<Object>} Color equities object
287
333
  */
288
334
  calculateColorEquities(hands?: any, heroCards?: any, boardCards?: any, deadCards?: any, colors?: any): Promise<any>;
335
+ /**
336
+ * Load a predefined range as a fresh grid: every combo gets `color` and nothing is selected.
337
+ * @param {string} rangeName - Range name
338
+ * @param {Array} ranges - Ranges array
339
+ * @param {string} color - Color for the range's combos
340
+ * @returns {{hands: Object, rangeName: string|null}}
341
+ */
342
+ loadRange(rangeName: string, ranges: any[], color: string): {
343
+ hands: any;
344
+ rangeName: string | null;
345
+ };
289
346
  /**
290
347
  * Apply predefined range.
291
348
  * @param {string} rangeName - Range name
@@ -617,6 +617,112 @@ export class PokerGridManager {
617
617
  return this.rangeManagerToMatching(drawOnlyRm, hands);
618
618
  }
619
619
 
620
+ // ==================== Range Views (design) ====================
621
+
622
+ /** The color of a hand's values (its hsl entry), or null. */
623
+ colorOf(values) {
624
+ return (values || []).find(item => typeof item === 'string' && item.startsWith('hsl(')) || null;
625
+ }
626
+
627
+ /**
628
+ * Combos in the range: those with a color or selected (dead combos are expected to be filtered out already).
629
+ * @param {Object} hands - Hands object (visible hands)
630
+ * @returns {string[]}
631
+ */
632
+ getRangeCombos(hands = this.hands) {
633
+ return Object.keys(hands || {}).filter(h => isCombo(h) && (this.hasExplicitColor(hands[h]) || this.isSelected(hands[h])));
634
+ }
635
+
636
+ /**
637
+ * Select combos with the usual modifiers: plain replaces the selection, ctrl/cmd adds, shift removes.
638
+ * @param {Object} hands - Hands object
639
+ * @param {string[]} combos - Combos to select
640
+ * @param {{ctrl?: boolean, shift?: boolean}} modifiers
641
+ * @returns {Object} Updated hands object
642
+ */
643
+ selectCombos(hands, combos, modifiers = {}) {
644
+ const newHands = modifiers.ctrl || modifiers.shift ? { ...hands } : this.unselectAll(hands);
645
+ combos.forEach(h => {
646
+ const values = (newHands[h] || []).filter(item => item !== 'selected');
647
+ newHands[h] = modifiers.shift ? values : [...values, 'selected'];
648
+ });
649
+ return newHands;
650
+ }
651
+
652
+ /**
653
+ * Invert the selection within the range.
654
+ * @param {Object} hands - Hands object
655
+ * @returns {Object} Updated hands object
656
+ */
657
+ invertRangeSelection(hands) {
658
+ const newHands = { ...hands };
659
+ this.getRangeCombos(hands).forEach(h => {
660
+ const values = newHands[h] || [];
661
+ newHands[h] = values.includes('selected') ? values.filter(item => item !== 'selected') : [...values, 'selected'];
662
+ });
663
+ return newHands;
664
+ }
665
+
666
+ /**
667
+ * Classify combos on a board for the Made x Draw table: made hand (row) and draw (column), from
668
+ * poker-extval with the hole cards, so draws must use a hole card. Cached per board.
669
+ * Made: highCard, onePair, twoPair, trips, straight, flush, fullHouse, quads, straightFlush.
670
+ * Draw (flop/turn only): combo (flush + straight draw), flush, straight, backdoor (backdoor flush draw,
671
+ * flop only), none.
672
+ * @param {string[]} combos - Combos
673
+ * @param {string[]} boardCards - Board (3-5 cards)
674
+ * @returns {Map<string, {made: string, draw: string}>}
675
+ */
676
+ classifyCombos(combos, boardCards = this.boardCards) {
677
+ const result = new Map();
678
+ if (!boardCards || boardCards.length < 3) return result;
679
+ const key = boardCards.join('');
680
+ if (this.classifyCacheBoard !== key) {
681
+ this.classifyCacheBoard = key;
682
+ this.classifyCache = new Map();
683
+ }
684
+ combos.forEach(h => {
685
+ if (!this.classifyCache.has(h)) {
686
+ this.classifyCache.set(h, this.classifyCombo(h, boardCards));
687
+ }
688
+ const value = this.classifyCache.get(h);
689
+ if (value) result.set(h, value);
690
+ });
691
+ return result;
692
+ }
693
+
694
+ /** Uncached classifyCombos for one combo, or null if the cards clash. */
695
+ classifyCombo(combo, boardCards) {
696
+ const hole = [combo.substring(0, 2), combo.substring(2, 4)];
697
+ if (hole.some(card => boardCards.includes(card))) return null;
698
+ try {
699
+ const evaluation = Object.values(evaluateHand([...boardCards, ...hole], { holeCards: hole }))[0];
700
+ if (!evaluation) return null;
701
+ let made = 'highCard';
702
+ if (evaluation.isStraightFlush || evaluation.isRoyalFlush) made = 'straightFlush';
703
+ else if (evaluation.isFourOfAKind) made = 'quads';
704
+ else if (evaluation.isFullHouse) made = 'fullHouse';
705
+ else if (evaluation.isFlush) made = 'flush';
706
+ else if (evaluation.isStraight) made = 'straight';
707
+ else if (evaluation.isThreeOfAKind) made = 'trips';
708
+ else if (evaluation.isTwoPair) made = 'twoPair';
709
+ else if (evaluation.isPair) made = 'onePair';
710
+
711
+ let draw = 'none';
712
+ if (boardCards.length < 5) {
713
+ const flushDraw = !!evaluation.isFlushDraw;
714
+ const straightDraw = !!evaluation.isStraightDraw;
715
+ if (flushDraw && straightDraw) draw = 'combo';
716
+ else if (flushDraw) draw = 'flush';
717
+ else if (straightDraw) draw = 'straight';
718
+ else if (boardCards.length === 3 && evaluation.isBackdoorFlushDraw) draw = 'backdoor';
719
+ }
720
+ return { made, draw };
721
+ } catch (error) {
722
+ return null;
723
+ }
724
+ }
725
+
620
726
  // ==================== Color Methods ====================
621
727
 
622
728
  /**
@@ -654,32 +760,26 @@ export class PokerGridManager {
654
760
  /**
655
761
  * Hero's equity against the villain hands on the grid, overall and per color, from one
656
762
  * poker-bitvaljs compareRangeHands call (each villain combo's result against hero's hand).
657
- * Villain hands are the combos that are selected or colored by hand (colors in `autoColors`, set by
658
- * applyEquityBands, don't count); hands holding a dead, board or hero card are left out.
763
+ * Villain hands are the combos in the range (colored or selected); hands holding a dead, board or hero
764
+ * card are left out.
659
765
  * @param {Object} hands - Hands object
660
766
  * @param {string[]} heroCards - Hero cards (two)
661
767
  * @param {string[]} boardCards - Board cards (0-5)
662
768
  * @param {string[]} deadCards - Dead cards (may include hero and board cards)
663
769
  * @param {string[]} colors - Colors array
664
- * @param {Map<string, string>|null} autoColors - Colors applyEquityBands set (combo -> color)
665
770
  * @returns {Promise<{hero: number|null, villain: number|null, combos: number, byColor: Object, byCombo: Object}|null>}
666
771
  * Equities in percent (null when hero meets no villain combo); byCombo is hero's equity against each
667
772
  * villain combo. Null without two hero cards.
668
773
  */
669
- async calculateEquity(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors, autoColors = null) {
774
+ async calculateEquity(hands = this.hands, heroCards = this.heroCards, boardCards = this.boardCards, deadCards = this.deadCards, colors = this.colors) {
670
775
  if (heroCards?.length !== 2) return null;
671
776
  const board = boardCards || [];
672
777
  const otherDead = (deadCards || []).filter(card => !heroCards.includes(card) && !board.includes(card));
673
778
  const blocked = new Set([...heroCards, ...board, ...otherDead]);
674
779
 
675
- const colorOf = (h) => (hands[h] || []).find(item => typeof item === 'string' && item.startsWith('hsl('));
676
- const coloredByHand = (h) => {
677
- const color = colorOf(h);
678
- return !!color && !(autoColors && autoColors.get(h) === color);
679
- };
680
780
  const villainHands = Object.keys(hands || {}).filter(h =>
681
781
  isCombo(h) &&
682
- (coloredByHand(h) || this.isSelected(hands[h])) &&
782
+ (this.hasExplicitColor(hands[h]) || this.isSelected(hands[h])) &&
683
783
  !blocked.has(h.substring(0, 2)) && !blocked.has(h.substring(2, 4))
684
784
  );
685
785
 
@@ -729,16 +829,17 @@ export class PokerGridManager {
729
829
  * Color villain combos by hero's equity against them: one equal-width band per color, colors[0] for
730
830
  * hero's lowest equity (villain's strongest hands) up to the last color for hero's highest. With 6
731
831
  * colors the bands are 0-16.7%, 16.7-33.3%, ... 83.3-100%.
732
- * Only combos without a color, or still holding the color this method gave them, are recolored, so
733
- * hand-applied colors stay. Colors this method set are removed from combos that are no longer in
734
- * `byCombo` (deselected, dead, or no equity).
832
+ * Only combos without a color, still holding the color this method gave them, or holding their base
833
+ * color (e.g. from loading a range) are recolored, so hand-applied colors stay. Combos this method colored
834
+ * that are no longer in `byCombo` (dead, or no equity) go back to their base color, or lose the color.
735
835
  * @param {Object} hands - Hands object
736
836
  * @param {Object} byCombo - Hero's equity (%) against each villain combo, from calculateEquity
737
837
  * @param {string[]} colors - Colors, one per band
738
838
  * @param {Map<string, string>} autoColors - Colors set by earlier calls (combo -> color)
839
+ * @param {Map<string, string>} baseColors - Colors combos had before banding (combo -> color)
739
840
  * @returns {{hands: Object, autoColors: Map<string, string>, changed: boolean}}
740
841
  */
741
- applyEquityBands(hands, byCombo, colors, autoColors = new Map()) {
842
+ applyEquityBands(hands, byCombo, colors, autoColors = new Map(), baseColors = new Map()) {
742
843
  const next = new Map();
743
844
  const newHands = { ...hands };
744
845
  let changed = false;
@@ -754,7 +855,7 @@ export class PokerGridManager {
754
855
  Object.entries(byCombo || {}).forEach(([h, equity]) => {
755
856
  if (equity === null || equity === undefined || Number.isNaN(equity)) return;
756
857
  const current = colorOf(newHands[h]);
757
- if (current && autoColors.get(h) !== current) return; // colored by hand
858
+ if (current && autoColors.get(h) !== current && baseColors.get(h) !== current) return; // colored by hand
758
859
  const color = colors[Math.min(colors.length - 1, Math.floor(equity / bandWidth))];
759
860
  next.set(h, color);
760
861
  if (current !== color) setColor(h, color);
@@ -764,7 +865,7 @@ export class PokerGridManager {
764
865
  // Combos that lost their equity color (no longer villain hands, or no equity)
765
866
  autoColors.forEach((color, h) => {
766
867
  if (next.has(h) || !newHands[h]) return;
767
- if (colorOf(newHands[h]) === color) setColor(h, null);
868
+ if (colorOf(newHands[h]) === color) setColor(h, baseColors.get(h) || null);
768
869
  });
769
870
 
770
871
  return { hands: changed ? newHands : hands, autoColors: next, changed };
@@ -783,6 +884,21 @@ export class PokerGridManager {
783
884
 
784
885
  // ==================== Range Methods ====================
785
886
 
887
+ /**
888
+ * Load a predefined range as a fresh grid: every combo gets `color` and nothing is selected.
889
+ * @param {string} rangeName - Range name
890
+ * @param {Array} ranges - Ranges array
891
+ * @param {string} color - Color for the range's combos
892
+ * @returns {{hands: Object, rangeName: string|null}}
893
+ */
894
+ loadRange(rangeName, ranges, color) {
895
+ const selectedRange = ranges.find(r => r.name === rangeName);
896
+ if (!selectedRange) return { hands: {}, rangeName: null };
897
+ const hands = {};
898
+ new RangeManager(selectedRange.abbreviation).toArray().forEach(h => { hands[h] = [color]; });
899
+ return { hands, rangeName };
900
+ }
901
+
786
902
  /**
787
903
  * Apply predefined range.
788
904
  * @param {string} rangeName - Range name
@@ -1087,8 +1203,8 @@ export class PokerGridManager {
1087
1203
  newSelection[h] = (newSelection[h] || []).filter(item => item !== 'selected');
1088
1204
  });
1089
1205
  } else {
1090
- // Normal: Apply color to selected hands
1091
- newSelection = this.applyColor(newSelection, color, selectedHands);
1206
+ // Normal: Apply color to selected hands, then clear the selection
1207
+ newSelection = this.unselectAll(this.applyColor(newSelection, color, selectedHands));
1092
1208
  }
1093
1209
 
1094
1210
  return newSelection;
@@ -29,20 +29,6 @@
29
29
  return index + 1 + (split ? (index >= 5 ? 1 : 0) + (index >= 9 ? 1 : 0) : 0);
30
30
  }
31
31
 
32
- /**
33
- * Hairline frame around each 3x3 block (subgrid): inset shadows on the block's edge cells, so cells stay
34
- * flush with their neighbours and only the blocks are outlined.
35
- */
36
- function blockFrame(row, col, split) {
37
- if (!split) return '';
38
- const shadows = [];
39
- if (row === 0 || row === 5 || row === 9) shadows.push(`inset 0 var(--irongrid-border-width) 0 var(--irongrid-color-border)`);
40
- if (row === 4 || row === 8 || row === 12) shadows.push(`inset 0 calc(-1 * var(--irongrid-border-width)) 0 var(--irongrid-color-border)`);
41
- if (col === 0 || col === 5 || col === 9) shadows.push(`inset var(--irongrid-border-width) 0 0 var(--irongrid-color-border)`);
42
- if (col === 4 || col === 8 || col === 12) shadows.push(`inset calc(-1 * var(--irongrid-border-width)) 0 0 var(--irongrid-color-border)`);
43
- return shadows.length ? `box-shadow: ${shadows.join(', ')};` : '';
44
- }
45
-
46
32
  /** The grid cell under a point (or null), from the cells' own positions, so it works with any spacing. */
47
33
  function cellAtPoint(gridElement, x, y) {
48
34
  const cells = gridElement.querySelectorAll('[data-row]');
@@ -82,25 +68,52 @@
82
68
  return manager ? manager.getCellInfo(rowIndex, colIndex) : { hand: '', display: '', type: '' };
83
69
  }
84
70
 
85
- function getGradient(hand) {
86
- return manager ? manager.getGradient(hand, hands, deadCards) : `linear-gradient(to right, ${defaultColor})`;
71
+ /**
72
+ * How a cell looks: in range (combos colored or selected), the range color, lighter when only some of the
73
+ * hand's live combos are in the range; out of range, a faint suited/offsuit/pair tint. Selected colored
74
+ * combos get a ring.
75
+ */
76
+ /** Out-of-range tints (theme variables, with defaults). */
77
+ const TINTS = {
78
+ suited: 'var(--irongrid-cell-suited-bg, #eef0f9)',
79
+ offsuit: 'var(--irongrid-cell-offsuit-bg, #f6f3ee)',
80
+ paired: 'var(--irongrid-cell-paired-bg, #f1f2f4)'
81
+ };
82
+
83
+ function cellLook(hand, type, handsMap, dead) {
84
+ const combos = getCombinations(hand);
85
+ const live = combos.filter(c => !dead.includes(c.substring(0, 2)) && !dead.includes(c.substring(2, 4)));
86
+ const counts = new Map();
87
+ let inRange = 0;
88
+ let selected = 0;
89
+ let coloredSelected = 0;
90
+ live.forEach(c => {
91
+ const values = handsMap[c] || [];
92
+ const color = manager ? manager.colorOf(values) : null;
93
+ const isSelected = values.includes('selected');
94
+ if (!color && !isSelected) return;
95
+ inRange++;
96
+ if (isSelected) selected++;
97
+ if (color && isSelected) coloredSelected++;
98
+ const key = color || defaultSelectedColor;
99
+ counts.set(key, (counts.get(key) || 0) + 1);
100
+ });
101
+ if (live.length === 0) return { background: TINTS[type], inRange: false, dead: true, ring: '' };
102
+ if (inRange === 0) return { background: TINTS[type], inRange: false, dead: false, ring: '' };
103
+ let dominant = null;
104
+ counts.forEach((n, color) => { if (!dominant || n > counts.get(dominant)) dominant = color; });
105
+ const strength = Math.round(45 + 55 * (inRange / live.length));
106
+ const ring = coloredSelected === 0 ? '' : selected === inRange ? 'full' : 'partial';
107
+ return { background: `color-mix(in srgb, ${dominant} ${strength}%, white)`, inRange: true, dead: false, ring };
87
108
  }
88
- $: handsLength = Object.keys(hands).length;
89
- $: handsString = JSON.stringify(hands);
90
- $: gradients = (() => {
109
+
110
+ $: looks = (() => {
91
111
  const result = {};
92
- try {
93
- const totalKeys = Object.keys(hands || {}).length;
94
- const selected = Object.keys(hands || {}).filter(k => Array.isArray(hands[k]) && hands[k].includes('selected')).length;
95
- void totalKeys;
96
- void selected;
97
- } catch (e) {
98
- // Ignore gradient recompute errors
99
- }
100
- ranks.forEach((rowRank, rowIndex) => {
101
- ranks.forEach((colRank, colIndex) => {
112
+ const dead = deadCards || [];
113
+ ranks.forEach((_, rowIndex) => {
114
+ ranks.forEach((__, colIndex) => {
102
115
  const cellInfo = getCellInfo(rowIndex, colIndex);
103
- result[cellInfo.hand] = getGradient(cellInfo.hand);
116
+ result[cellInfo.hand] = cellLook(cellInfo.hand, cellInfo.type, hands || {}, dead);
104
117
  });
105
118
  });
106
119
  return result;
@@ -445,7 +458,11 @@
445
458
  <button
446
459
  type="button"
447
460
  class="grid-cell {isDragging ? 'dragging' : ''} {interactive ? '' : showTips ? 'non-interactive show-tips' : 'non-interactive'}"
448
- style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)}; {blockFrame(rowIndex, colIndex, subgrid)}"
461
+ class:in-range={looks[cellInfo.hand].inRange}
462
+ class:dead={looks[cellInfo.hand].dead}
463
+ class:ring-full={looks[cellInfo.hand].ring === 'full'}
464
+ class:ring-partial={looks[cellInfo.hand].ring === 'partial'}
465
+ style="background: {looks[cellInfo.hand].background} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
449
466
  data-row={rowIndex}
450
467
  data-col={colIndex}
451
468
  onclick={(e) => handleClick(e, cellInfo)}
@@ -455,18 +472,20 @@
455
472
  onkeydown={(e) => handleKeyDown(e, cellInfo)}
456
473
  title={cellInfo.display}
457
474
  >
458
- <span class="cell-text">{cellInfo.display}</span>
475
+ <span class="cell-text">{cellInfo.hand.substring(0, 2)}</span>
459
476
  </button>
460
477
  {:else}
461
478
  <div
462
479
  class="grid-cell non-interactive"
463
- style="background: {gradients[cellInfo.hand]} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)}; {blockFrame(rowIndex, colIndex, subgrid)}"
480
+ class:in-range={looks[cellInfo.hand].inRange}
481
+ class:dead={looks[cellInfo.hand].dead}
482
+ style="background: {looks[cellInfo.hand].background} !important; grid-row: {gridLine(rowIndex, subgrid)}; grid-column: {gridLine(colIndex, subgrid)};"
464
483
  data-row={rowIndex}
465
484
  data-col={colIndex}
466
485
  role="presentation"
467
486
  title={cellInfo.display}
468
487
  >
469
- <span class="cell-text">{cellInfo.display}</span>
488
+ <span class="cell-text">{cellInfo.hand.substring(0, 2)}</span>
470
489
  </div>
471
490
  {/if}
472
491
  {/each}
@@ -496,33 +515,29 @@
496
515
  .poker-grid {
497
516
  display: grid;
498
517
  grid-template-columns: repeat(13, 1fr);
499
- grid-template-rows: repeat(13, 1fr);
500
- gap: 0;
501
- border: var(--irongrid-border-width) solid var(--irongrid-color-border);
518
+ grid-template-rows: repeat(13, auto);
519
+ gap: var(--irongrid-cell-gap, 2px);
502
520
  }
503
521
  /* 3x3 blocks: spacer tracks after T and 6 (A-T | 9-6 | 5-2) */
504
522
  .poker-grid.subgrid {
505
- grid-template-columns: repeat(5, 1fr) var(--irongrid-subgrid-gap, 6px) repeat(4, 1fr) var(--irongrid-subgrid-gap, 6px) repeat(4, 1fr);
506
- grid-template-rows: repeat(5, auto) var(--irongrid-subgrid-gap, 6px) repeat(4, auto) var(--irongrid-subgrid-gap, 6px) repeat(4, auto);
507
- border: none;
523
+ grid-template-columns: repeat(5, 1fr) var(--irongrid-subgrid-gap, 4px) repeat(4, 1fr) var(--irongrid-subgrid-gap, 4px) repeat(4, 1fr);
524
+ grid-template-rows: repeat(5, auto) var(--irongrid-subgrid-gap, 4px) repeat(4, auto) var(--irongrid-subgrid-gap, 4px) repeat(4, auto);
508
525
  }
509
526
  .grid-cell {
510
527
  aspect-ratio: 1;
528
+ min-width: 0;
511
529
  display: flex;
512
530
  align-items: center;
513
531
  justify-content: center;
514
532
  cursor: pointer;
515
- transition: opacity var(--irongrid-transition-fast);
516
- min-height: var(--irongrid-grid-min-cell-height);
517
- border-right: var(--irongrid-border-width) solid var(--irongrid-color-border);
518
- border-bottom: var(--irongrid-border-width) solid var(--irongrid-color-border);
533
+ border-radius: var(--irongrid-cell-radius, 3px);
534
+ transition: background-color var(--irongrid-transition-fast), box-shadow var(--irongrid-transition-fast);
519
535
  user-select: none;
520
536
  -webkit-user-select: none;
521
537
  -moz-user-select: none;
522
538
  -ms-user-select: none;
523
539
  }
524
540
  button.grid-cell {
525
- background: none;
526
541
  border: none;
527
542
  padding: 0;
528
543
  margin: 0;
@@ -530,11 +545,14 @@
530
545
  color: inherit;
531
546
  text-align: inherit;
532
547
  }
533
- .grid-cell:nth-child(13n) {
534
- border-right: none;
548
+ .grid-cell.ring-full {
549
+ box-shadow: inset 0 0 0 2px var(--irongrid-cell-ring-color, #1f2937);
535
550
  }
536
- .grid-cell:nth-child(n + 157) {
537
- border-bottom: none;
551
+ .grid-cell.ring-partial {
552
+ box-shadow: inset 0 0 0 1px var(--irongrid-cell-ring-color, #1f2937);
553
+ }
554
+ .grid-cell.dead {
555
+ opacity: 0.45;
538
556
  }
539
557
  .grid-cell.dragging {
540
558
  cursor: crosshair;
@@ -548,48 +566,22 @@
548
566
  pointer-events: auto;
549
567
  }
550
568
  .cell-text {
551
- font-size: var(--irongrid-font-size-base);
552
- font-weight: var(--irongrid-font-weight-semibold);
553
- color: var(--irongrid-color-text-primary);
569
+ font-size: var(--irongrid-cell-font-size, 0.8125rem);
570
+ font-weight: 500;
571
+ letter-spacing: 0.01em;
572
+ color: var(--irongrid-cell-muted-text, #a3a8b3);
554
573
  pointer-events: none;
555
574
  text-rendering: optimizeLegibility;
556
575
  -webkit-font-smoothing: antialiased;
557
576
  -moz-osx-font-smoothing: grayscale;
558
577
  }
559
- @media (max-width: 768px) {
560
- .poker-grid-container {
561
- padding: var(--irongrid-spacing-sm);
562
- }
563
- .poker-grid {
564
- gap: 0;
565
- }
566
- .grid-cell {
567
- min-height: var(--irongrid-grid-min-cell-height-tablet);
568
- }
569
- .cell-text {
570
- font-size: var(--irongrid-font-size-sm);
571
- }
578
+ .grid-cell.in-range .cell-text {
579
+ font-weight: 700;
580
+ color: var(--irongrid-color-text-primary);
572
581
  }
573
582
  @media (max-width: 480px) {
574
- .poker-grid-container {
575
- padding: var(--irongrid-grid-container-padding);
576
- }
577
- .grid-cell {
578
- min-height: var(--irongrid-grid-min-cell-height-mobile);
579
- }
580
- .cell-text {
581
- font-size: var(--irongrid-font-size-xxs);
582
- }
583
- }
584
- @media (max-width: 375px) {
585
- .poker-grid-container {
586
- padding: var(--irongrid-grid-container-padding-small);
587
- }
588
- .grid-cell {
589
- min-height: var(--irongrid-grid-min-cell-height-small);
590
- }
591
583
  .cell-text {
592
- font-size: var(--irongrid-font-size-xs);
584
+ font-size: var(--irongrid-cell-font-size-mobile, 0.6875rem);
593
585
  }
594
586
  }
595
587