@spotlab/hand-matrix 0.4.0 → 0.5.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
@@ -5,13 +5,13 @@ and [`@spotlab/range`](../range) — no Tailwind, no bundler-plugin
5
5
  requirement.
6
6
 
7
7
  ```tsx
8
- import { RangeGrid } from "@spotlab/hand-matrix";
8
+ import { RangeMatrix, rangeToMatrix } from "@spotlab/hand-matrix";
9
9
  import "@spotlab/hand-matrix/style.css";
10
10
  import { initRange } from "@spotlab/range";
11
11
 
12
12
  await initRange(); // once, at startup
13
13
 
14
- <RangeGrid range="AKs:0.5,QQ" />;
14
+ <RangeMatrix {...rangeToMatrix("AKs:0.5,QQ")} />;
15
15
  ```
16
16
 
17
17
  ## Exports
@@ -20,31 +20,33 @@ Everything below is exported from the package root.
20
20
 
21
21
  ### Display
22
22
 
23
- - `HandMatrix` — the bare 13x13 grid primitive. Takes no range data; styling
24
- is entirely the caller's job via `comboStyle`, keyed by combo string (e.g.
25
- `"AKs"`). Each cell carries a `data-combo` attribute. Props:
26
- - `comboStyle?: (combo) => CSSProperties` — per-cell style.
27
- - `onPointerDown?`, `onPointerEnter?: CellPointerHandler` — the
28
- `(combo, event)` handlers to use for dragging across cells, with a mouse
29
- or a finger. `onPointerDown` releases the implicit pointer capture a
30
- touch takes on its first cell. `onPointerEnter` fires once each time the
31
- pointer moves onto a new cell, found by hit-testing each `pointermove`
32
- (`elementFromPoint`), so it also fires during a touch drag, where
33
- enter events never reach the other cells. Passing `onPointerDown` sets
34
- `touch-action: none` and `user-select: none` on the grid, so a drag
35
- neither scrolls the page nor selects the combo text it crosses.
36
- - `className?: string` — merged onto the `.spotlab-hm-root` wrapper.
37
- - `RangeGrid` — a read-only display component. Accepts the expanded
38
- `HandFrequency[]` form or a range string directly (parsed via
39
- `@spotlab/range`), and fills each cell to its combo's mean weight. Props:
40
- `range: HandFrequency[] | string`, `fill?: string` (a CSS color, defaulting
41
- to `var(--spotlab-hm-fill)`), `className?: string`.
42
- - `cellFillGradient(stops: string[]): string` / `CELL_FILL_DIRECTION` — the
43
- canonical left-to-right gradient helper used to render a cell's fill.
44
- Returns `"transparent"` for no stops.
45
-
46
- `RangeGrid`'s combo collapse (`toComboWeights`) is a wasm call, so
47
- `initRange()` must have resolved before rendering it — see
23
+ - `RangeMatrix` — the read-only grid, for one range or several actions at
24
+ once, drawn exactly as `RangeMatrixEditor` draws it but without painting.
25
+ Each cell stacks its actions' weights as colored bands, dead cells
26
+ (`avail` 0) are hatched, and capped cells (`avail` below 1) show a cap
27
+ line with the rest hatched. Each cell carries a `data-combo` attribute.
28
+ Props (`RangeMatrixProps`):
29
+ - `cells: EditorCell[]` — one per combo, in `@spotlab/range`'s `COMBOS`
30
+ order (see [Editing](#editing)).
31
+ - `actions: EditorActionSpec[]` — index-aligned with each cell's
32
+ `weights`.
33
+ - `order: number[]` — indices into `actions`, in the order bands stack.
34
+ An action left out isn't drawn, so its share of a cell stays
35
+ transparent; include it to give it a color.
36
+ - `className?: string`.
37
+ - `rangeToMatrix(range, { color?, labels? }): RangeMatrixProps` (without
38
+ `className`) — `RangeMatrix`'s props for drawing one range:
39
+ `<RangeMatrix {...rangeToMatrix(range)} />`. Takes a range string (parsed
40
+ via `@spotlab/range`) or the expanded `HandFrequency[]` form, and fills
41
+ each cell to its combo's mean weight in `color` (default
42
+ `var(--spotlab-hm-fill)`), leaving the rest blank. The returned `actions`
43
+ are `in` and `out`, labeled by `labels.in` (default `"In range"`) and
44
+ `labels.out` (default `"Out of range"`), for building your own legend.
45
+ Memoize the result (e.g. `useMemo`) rather than calling it on every
46
+ render.
47
+
48
+ `rangeToMatrix`'s parse and combo collapse (`toComboWeights`) are wasm
49
+ calls, so `initRange()` must have resolved before calling it — see
48
50
  [`@spotlab/range`](../range)'s own README for why that call is explicit
49
51
  rather than automatic.
50
52
 
@@ -55,9 +57,8 @@ raise). Its state is an array of 169 `EditorCell`s, one per combo in
55
57
  `@spotlab/range`'s `COMBOS` order. Every component is fully controlled: state
56
58
  goes in as props and changes come out through `on*Change` callbacks.
57
59
 
58
- - `RangeMatrixEditor` — the paintable grid. Draws each cell's action bands,
59
- hatches dead cells (`avail` 0), marks capped cells (`avail` below 1), and
60
- wires brush/eyedropper painting onto `HandMatrix` for mouse and touch.
60
+ - `RangeMatrixEditor` — the paintable grid: `RangeMatrix`'s cells with
61
+ brush/eyedropper painting wired on for mouse and touch.
61
62
  Props (`RangeMatrixEditorProps`):
62
63
  - `cells: EditorCell[]` and `onCellsChange(cells)`.
63
64
  - `actions: EditorActionSpec[]` — index-aligned with each cell's
@@ -85,9 +86,9 @@ goes in as props and changes come out through `on*Change` callbacks.
85
86
  - `useRangePainting(opts)` — the brush/eyedropper state machine behind
86
87
  `RangeMatrixEditor`, for building your own grid. `opts` takes the editor's
87
88
  `cells`, `brush`, `tool` and their three `on*Change` callbacks. Returns
88
- `{ onCellDown(combo), onCellEnter(combo) }`; wire them to `HandMatrix`'s
89
- `onPointerDown`/`onPointerEnter`. A stroke ends on any window `pointerup`
90
- or `pointercancel`.
89
+ `{ onCellDown(combo), onCellEnter(combo) }`: call `onCellDown` when a
90
+ pointer goes down on a cell and `onCellEnter` each time it moves onto
91
+ another one. A stroke ends on any window `pointerup` or `pointercancel`.
91
92
  - `seedCells(avail, actionWeights, passive): EditorCell[]` — build the
92
93
  initial grid from a parent range's per-combo availability (`null` for
93
94
  unconstrained) and each action's absolute per-combo weights (the
@@ -115,7 +116,7 @@ goes in as props and changes come out through `on*Change` callbacks.
115
116
  is the combo's cap in the parent range and `weights` are each action's
116
117
  share of it, summing to 100), `EditorActionSpec`
117
118
  (`{ id: string; label: string; color: string }`), `PaintTool`,
118
- `CellPointerHandler`, `RangeMatrixEditorProps`, `ActionWeightSlidersProps`,
119
+ `RangeMatrixEditorProps`, `ActionWeightSlidersProps`,
119
120
  `WeightInputProps`.
120
121
 
121
122
  **The editor works at combo level.** A cell is one of the 169 combos (`AKs`,
@@ -133,7 +134,7 @@ custom property has a default declared on `.spotlab-hm-root`, so the
133
134
  component renders correctly with zero theming.
134
135
 
135
136
  Because the `className` you pass to a component lands on that same root, a
136
- class that sets variables works there too (`<RangeGrid className="my-theme" />`
137
+ class that sets variables works there too (`<RangeMatrix className="my-theme" … />`
137
138
  with `.my-theme { --spotlab-hm-border: … }`), in addition to the
138
139
  `.spotlab-hm-root` selector form below.
139
140
 
@@ -195,9 +196,10 @@ Available variables: `--spotlab-hm-border`, `--spotlab-hm-border-width`,
195
196
  `--spotlab-hm-muted-bg`, `--spotlab-hm-muted-fg`, `--spotlab-hm-track-bg`,
196
197
  `--spotlab-hm-row-passive-bg`, `--spotlab-hm-dead-hatch`.
197
198
 
198
- Action/fill colors (`RangeGrid`'s `fill` prop) are **not** theme variables —
199
- they are data, passed as props. `--spotlab-hm-fill` is only the default used
200
- when `fill` is omitted.
199
+ Action colors (each `EditorActionSpec`'s `color`, or `rangeToMatrix`'s
200
+ `color` option) are **not** theme variables — they are data, passed as props.
201
+ `--spotlab-hm-fill` is only the default `rangeToMatrix` uses when `color` is
202
+ omitted.
201
203
 
202
204
  ## License
203
205
 
@@ -1,24 +1,17 @@
1
1
  import { FC } from 'react';
2
+ import { MatrixDisplayProps } from '../hand-matrix';
2
3
  import { PaintTool } from './use-range-painting';
3
4
  import { EditorCell } from './cells';
4
- import { EditorActionSpec } from './weights';
5
- export type RangeMatrixEditorProps = {
6
- /** The 169-cell grid, in `@spotlab/range`'s `COMBOS` order. */
7
- cells: EditorCell[];
8
- /** Index-aligned with each cell's `weights` array. */
9
- actions: EditorActionSpec[];
10
- /** Indices into `actions`/`weights`, in the order bands should stack — a display concern the consumer owns (e.g. biggest raise first). */
11
- order: number[];
5
+ export type RangeMatrixEditorProps = MatrixDisplayProps & {
12
6
  brush: number[];
13
7
  tool: PaintTool;
14
8
  onCellsChange: (cells: EditorCell[]) => void;
15
9
  onBrushChange: (brush: number[]) => void;
16
10
  onToolChange: (tool: PaintTool) => void;
17
- className?: string;
18
11
  };
19
12
  /**
20
- * The paintable 13x13 range grid: per-cell action bands, the dead-cell hatch,
21
- * the capped-cell cap marker, and the brush/eyedropper painting wired onto
22
- * `HandMatrix`. Fully controlled; `useRangePainting` drives the state changes.
13
+ * The paintable 13x13 range grid: `RangeMatrix`'s cells with brush/eyedropper
14
+ * painting wired onto them. Fully controlled; `useRangePainting` drives the
15
+ * state changes.
23
16
  */
24
17
  export declare const RangeMatrixEditor: FC<RangeMatrixEditorProps>;
@@ -1,10 +1,7 @@
1
1
  import { CSSProperties, FC, PointerEvent as ReactPointerEvent } from 'react';
2
- export declare const CELL_FILL_DIRECTION = "to right";
3
- export declare const cellFillGradient: (stops: string[]) => string;
4
- export type CellPointerHandler = (combo: string, event: ReactPointerEvent<HTMLDivElement>) => void;
5
- type ComboStyleProp = {
6
- comboStyle?: (combo: string) => CSSProperties;
7
- };
2
+ import { EditorCell } from './editor/cells';
3
+ import { EditorActionSpec } from './editor/weights';
4
+ type CellPointerHandler = (combo: string, event: ReactPointerEvent<HTMLDivElement>) => void;
8
5
  type GridPointerHandlers = {
9
6
  /**
10
7
  * A pointer (mouse, pen or touch) went down on `combo`'s cell. The grid
@@ -22,12 +19,29 @@ type GridPointerHandlers = {
22
19
  */
23
20
  onPointerEnter?: CellPointerHandler;
24
21
  };
22
+ /** The cell data both `RangeMatrix` and `RangeMatrixEditor` draw from. */
23
+ export type MatrixDisplayProps = {
24
+ /** The 169-cell grid, in `@spotlab/range`'s `COMBOS` order. */
25
+ cells: EditorCell[];
26
+ /** Index-aligned with each cell's `weights` array. */
27
+ actions: EditorActionSpec[];
28
+ /**
29
+ * Indices into `actions`/`weights`, in the order bands should stack. An
30
+ * action left out isn't drawn, so its share of a cell stays transparent.
31
+ */
32
+ order: number[];
33
+ className?: string;
34
+ };
25
35
  /**
26
- * The bare 13x13 grid. Each cell carries a `data-combo` attribute. Passing
27
- * `onPointerDown` marks the grid as a drag surface (`dragSurfaceStyle`): a
28
- * drag reaches the handlers instead of scrolling the page or selecting text.
36
+ * The 13x13 grid that `RangeMatrix` and `RangeMatrixEditor` both render, so
37
+ * the two draw cells identically: each cell stacks its actions' weights as
38
+ * bands in `order`, with dead and capped cells marked. Each cell carries a
39
+ * `data-combo` attribute. The editor adds a per-cell cursor and the painting
40
+ * handlers; passing `onPointerDown` also makes the grid a drag surface
41
+ * (`dragSurfaceStyle`), so a drag reaches the handlers instead of scrolling
42
+ * the page or selecting text. Internal: not exported from the package root.
29
43
  */
30
- export declare const HandMatrix: FC<{
31
- className?: string;
32
- } & ComboStyleProp & GridPointerHandlers>;
44
+ export declare const HandMatrix: FC<MatrixDisplayProps & GridPointerHandlers & {
45
+ cursorOf?: (cell: EditorCell) => CSSProperties["cursor"];
46
+ }>;
33
47
  export {};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- export { HandMatrix, cellFillGradient, CELL_FILL_DIRECTION, type CellPointerHandler, } from './hand-matrix';
2
- export { RangeGrid } from './range-grid';
1
+ export { RangeMatrix, type RangeMatrixProps } from './range-matrix';
2
+ export { rangeToMatrix } from './range-to-matrix';
3
3
  export { RangeMatrixEditor, type RangeMatrixEditorProps, } from './editor/matrix-editor';
4
4
  export { ActionWeightSliders, type ActionWeightSlidersProps, type WeightInputProps, } from './editor/action-sliders';
5
5
  export { useRangePainting, type PaintTool } from './editor/use-range-painting';
package/dist/index.js CHANGED
@@ -1,364 +1,368 @@
1
- import { useCallback as M, useEffect as x, useLayoutEffect as P, useMemo as T, useRef as $ } from "react";
2
- import { COMBOS as C, comboOf as W, handsOfCombo as D, parseRange as F, toComboWeights as B } from "@spotlab/range";
3
- import { jsx as g, jsxs as y } from "react/jsx-runtime";
4
- var X = (t, e) => Array.from({ length: Math.ceil(t.length / e) }, (n, r) => t.slice(r * e, r * e + e)), _ = "to right", L = (t) => t.length === 0 ? "transparent" : `linear-gradient(${_}, ${t.join(", ")})`, j = (t) => {
5
- const e = t.target;
6
- e.hasPointerCapture(t.pointerId) && e.releasePointerCapture(t.pointerId);
7
- }, E = (t, e, n) => {
8
- const r = t.ownerDocument.elementFromPoint(e, n)?.closest("[data-combo]");
9
- return !r || !t.contains(r) ? null : r.getAttribute("data-combo");
10
- }, q = ({ combo: t, onPointerDown: e, comboStyle: n }) => /* @__PURE__ */ g("div", {
11
- className: "spotlab-hm-cell",
12
- "data-combo": t,
13
- style: n?.(t),
14
- onPointerDown: e && ((r) => {
15
- j(r), e(t, r);
16
- }),
17
- children: /* @__PURE__ */ g("div", {
18
- className: "spotlab-hm-cell-inner",
19
- children: /* @__PURE__ */ g("div", {
20
- "data-combo": t,
21
- children: t
22
- })
23
- })
24
- }), G = ({ row: t, ...e }) => /* @__PURE__ */ g("div", {
25
- className: "spotlab-hm-row",
26
- children: t.map((n, r) => /* @__PURE__ */ g(q, {
27
- combo: n,
28
- ...e
29
- }, r))
30
- }), U = {
31
- touchAction: "none",
32
- userSelect: "none",
33
- WebkitUserSelect: "none",
34
- WebkitTouchCallout: "none"
35
- }, S = ({ className: t, onPointerDown: e, onPointerEnter: n, ...r }) => {
36
- const a = $(null), o = (c) => {
37
- const s = E(c.currentTarget, c.clientX, c.clientY);
38
- s !== a.current && (a.current = s, s && n?.(s, c));
39
- };
40
- return /* @__PURE__ */ g("div", {
41
- className: ["spotlab-hm-root", t].filter(Boolean).join(" "),
42
- style: e ? U : void 0,
43
- onPointerDown: n && ((c) => {
44
- a.current = E(c.currentTarget, c.clientX, c.clientY);
45
- }),
46
- onPointerMove: n && o,
47
- onPointerLeave: n && (() => a.current = null),
48
- children: X(C, 13).map((c, s) => /* @__PURE__ */ g(G, {
49
- row: c,
50
- onPointerDown: e,
51
- ...r
52
- }, s))
53
- });
54
- }, Y = (t, e) => ({ background: L([`${t} 0 ${e * 100}%`, `transparent ${e * 100}% 100%`]) }), ot = ({ range: t, fill: e = "var(--spotlab-hm-fill)", className: n }) => {
55
- const r = T(() => {
56
- const a = typeof t == "string" ? F(t) : t;
57
- return B(a);
58
- }, [t]);
59
- return /* @__PURE__ */ g(S, {
60
- className: n,
61
- comboStyle: (a) => {
62
- const o = r.get(a);
63
- return o ? Y(e, o) : {};
64
- }
65
- });
66
- }, H = (t) => Math.max(0, Math.min(100, Math.round(t))), Q = (t, e) => {
1
+ import { useCallback as M, useEffect as R, useLayoutEffect as L, useRef as y } from "react";
2
+ import { COMBOS as C, comboOf as P, handsOfCombo as T, parseRange as W, toComboWeights as D } from "@spotlab/range";
3
+ import { jsx as v, jsxs as E } from "react/jsx-runtime";
4
+ var B = (t) => Math.max(0, Math.min(100, Math.round(t))), F = (t, r) => {
67
5
  const n = t.map(Math.round);
68
- let r = 100 - n.reduce((a, o) => a + o, 0);
69
- if (r !== 0) {
70
- for (let a = 0; a < n.length; a++) if (a !== e && n[a] + r >= 0) {
71
- n[a] += r, r = 0;
6
+ let e = 100 - n.reduce((a, o) => a + o, 0);
7
+ if (e !== 0) {
8
+ for (let a = 0; a < n.length; a++) if (a !== r && n[a] + e >= 0) {
9
+ n[a] += e, e = 0;
72
10
  break;
73
11
  }
74
- if (r !== 0) {
75
- for (let a = 0; a < n.length; a++) if (a !== e && n[a] > 0) {
76
- const o = Math.min(n[a], -r);
77
- if (n[a] -= o, r += o, r === 0) break;
12
+ if (e !== 0) {
13
+ for (let a = 0; a < n.length; a++) if (a !== r && n[a] > 0) {
14
+ const o = Math.min(n[a], -e);
15
+ if (n[a] -= o, e += o, e === 0) break;
78
16
  }
79
- n[e] = Math.max(0, n[e] + r);
17
+ n[r] = Math.max(0, n[r] + e);
80
18
  }
81
19
  }
82
20
  return n;
83
- }, J = (t, e, n, r) => {
84
- const a = H(n), o = t.slice();
85
- o[e] = a;
86
- const c = 100 - a, s = [];
87
- for (let h = 0; h < t.length; h++) h !== e && s.push(h);
88
- const d = s.reduce((h, p) => h + t[p], 0);
89
- if (d <= 0) {
90
- s.forEach((p) => o[p] = 0);
91
- const h = e === r ? s[0] : r;
92
- h != null && (o[h] = c);
93
- } else s.forEach((h) => o[h] = Math.round(t[h] / d * c));
94
- return Q(o, e);
95
- }, N = (t, e) => t.reduce((n, r, a) => a === e ? n : n + r, 0), k = new Map(C.map((t, e) => [t, e])), K = (t, e, n) => {
96
- const r = t.slice();
97
- return r[k.get(e)] = n, r;
98
- }, I = (t, e) => {
21
+ }, X = (t, r, n, e) => {
22
+ const a = B(n), o = t.slice();
23
+ o[r] = a;
24
+ const c = 100 - a, i = [];
25
+ for (let l = 0; l < t.length; l++) l !== r && i.push(l);
26
+ const u = i.reduce((l, f) => l + t[f], 0);
27
+ if (u <= 0) {
28
+ i.forEach((f) => o[f] = 0);
29
+ const l = r === e ? i[0] : e;
30
+ l != null && (o[l] = c);
31
+ } else i.forEach((l) => o[l] = Math.round(t[l] / u * c));
32
+ return F(o, r);
33
+ }, N = (t, r) => t.reduce((n, e, a) => a === r ? n : n + e, 0), k = new Map(C.map((t, r) => [t, r])), j = (t, r, n) => {
34
+ const e = t.slice();
35
+ return e[k.get(r)] = n, e;
36
+ }, I = (t, r) => {
99
37
  const n = t.slice();
100
- let r = N(n, e);
101
- if (r > 100) {
102
- const a = 100 / r;
103
- for (let o = 0; o < n.length; o++) o !== e && (n[o] = Math.round(n[o] * a));
104
- r = N(n, e);
38
+ let e = N(n, r);
39
+ if (e > 100) {
40
+ const a = 100 / e;
41
+ for (let o = 0; o < n.length; o++) o !== r && (n[o] = Math.round(n[o] * a));
42
+ e = N(n, r);
105
43
  }
106
- return n[e] = Math.max(0, 100 - r), n;
44
+ return n[r] = Math.max(0, 100 - e), n;
107
45
  };
108
- function lt(t, e, n) {
109
- return C.map((r) => {
110
- const a = t ? t.get(r) ?? 0 : 1, o = e.map(() => 0);
46
+ function et(t, r, n) {
47
+ return C.map((e) => {
48
+ const a = t ? t.get(e) ?? 0 : 1, o = r.map(() => 0);
111
49
  return a <= 0 ? (o[n] = 100, {
112
50
  avail: 0,
113
51
  weights: o
114
- }) : (e.forEach((c, s) => {
115
- const d = c?.get(r) ?? 0;
116
- o[s] = Math.round(d / a * 100);
52
+ }) : (r.forEach((c, i) => {
53
+ const u = c?.get(e) ?? 0;
54
+ o[i] = Math.round(u / a * 100);
117
55
  }), {
118
56
  avail: a,
119
57
  weights: I(o, n)
120
58
  });
121
59
  });
122
60
  }
123
- var A = 8;
124
- function st(t, e) {
61
+ var S = 8;
62
+ function rt(t, r) {
125
63
  const n = /* @__PURE__ */ new Map();
126
- return t.forEach((r, a) => {
127
- if (r.avail <= 0) return;
128
- const o = r.weights.reduce((s, d) => s + d, 0);
64
+ return t.forEach((e, a) => {
65
+ if (e.avail <= 0) return;
66
+ const o = e.weights.reduce((i, u) => i + u, 0);
129
67
  if (!o) return;
130
- const c = Number((r.weights[e] / o * r.avail).toFixed(A));
68
+ const c = Number((e.weights[r] / o * e.avail).toFixed(S));
131
69
  c && n.set(C[a], c);
132
70
  }), n;
133
71
  }
134
- function ct(t, e, n) {
135
- return t.map((r, a) => {
136
- const o = e.map(() => 0);
137
- if (r.avail <= 0)
72
+ function nt(t, r, n) {
73
+ return t.map((e, a) => {
74
+ const o = r.map(() => 0);
75
+ if (e.avail <= 0)
138
76
  return o[n] = 100, {
139
- avail: r.avail,
77
+ avail: e.avail,
140
78
  weights: o
141
79
  };
142
80
  const c = C[a];
143
- return e.forEach((s, d) => {
144
- const h = s?.get(c) ?? 0;
145
- o[d] = Math.round(h / r.avail * 100);
81
+ return r.forEach((i, u) => {
82
+ const l = i?.get(c) ?? 0;
83
+ o[u] = Math.round(l / e.avail * 100);
146
84
  }), {
147
- avail: r.avail,
85
+ avail: e.avail,
148
86
  weights: I(o, n)
149
87
  };
150
88
  });
151
89
  }
152
- var V = (t, e) => t.avail !== e.avail || t.weights.length !== e.weights.length || t.weights.some((n, r) => n !== e.weights[r]);
153
- function it(t, e, n, r) {
154
- const a = r && new Map(r.map(({ hand: l, frequency: i }) => [l, i])), o = (l, i) => r === void 0 ? i.avail : a ? a.get(l) ?? 0 : 1, c = t.map((l) => l && new Map(l.map(({ hand: i, frequency: f }) => [i, f]))), s = (l) => c.reduce((i, f) => i + (f?.get(l) ?? 0), 0), d = t.flatMap((l, i) => l ? [] : [i]), h = (l, i) => D(l).flatMap((f) => {
155
- const b = Number(i(f).toFixed(A));
90
+ var q = (t, r) => t.avail !== r.avail || t.weights.length !== r.weights.length || t.weights.some((n, e) => n !== r.weights[e]);
91
+ function at(t, r, n, e) {
92
+ const a = e && new Map(e.map(({ hand: s, frequency: h }) => [s, h])), o = (s, h) => e === void 0 ? h.avail : a ? a.get(s) ?? 0 : 1, c = t.map((s) => s && new Map(s.map(({ hand: h, frequency: g }) => [h, g]))), i = (s) => c.reduce((h, g) => h + (g?.get(s) ?? 0), 0), u = t.flatMap((s, h) => s ? [] : [h]), l = (s, h) => T(s).flatMap((g) => {
93
+ const b = Number(h(g).toFixed(S));
156
94
  return b > 0 ? [{
157
- hand: f,
95
+ hand: g,
158
96
  frequency: b
159
97
  }] : [];
160
- }), p = (l, i) => {
161
- const f = n[k.get(l)], b = f.weights.reduce((v, w) => v + w, 0);
162
- return f.avail <= 0 || !b ? [] : h(l, (v) => f.weights[i] / b * o(v, f));
163
- }, m = (l, i) => {
164
- const f = n[k.get(l)], b = d.reduce((w, O) => w + f.weights[O], 0), v = b ? f.weights[i] / b : 1 / d.length;
165
- return h(l, (w) => Math.max(0, o(w, f) - s(w)) * v);
166
- }, u = new Set(C.filter((l, i) => V(e[i], n[i])));
167
- return t.map((l, i) => {
168
- if (l === null) return C.flatMap((v) => u.has(v) ? p(v, i) : m(v, i));
169
- const f = [], b = /* @__PURE__ */ new Set();
170
- for (const v of l) {
171
- const w = W(v.hand);
172
- u.has(w) ? b.has(w) || (b.add(w), f.push(...p(w, i))) : f.push(v);
98
+ }), f = (s, h) => {
99
+ const g = n[k.get(s)], b = g.weights.reduce((m, w) => m + w, 0);
100
+ return g.avail <= 0 || !b ? [] : l(s, (m) => g.weights[h] / b * o(m, g));
101
+ }, p = (s, h) => {
102
+ const g = n[k.get(s)], b = u.reduce((w, O) => w + g.weights[O], 0), m = b ? g.weights[h] / b : 1 / u.length;
103
+ return l(s, (w) => Math.max(0, o(w, g) - i(w)) * m);
104
+ }, d = new Set(C.filter((s, h) => q(r[h], n[h])));
105
+ return t.map((s, h) => {
106
+ if (s === null) return C.flatMap((m) => d.has(m) ? f(m, h) : p(m, h));
107
+ const g = [], b = /* @__PURE__ */ new Set();
108
+ for (const m of s) {
109
+ const w = P(m.hand);
110
+ d.has(w) ? b.has(w) || (b.add(w), g.push(...f(w, h))) : g.push(m);
173
111
  }
174
- for (const v of u) b.has(v) || f.push(...p(v, i));
175
- return f;
112
+ for (const m of d) b.has(m) || g.push(...f(m, h));
113
+ return g;
114
+ });
115
+ }
116
+ var _ = (t, r) => Array.from({ length: Math.ceil(t.length / r) }, (n, e) => t.slice(e * r, e * r + r)), U = (t) => `linear-gradient(to right, ${t.join(", ")})`, $ = (t, r, n) => {
117
+ const e = [];
118
+ let a = 0;
119
+ return n.forEach((o) => {
120
+ const c = (t.weights[o] || 0) / 100;
121
+ if (c <= 0) return;
122
+ const i = r[o].color;
123
+ e.push(`${i} ${a * 100}%`), e.push(`${i} ${(a + c) * 100}%`), a += c;
124
+ }), e.length > 0 && a < 1 - 1e-9 && e.push(`transparent ${a * 100}% 100%`), e.length > 0 ? U(e) : "none";
125
+ }, Y = (t, r, n) => {
126
+ if (t.avail <= 0) return {
127
+ backgroundImage: "var(--spotlab-hm-dead-hatch)",
128
+ backgroundColor: "var(--spotlab-hm-muted-bg)",
129
+ color: "var(--spotlab-hm-muted-fg)"
130
+ };
131
+ if (t.avail < 1) {
132
+ const e = `${t.avail * 100}%`;
133
+ return {
134
+ backgroundImage: `repeating-linear-gradient(180deg, var(--spotlab-hm-muted-fg) 0 4px, transparent 4px 8px), var(--spotlab-hm-dead-hatch), ${$(t, r, n)}`,
135
+ backgroundSize: `1px 100%, ${(1 - t.avail) * 100}% 100%, ${t.avail * 100}% 100%`,
136
+ backgroundPosition: `${e} center, right center, left center`,
137
+ backgroundRepeat: "no-repeat, no-repeat, no-repeat"
138
+ };
139
+ }
140
+ return { backgroundImage: $(t, r, n) };
141
+ }, G = (t) => {
142
+ const r = t.target;
143
+ r.hasPointerCapture(t.pointerId) && r.releasePointerCapture(t.pointerId);
144
+ }, x = (t, r, n) => {
145
+ const e = t.ownerDocument.elementFromPoint(r, n)?.closest("[data-combo]");
146
+ return !e || !t.contains(e) ? null : e.getAttribute("data-combo");
147
+ }, H = {
148
+ touchAction: "none",
149
+ userSelect: "none",
150
+ WebkitUserSelect: "none",
151
+ WebkitTouchCallout: "none"
152
+ }, A = ({ cells: t, actions: r, order: n, className: e, cursorOf: a, onPointerDown: o, onPointerEnter: c }) => {
153
+ const i = y(null), u = (l) => {
154
+ const f = x(l.currentTarget, l.clientX, l.clientY);
155
+ f !== i.current && (i.current = f, f && c?.(f, l));
156
+ };
157
+ return /* @__PURE__ */ v("div", {
158
+ className: ["spotlab-hm-root", e].filter(Boolean).join(" "),
159
+ style: o ? H : void 0,
160
+ onPointerDown: c && ((l) => {
161
+ i.current = x(l.currentTarget, l.clientX, l.clientY);
162
+ }),
163
+ onPointerMove: c && u,
164
+ onPointerLeave: c && (() => i.current = null),
165
+ children: _(C, 13).map((l, f) => /* @__PURE__ */ v("div", {
166
+ className: "spotlab-hm-row",
167
+ children: l.map((p) => {
168
+ const d = t[k.get(p)];
169
+ return /* @__PURE__ */ v("div", {
170
+ className: "spotlab-hm-cell",
171
+ "data-combo": p,
172
+ style: {
173
+ ...Y(d, r, n),
174
+ cursor: a?.(d)
175
+ },
176
+ onPointerDown: o && ((s) => {
177
+ G(s), o(p, s);
178
+ }),
179
+ children: /* @__PURE__ */ v("div", {
180
+ className: "spotlab-hm-cell-inner",
181
+ children: /* @__PURE__ */ v("div", {
182
+ "data-combo": p,
183
+ children: p
184
+ })
185
+ })
186
+ }, p);
187
+ })
188
+ }, f))
176
189
  });
190
+ }, ot = A;
191
+ function lt(t, { color: r = "var(--spotlab-hm-fill)", labels: n = {} } = {}) {
192
+ const e = D(typeof t == "string" ? W(t) : t);
193
+ return {
194
+ cells: C.map((a) => {
195
+ const o = (e.get(a) ?? 0) * 100;
196
+ return {
197
+ avail: 1,
198
+ weights: [o, 100 - o]
199
+ };
200
+ }),
201
+ actions: [{
202
+ id: "in",
203
+ label: n.in ?? "In range",
204
+ color: r
205
+ }, {
206
+ id: "out",
207
+ label: n.out ?? "Out of range",
208
+ color: "transparent"
209
+ }],
210
+ order: [0]
211
+ };
177
212
  }
178
- function Z(t) {
179
- const { cells: e, brush: n, tool: r, onCellsChange: a, onBrushChange: o, onToolChange: c } = t, s = $(null), d = M(() => {
180
- s.current?.abort(), s.current = null;
213
+ function Q(t) {
214
+ const { cells: r, brush: n, tool: e, onCellsChange: a, onBrushChange: o, onToolChange: c } = t, i = y(null), u = M(() => {
215
+ i.current?.abort(), i.current = null;
181
216
  }, []);
182
- x(() => d, [d]);
183
- const h = $(e);
184
- P(() => {
185
- h.current = e;
186
- }, [e]);
187
- const p = M((u) => {
188
- const l = h.current, { avail: i } = l[k.get(u)];
189
- if (i <= 0) return;
190
- const f = K(l, u, {
191
- avail: i,
217
+ R(() => u, [u]);
218
+ const l = y(r);
219
+ L(() => {
220
+ l.current = r;
221
+ }, [r]);
222
+ const f = M((d) => {
223
+ const s = l.current, { avail: h } = s[k.get(d)];
224
+ if (h <= 0) return;
225
+ const g = j(s, d, {
226
+ avail: h,
192
227
  weights: [...n]
193
228
  });
194
- h.current = f, a(f);
195
- }, [n, a]), m = M((u) => {
196
- const l = k.get(u);
197
- e[l].avail <= 0 || (o([...e[l].weights]), c("brush"));
229
+ l.current = g, a(g);
230
+ }, [n, a]), p = M((d) => {
231
+ const s = k.get(d);
232
+ r[s].avail <= 0 || (o([...r[s].weights]), c("brush"));
198
233
  }, [
199
- e,
234
+ r,
200
235
  o,
201
236
  c
202
237
  ]);
203
238
  return {
204
- onCellDown: M((u) => {
205
- if (r === "eyedropper") {
206
- m(u);
239
+ onCellDown: M((d) => {
240
+ if (e === "eyedropper") {
241
+ p(d);
207
242
  return;
208
243
  }
209
- d();
210
- const l = new AbortController();
211
- s.current = l, window.addEventListener("pointerup", d, { signal: l.signal }), window.addEventListener("pointercancel", d, { signal: l.signal }), p(u);
244
+ u();
245
+ const s = new AbortController();
246
+ i.current = s, window.addEventListener("pointerup", u, { signal: s.signal }), window.addEventListener("pointercancel", u, { signal: s.signal }), f(d);
212
247
  }, [
213
- r,
214
- m,
248
+ e,
215
249
  p,
216
- d
250
+ f,
251
+ u
217
252
  ]),
218
- onCellEnter: M((u) => {
219
- s.current && r === "brush" && p(u);
220
- }, [r, p])
253
+ onCellEnter: M((d) => {
254
+ i.current && e === "brush" && f(d);
255
+ }, [e, f])
221
256
  };
222
257
  }
223
- var R = (t, e, n) => {
224
- const r = [];
225
- let a = 0;
226
- return n.forEach((o) => {
227
- const c = (t.weights[o] || 0) / 100;
228
- if (c <= 0) return;
229
- const s = e[o].color;
230
- r.push(`${s} ${a * 100}%`), r.push(`${s} ${(a + c) * 100}%`), a += c;
231
- }), r.length > 0 && a < 1 - 1e-9 && r.push(`transparent ${a * 100}% 100%`), L(r);
232
- }, ut = ({ cells: t, actions: e, order: n, brush: r, tool: a, onCellsChange: o, onBrushChange: c, onToolChange: s, className: d }) => {
233
- const { onCellDown: h, onCellEnter: p } = Z({
258
+ var st = ({ cells: t, actions: r, order: n, brush: e, tool: a, onCellsChange: o, onBrushChange: c, onToolChange: i, className: u }) => {
259
+ const { onCellDown: l, onCellEnter: f } = Q({
234
260
  cells: t,
235
- brush: r,
261
+ brush: e,
236
262
  tool: a,
237
263
  onCellsChange: o,
238
264
  onBrushChange: c,
239
- onToolChange: s
265
+ onToolChange: i
240
266
  });
241
- return /* @__PURE__ */ g(S, {
242
- className: d,
243
- comboStyle: (m) => {
244
- const u = t[k.get(m)];
245
- if (u.avail <= 0) return {
246
- background: "var(--spotlab-hm-dead-hatch)",
247
- backgroundColor: "var(--spotlab-hm-muted-bg)",
248
- cursor: "not-allowed",
249
- color: "var(--spotlab-hm-muted-fg)"
250
- };
251
- const l = a === "eyedropper" ? "copy" : "pointer";
252
- if (u.avail < 1) {
253
- const i = `${u.avail * 100}%`;
254
- return {
255
- background: `repeating-linear-gradient(180deg, var(--spotlab-hm-muted-fg) 0 4px, transparent 4px 8px), var(--spotlab-hm-dead-hatch), ${R(u, e, n)}`,
256
- backgroundSize: `1px 100%, ${(1 - u.avail) * 100}% 100%, ${u.avail * 100}% 100%`,
257
- backgroundPosition: `${i} center, right center, left center`,
258
- backgroundRepeat: "no-repeat, no-repeat, no-repeat",
259
- cursor: l
260
- };
261
- }
262
- return {
263
- background: R(u, e, n),
264
- cursor: l
265
- };
266
- },
267
- onPointerDown: h,
268
- onPointerEnter: p
267
+ return /* @__PURE__ */ v(A, {
268
+ cells: t,
269
+ actions: r,
270
+ order: n,
271
+ className: u,
272
+ cursorOf: (p) => p.avail <= 0 ? "not-allowed" : a === "eyedropper" ? "copy" : "pointer",
273
+ onPointerDown: l,
274
+ onPointerEnter: f
269
275
  });
270
- }, z = (t) => /* @__PURE__ */ g("input", {
276
+ }, J = (t) => /* @__PURE__ */ v("input", {
271
277
  type: "number",
272
278
  className: "spotlab-hm-weight-input",
273
279
  ...t
274
- }), tt = ({ value: t, color: e, passive: n, onChange: r }) => {
275
- const a = $(null), o = $(!1), c = M((s) => {
276
- const d = a.current;
277
- if (!d) return;
278
- const h = d.getBoundingClientRect();
279
- r((s - h.left) / h.width * 100);
280
- }, [r]);
281
- return x(() => {
282
- const s = (h) => {
283
- o.current && c(h.clientX);
284
- }, d = () => o.current = !1;
285
- return window.addEventListener("pointermove", s), window.addEventListener("pointerup", d), () => {
286
- window.removeEventListener("pointermove", s), window.removeEventListener("pointerup", d);
280
+ }), K = ({ value: t, color: r, passive: n, onChange: e }) => {
281
+ const a = y(null), o = y(!1), c = M((i) => {
282
+ const u = a.current;
283
+ if (!u) return;
284
+ const l = u.getBoundingClientRect();
285
+ e((i - l.left) / l.width * 100);
286
+ }, [e]);
287
+ return R(() => {
288
+ const i = (l) => {
289
+ o.current && c(l.clientX);
290
+ }, u = () => o.current = !1;
291
+ return window.addEventListener("pointermove", i), window.addEventListener("pointerup", u), () => {
292
+ window.removeEventListener("pointermove", i), window.removeEventListener("pointerup", u);
287
293
  };
288
- }, [c]), /* @__PURE__ */ g("div", {
294
+ }, [c]), /* @__PURE__ */ v("div", {
289
295
  ref: a,
290
296
  role: "slider",
291
297
  "aria-valuenow": t,
292
298
  "aria-valuemin": 0,
293
299
  "aria-valuemax": 100,
294
300
  className: "spotlab-hm-slider-track",
295
- style: { background: `linear-gradient(to right, ${e} 0 ${t}%, var(--spotlab-hm-track-bg) ${t}% 100%)` },
296
- onPointerDown: (s) => {
297
- o.current = !0, c(s.clientX);
301
+ style: { background: `linear-gradient(to right, ${r} 0 ${t}%, var(--spotlab-hm-track-bg) ${t}% 100%)` },
302
+ onPointerDown: (i) => {
303
+ o.current = !0, c(i.clientX);
298
304
  },
299
- children: /* @__PURE__ */ g("div", {
305
+ children: /* @__PURE__ */ v("div", {
300
306
  className: "spotlab-hm-slider-thumb",
301
307
  style: {
302
308
  left: `${t}%`,
303
- border: `2px solid ${n ? "var(--spotlab-hm-muted-fg)" : e}`
309
+ border: `2px solid ${n ? "var(--spotlab-hm-muted-fg)" : r}`
304
310
  }
305
311
  })
306
312
  });
307
- }, et = (t, e) => /* @__PURE__ */ y("span", {
313
+ }, V = (t, r) => /* @__PURE__ */ E("span", {
308
314
  className: "spotlab-hm-slider-label",
309
- children: [t.label, e && /* @__PURE__ */ g("span", {
315
+ children: [t.label, r && /* @__PURE__ */ v("span", {
310
316
  className: "spotlab-hm-slider-remainder",
311
317
  children: " · remainder"
312
318
  })]
313
- }), ht = ({ actions: t, order: e, weights: n, passive: r, onWeightsChange: a, renderActionLabel: o, renderActionExtra: c, components: s, className: d }) => {
314
- const h = s?.WeightInput ?? z, p = (m, u) => {
315
- a(J(n, m, u, r));
319
+ }), it = ({ actions: t, order: r, weights: n, passive: e, onWeightsChange: a, renderActionLabel: o, renderActionExtra: c, components: i, className: u }) => {
320
+ const l = i?.WeightInput ?? J, f = (p, d) => {
321
+ a(X(n, p, d, e));
316
322
  };
317
- return /* @__PURE__ */ g("div", {
318
- className: ["spotlab-hm-root spotlab-hm-sliders", d].filter(Boolean).join(" "),
319
- children: e.map((m) => {
320
- const u = t[m], l = m === r;
321
- return /* @__PURE__ */ y("div", {
323
+ return /* @__PURE__ */ v("div", {
324
+ className: ["spotlab-hm-root spotlab-hm-sliders", u].filter(Boolean).join(" "),
325
+ children: r.map((p) => {
326
+ const d = t[p], s = p === e;
327
+ return /* @__PURE__ */ E("div", {
322
328
  className: "spotlab-hm-slider-row",
323
- "data-passive": l,
324
- children: [/* @__PURE__ */ y("div", {
329
+ "data-passive": s,
330
+ children: [/* @__PURE__ */ E("div", {
325
331
  className: "spotlab-hm-slider-header",
326
332
  children: [
327
- /* @__PURE__ */ g("span", {
333
+ /* @__PURE__ */ v("span", {
328
334
  className: "spotlab-hm-slider-swatch",
329
- style: { background: u.color }
335
+ style: { background: d.color }
330
336
  }),
331
- o?.(u, m, l) ?? et(u, l),
332
- c?.(u, m)
337
+ o?.(d, p, s) ?? V(d, s),
338
+ c?.(d, p)
333
339
  ]
334
- }), /* @__PURE__ */ y("div", {
340
+ }), /* @__PURE__ */ E("div", {
335
341
  className: "spotlab-hm-slider-track-row",
336
- children: [/* @__PURE__ */ g(tt, {
337
- value: n[m],
338
- color: u.color,
339
- passive: l,
340
- onChange: (i) => p(m, i)
341
- }), /* @__PURE__ */ g(h, {
342
- value: n[m],
343
- "aria-label": `${u.label} weight`,
344
- onChange: (i) => p(m, parseInt(i.target.value || "0", 10))
342
+ children: [/* @__PURE__ */ v(K, {
343
+ value: n[p],
344
+ color: d.color,
345
+ passive: s,
346
+ onChange: (h) => f(p, h)
347
+ }), /* @__PURE__ */ v(l, {
348
+ value: n[p],
349
+ "aria-label": `${d.label} weight`,
350
+ onChange: (h) => f(p, parseInt(h.target.value || "0", 10))
345
351
  })]
346
352
  })]
347
- }, u.id);
353
+ }, d.id);
348
354
  })
349
355
  });
350
356
  };
351
357
  export {
352
- ht as ActionWeightSliders,
353
- _ as CELL_FILL_DIRECTION,
354
- S as HandMatrix,
355
- ot as RangeGrid,
356
- ut as RangeMatrixEditor,
357
- L as cellFillGradient,
358
- st as cellsToComboWeights,
359
- it as cellsToRanges,
360
- ct as comboWeightsToCells,
361
- J as rebalance,
362
- lt as seedCells,
363
- Z as useRangePainting
358
+ it as ActionWeightSliders,
359
+ ot as RangeMatrix,
360
+ st as RangeMatrixEditor,
361
+ rt as cellsToComboWeights,
362
+ at as cellsToRanges,
363
+ nt as comboWeightsToCells,
364
+ lt as rangeToMatrix,
365
+ X as rebalance,
366
+ et as seedCells,
367
+ Q as useRangePainting
364
368
  };
@@ -0,0 +1,10 @@
1
+ import { FC } from 'react';
2
+ import { MatrixDisplayProps } from './hand-matrix';
3
+ export type RangeMatrixProps = MatrixDisplayProps;
4
+ /**
5
+ * A read-only 13x13 grid of several actions at once: each cell stacks its
6
+ * actions' weights as colored bands in `order`, hatches dead cells (`avail`
7
+ * 0), and marks capped cells (`avail` below 1) with a cap line and hatched
8
+ * remainder. Draws exactly what `RangeMatrixEditor` does, without painting.
9
+ */
10
+ export declare const RangeMatrix: FC<RangeMatrixProps>;
@@ -0,0 +1,19 @@
1
+ import { HandFrequency } from '@spotlab/range';
2
+ import { RangeMatrixProps } from './range-matrix';
3
+ /**
4
+ * `RangeMatrix`'s props for drawing one range: every cell is fully available
5
+ * and filled to its combo's mean weight in `color` (default
6
+ * `var(--spotlab-hm-fill)`), with the rest left blank. The two actions are
7
+ * `in` and `out`, labeled `labels.in` (default "In range") and `labels.out`
8
+ * (default "Out of range") for a legend of your own. Takes a range string or
9
+ * its expanded hand-keyed form; a string is parsed via `@spotlab/range`, so
10
+ * `initRange()` must have resolved. Parsing and collapsing are wasm calls, so
11
+ * memoize the result rather than calling this on every render.
12
+ */
13
+ export declare function rangeToMatrix(range: HandFrequency[] | string, { color, labels, }?: {
14
+ color?: string;
15
+ labels?: {
16
+ in?: string;
17
+ out?: string;
18
+ };
19
+ }): Omit<RangeMatrixProps, "className">;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spotlab/hand-matrix",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "A 13x13 poker hand-matrix React component: the read-only grid primitive and a range-string-aware display component, themed through CSS custom properties.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,13 +0,0 @@
1
- import { FC } from 'react';
2
- import { HandFrequency } from '@spotlab/range';
3
- /**
4
- * A read-only 13x13 range grid: each cell fills to its combo's mean weight.
5
- * Accepts the expanded hand-keyed form or a range string directly — a range
6
- * string is parsed via `@spotlab/range`, so the caller never needs to touch
7
- * the parser itself to render one.
8
- */
9
- export declare const RangeGrid: FC<{
10
- range: HandFrequency[] | string;
11
- fill?: string;
12
- className?: string;
13
- }>;