@spotlab/hand-matrix 0.1.0 → 0.2.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
@@ -16,20 +16,104 @@ await initRange(); // once, at startup
16
16
 
17
17
  ## Exports
18
18
 
19
+ Everything below is exported from the package root.
20
+
21
+ ### Display
22
+
19
23
  - `HandMatrix` — the bare 13x13 grid primitive. Takes no range data; styling
20
24
  is entirely the caller's job via `comboStyle`, keyed by combo string (e.g.
21
- `"AKs"`).
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.
22
37
  - `RangeGrid` — a read-only display component. Accepts the expanded
23
38
  `HandFrequency[]` form or a range string directly (parsed via
24
- `@spotlab/range`), and fills each cell to its combo's mean weight.
25
- - `cellFillGradient` / `CELL_FILL_DIRECTION` — the canonical left-to-right
26
- gradient helper used to render a cell's fill.
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.
27
45
 
28
46
  `RangeGrid`'s combo collapse (`toComboWeights`) is a wasm call, so
29
47
  `initRange()` must have resolved before rendering it — see
30
48
  [`@spotlab/range`](../range)'s own README for why that call is explicit
31
49
  rather than automatic.
32
50
 
51
+ ### Editing
52
+
53
+ The editor splits a parent range across a set of actions (e.g. fold, call,
54
+ raise). Its state is an array of 169 `EditorCell`s, one per combo in
55
+ `@spotlab/range`'s `COMBOS` order. Every component is fully controlled: state
56
+ goes in as props and changes come out through `on*Change` callbacks.
57
+
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.
61
+ Props (`RangeMatrixEditorProps`):
62
+ - `cells: EditorCell[]` and `onCellsChange(cells)`.
63
+ - `actions: EditorActionSpec[]` — index-aligned with each cell's
64
+ `weights`.
65
+ - `order: number[]` — indices into `actions`, in the order bands stack.
66
+ - `brush: number[]` and `onBrushChange(brush)` — the weights a paint
67
+ stroke applies, index-aligned with `actions`.
68
+ - `tool: PaintTool` and `onToolChange(tool)` — `"brush"` paints on press
69
+ and drag; `"eyedropper"` loads a cell's weights onto the brush and
70
+ switches back to `"brush"`.
71
+ - `className?: string`.
72
+ - `ActionWeightSliders` — one slider and numeric input per action for
73
+ editing a brush (or any weight row). Moving one action rebalances the
74
+ others so the row keeps summing to 100. Props (`ActionWeightSlidersProps`):
75
+ - `actions`, `order`, `weights: number[]` and `onWeightsChange(weights)`.
76
+ - `passive: number` — the index of the remainder action, which absorbs
77
+ whatever the others leave.
78
+ - `renderActionLabel?(action, index, isPassive)` — replaces a row's
79
+ default label.
80
+ - `renderActionExtra?(action, index)` — content after the label (e.g. a
81
+ remove button).
82
+ - `components?: { WeightInput?: ComponentType<WeightInputProps> }` — swap
83
+ the default `<input type="number">`.
84
+ - `className?: string`.
85
+ - `useRangePainting(opts)` — the brush/eyedropper state machine behind
86
+ `RangeMatrixEditor`, for building your own grid. `opts` takes the editor's
87
+ `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`.
91
+ - `seedCells(avail, actionWeights, passive): EditorCell[]` — build the
92
+ initial grid from a parent range's per-combo availability (`null` for
93
+ unconstrained) and each action's absolute per-combo weights (the
94
+ `Map<string, number>` that `toComboWeights` from `@spotlab/range`
95
+ produces).
96
+ - `cellsToComboWeights(cells, actionIndex): Map<string, number>` — collapse
97
+ one action back to absolute per-combo weights, ready to save as a range.
98
+ - `comboWeightsToCells(cells, actionWeights, passive): EditorCell[]` — the
99
+ inverse: re-derive the cells' conditional weights from absolute
100
+ per-action ranges, keeping each cell's `avail`.
101
+ - `rebalance(weights, index, value, passive): number[]` — set one action's
102
+ weight and rescale the rest so the row sums to 100. This is what
103
+ `ActionWeightSliders` uses.
104
+ - Types: `EditorCell` (`{ avail: number; weights: number[] }`, where `avail`
105
+ is the combo's cap in the parent range and `weights` are each action's
106
+ share of it, summing to 100), `EditorActionSpec`
107
+ (`{ id: string; label: string; color: string }`), `PaintTool`,
108
+ `CellPointerHandler`, `RangeMatrixEditorProps`, `ActionWeightSlidersProps`,
109
+ `WeightInputProps`.
110
+
111
+ **The editor works at combo level.** A cell is one of the 169 combos (`AKs`,
112
+ `AKo`, `QQ`), so suit-specific hands such as `AsKd` don't survive a round
113
+ trip: `seedCells` takes combo weights, and `cellsToComboWeights` returns
114
+ them. To keep suit-specific hands, set them aside before editing and add them
115
+ back to the result yourself.
116
+
33
117
  ## Theming
34
118
 
35
119
  The package ships `hand-matrix.css` (`@spotlab/hand-matrix/style.css`),
@@ -2,9 +2,13 @@ import { EditorCell } from './cells';
2
2
  export type PaintTool = "brush" | "eyedropper";
3
3
  /**
4
4
  * The brush/eyedropper state machine behind `RangeMatrixEditor`: drag-painting
5
- * (tracked across `pointerdown`/mouse-enter without a React re-render per
6
- * cell) and eyedropping a cell's weights back onto the brush. Fully
7
- * controlled — all state changes go out through the `on*Change` callbacks.
5
+ * and eyedropping a cell's weights back onto the brush. Fully controlled — all
6
+ * state changes go out through the `on*Change` callbacks.
7
+ *
8
+ * Wire `onCellDown` to a cell's pointer-down and `onCellEnter` to the pointer
9
+ * moving onto a cell (`HandMatrix`'s `onPointerDown`/`onPointerEnter` do both,
10
+ * for mouse and touch alike). A stroke ends on any window `pointerup` or
11
+ * `pointercancel`.
8
12
  */
9
13
  export declare function useRangePainting(opts: {
10
14
  cells: EditorCell[];
@@ -1,13 +1,33 @@
1
- import { CSSProperties, FC } from 'react';
1
+ import { CSSProperties, FC, PointerEvent as ReactPointerEvent } from 'react';
2
2
  export declare const CELL_FILL_DIRECTION = "to right";
3
3
  export declare const cellFillGradient: (stops: string[]) => string;
4
- type ComboHandlers = {
5
- onClick?: (combo: string) => void;
6
- onMouseDown?: (combo: string) => void;
7
- onMouseEnter?: (combo: string) => void;
4
+ export type CellPointerHandler = (combo: string, event: ReactPointerEvent<HTMLDivElement>) => void;
5
+ type ComboStyleProp = {
8
6
  comboStyle?: (combo: string) => CSSProperties;
9
7
  };
8
+ type GridPointerHandlers = {
9
+ /**
10
+ * A pointer (mouse, pen or touch) went down on `combo`'s cell. The grid
11
+ * first releases the implicit pointer capture a touch takes on the element
12
+ * it starts on, so later moves can resolve to other cells. Passing this
13
+ * also makes the grid root a drag surface (see `dragSurfaceStyle`).
14
+ */
15
+ onPointerDown?: CellPointerHandler;
16
+ /**
17
+ * The pointer moved onto `combo`'s cell. Resolved by hit-testing each
18
+ * `pointermove` (`elementFromPoint`) rather than by enter events, so it
19
+ * fires across a touch drag, where `mouseenter`/`pointerenter` never reach
20
+ * the other cells, as well as for a mouse. Fires once per change of cell,
21
+ * whether or not a button is pressed.
22
+ */
23
+ onPointerEnter?: CellPointerHandler;
24
+ };
25
+ /**
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.
29
+ */
10
30
  export declare const HandMatrix: FC<{
11
31
  className?: string;
12
- } & ComboHandlers>;
32
+ } & ComboStyleProp & GridPointerHandlers>;
13
33
  export {};
package/dist/index.d.ts CHANGED
@@ -1,12 +1,7 @@
1
- export { HandMatrix, cellFillGradient, CELL_FILL_DIRECTION, } from './hand-matrix';
1
+ export { HandMatrix, cellFillGradient, CELL_FILL_DIRECTION, type CellPointerHandler, } from './hand-matrix';
2
2
  export { RangeGrid } from './range-grid';
3
- export { RangeMatrixEditor } from './editor/matrix-editor';
4
- export type { RangeMatrixEditorProps } from './editor/matrix-editor';
5
- export { ActionWeightSliders } from './editor/action-sliders';
6
- export type { ActionWeightSlidersProps, WeightInputProps, } from './editor/action-sliders';
7
- export { useRangePainting } from './editor/use-range-painting';
8
- export type { PaintTool } from './editor/use-range-painting';
9
- export { seedCells, cellsToComboWeights, comboWeightsToCells, } from './editor/cells';
10
- export type { EditorCell } from './editor/cells';
11
- export { rebalance } from './editor/weights';
12
- export type { EditorActionSpec } from './editor/weights';
3
+ export { RangeMatrixEditor, type RangeMatrixEditorProps, } from './editor/matrix-editor';
4
+ export { ActionWeightSliders, type ActionWeightSlidersProps, type WeightInputProps, } from './editor/action-sliders';
5
+ export { useRangePainting, type PaintTool } from './editor/use-range-painting';
6
+ export { seedCells, cellsToComboWeights, comboWeightsToCells, type EditorCell, } from './editor/cells';
7
+ export { rebalance, type EditorActionSpec } from './editor/weights';
package/dist/index.js CHANGED
@@ -1,307 +1,334 @@
1
- import { COMBOS as g, parseRange as R, toComboWeights as x } from "@spotlab/range";
2
- import { jsx as p, jsxs as b } from "react/jsx-runtime";
3
- import { useCallback as v, useEffect as k, useMemo as L, useRef as w } from "react";
4
- var I = (e, t) => Array.from({ length: Math.ceil(e.length / t) }, (n, r) => e.slice(r * t, r * t + t)), D = "to right", $ = (e) => e.length === 0 ? "transparent" : `linear-gradient(${D}, ${e.join(", ")})`, B = ({ combo: e, onClick: t, onMouseDown: n, onMouseEnter: r, comboStyle: a }) => /* @__PURE__ */ p("div", {
1
+ import { useCallback as g, useEffect as y, useLayoutEffect as I, useMemo as x, useRef as v } from "react";
2
+ import { COMBOS as b, parseRange as S, toComboWeights as A } from "@spotlab/range";
3
+ import { jsx as h, jsxs as w } from "react/jsx-runtime";
4
+ var P = (t, e) => Array.from({ length: Math.ceil(t.length / e) }, (n, r) => t.slice(r * e, r * e + e)), D = "to right", N = (t) => t.length === 0 ? "transparent" : `linear-gradient(${D}, ${t.join(", ")})`, W = (t) => {
5
+ const e = t.target;
6
+ e.hasPointerCapture(t.pointerId) && e.releasePointerCapture(t.pointerId);
7
+ }, M = (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
+ }, T = ({ combo: t, onPointerDown: e, comboStyle: n }) => /* @__PURE__ */ h("div", {
5
11
  className: "spotlab-hm-cell",
6
- style: a?.(e),
7
- onClick: () => t?.(e),
8
- onMouseDown: () => n?.(e),
9
- onMouseEnter: () => r?.(e),
10
- children: /* @__PURE__ */ p("div", {
12
+ "data-combo": t,
13
+ style: n?.(t),
14
+ onPointerDown: e && ((r) => {
15
+ W(r), e(t, r);
16
+ }),
17
+ children: /* @__PURE__ */ h("div", {
11
18
  className: "spotlab-hm-cell-inner",
12
- children: /* @__PURE__ */ p("div", {
13
- "data-combo": e,
14
- children: e
19
+ children: /* @__PURE__ */ h("div", {
20
+ "data-combo": t,
21
+ children: t
15
22
  })
16
23
  })
17
- }), O = ({ row: e, ...t }) => /* @__PURE__ */ p("div", {
24
+ }), B = ({ row: t, ...e }) => /* @__PURE__ */ h("div", {
18
25
  className: "spotlab-hm-row",
19
- children: e.map((n, r) => /* @__PURE__ */ p(B, {
26
+ children: t.map((n, r) => /* @__PURE__ */ h(T, {
20
27
  combo: n,
21
- ...t
28
+ ...e
22
29
  }, r))
23
- }), N = ({ className: e, ...t }) => /* @__PURE__ */ p("div", {
24
- className: ["spotlab-hm-root", e].filter(Boolean).join(" "),
25
- children: I(g, 13).map((n, r) => /* @__PURE__ */ p(O, {
26
- row: n,
27
- ...t
28
- }, r))
29
- }), S = (e, t) => ({ background: $([`${e} 0 ${t * 100}%`, `transparent ${t * 100}% 100%`]) }), H = ({ range: e, fill: t = "var(--spotlab-hm-fill)", className: n }) => {
30
- const r = L(() => {
31
- const a = typeof e == "string" ? R(e) : e;
32
- return x(a);
33
- }, [e]);
34
- return /* @__PURE__ */ p(N, {
30
+ }), F = {
31
+ touchAction: "none",
32
+ userSelect: "none",
33
+ WebkitUserSelect: "none",
34
+ WebkitTouchCallout: "none"
35
+ }, R = ({ className: t, onPointerDown: e, onPointerEnter: n, ...r }) => {
36
+ const a = v(null), o = (i) => {
37
+ const l = M(i.currentTarget, i.clientX, i.clientY);
38
+ l !== a.current && (a.current = l, l && n?.(l, i));
39
+ };
40
+ return /* @__PURE__ */ h("div", {
41
+ className: ["spotlab-hm-root", t].filter(Boolean).join(" "),
42
+ style: e ? F : void 0,
43
+ onPointerDown: n && ((i) => {
44
+ a.current = M(i.currentTarget, i.clientX, i.clientY);
45
+ }),
46
+ onPointerMove: n && o,
47
+ onPointerLeave: n && (() => a.current = null),
48
+ children: P(b, 13).map((i, l) => /* @__PURE__ */ h(B, {
49
+ row: i,
50
+ onPointerDown: e,
51
+ ...r
52
+ }, l))
53
+ });
54
+ }, O = (t, e) => ({ background: N([`${t} 0 ${e * 100}%`, `transparent ${e * 100}% 100%`]) }), V = ({ range: t, fill: e = "var(--spotlab-hm-fill)", className: n }) => {
55
+ const r = x(() => {
56
+ const a = typeof t == "string" ? S(t) : t;
57
+ return A(a);
58
+ }, [t]);
59
+ return /* @__PURE__ */ h(R, {
35
60
  className: n,
36
61
  comboStyle: (a) => {
37
62
  const o = r.get(a);
38
- return o ? S(t, o) : {};
63
+ return o ? O(e, o) : {};
39
64
  }
40
65
  });
41
- }, W = (e) => Math.max(0, Math.min(100, Math.round(e))), F = (e, t) => {
42
- const n = e.map(Math.round);
66
+ }, X = (t) => Math.max(0, Math.min(100, Math.round(t))), j = (t, e) => {
67
+ const n = t.map(Math.round);
43
68
  let r = 100 - n.reduce((a, o) => a + o, 0);
44
69
  if (r !== 0) {
45
- for (let a = 0; a < n.length; a++) if (a !== t && n[a] + r >= 0) {
70
+ for (let a = 0; a < n.length; a++) if (a !== e && n[a] + r >= 0) {
46
71
  n[a] += r, r = 0;
47
72
  break;
48
73
  }
49
74
  if (r !== 0) {
50
- for (let a = 0; a < n.length; a++) if (a !== t && n[a] > 0) {
75
+ for (let a = 0; a < n.length; a++) if (a !== e && n[a] > 0) {
51
76
  const o = Math.min(n[a], -r);
52
77
  if (n[a] -= o, r += o, r === 0) break;
53
78
  }
54
- n[t] = Math.max(0, n[t] + r);
79
+ n[e] = Math.max(0, n[e] + r);
55
80
  }
56
81
  }
57
82
  return n;
58
- }, j = (e, t, n, r) => {
59
- const a = W(n), o = e.slice();
60
- o[t] = a;
83
+ }, _ = (t, e, n, r) => {
84
+ const a = X(n), o = t.slice();
85
+ o[e] = a;
61
86
  const i = 100 - a, l = [];
62
- for (let s = 0; s < e.length; s++) s !== t && l.push(s);
63
- const c = l.reduce((s, u) => s + e[u], 0);
64
- if (c <= 0) {
65
- l.forEach((u) => o[u] = 0);
66
- const s = t === r ? l[0] : r;
87
+ for (let s = 0; s < t.length; s++) s !== e && l.push(s);
88
+ const u = l.reduce((s, p) => s + t[p], 0);
89
+ if (u <= 0) {
90
+ l.forEach((p) => o[p] = 0);
91
+ const s = e === r ? l[0] : r;
67
92
  s != null && (o[s] = i);
68
- } else l.forEach((s) => o[s] = Math.round(e[s] / c * i));
69
- return F(o, t);
70
- }, M = (e, t) => e.reduce((n, r, a) => a === t ? n : n + r, 0), C = new Map(g.map((e, t) => [e, t])), y = (e, t) => {
71
- const n = e.slice();
72
- let r = M(n, t);
93
+ } else l.forEach((s) => o[s] = Math.round(t[s] / u * i));
94
+ return j(o, e);
95
+ }, $ = (t, e) => t.reduce((n, r, a) => a === e ? n : n + r, 0), k = new Map(b.map((t, e) => [t, e])), L = (t, e) => {
96
+ const n = t.slice();
97
+ let r = $(n, e);
73
98
  if (r > 100) {
74
99
  const a = 100 / r;
75
- for (let o = 0; o < n.length; o++) o !== t && (n[o] = Math.round(n[o] * a));
76
- r = M(n, t);
100
+ for (let o = 0; o < n.length; o++) o !== e && (n[o] = Math.round(n[o] * a));
101
+ r = $(n, e);
77
102
  }
78
- return n[t] = Math.max(0, 100 - r), n;
103
+ return n[e] = Math.max(0, 100 - r), n;
79
104
  };
80
- function Q(e, t, n) {
81
- return g.map((r) => {
82
- const a = e ? e.get(r) ?? 0 : 1, o = t.map(() => 0);
105
+ function Z(t, e, n) {
106
+ return b.map((r) => {
107
+ const a = t ? t.get(r) ?? 0 : 1, o = e.map(() => 0);
83
108
  return a <= 0 ? (o[n] = 100, {
84
109
  avail: 0,
85
110
  weights: o
86
- }) : (t.forEach((i, l) => {
87
- const c = i?.get(r) ?? 0;
88
- o[l] = Math.round(c / a * 100);
111
+ }) : (e.forEach((i, l) => {
112
+ const u = i?.get(r) ?? 0;
113
+ o[l] = Math.round(u / a * 100);
89
114
  }), {
90
115
  avail: a,
91
- weights: y(o, n)
116
+ weights: L(o, n)
92
117
  });
93
118
  });
94
119
  }
95
- var T = 8;
96
- function Y(e, t) {
120
+ var q = 8;
121
+ function z(t, e) {
97
122
  const n = /* @__PURE__ */ new Map();
98
- return e.forEach((r, a) => {
123
+ return t.forEach((r, a) => {
99
124
  if (r.avail <= 0) return;
100
- const o = r.weights.reduce((l, c) => l + c, 0);
125
+ const o = r.weights.reduce((l, u) => l + u, 0);
101
126
  if (!o) return;
102
- const i = Number((r.weights[t] / o * r.avail).toFixed(T));
103
- i && n.set(g[a], i);
127
+ const i = Number((r.weights[e] / o * r.avail).toFixed(q));
128
+ i && n.set(b[a], i);
104
129
  }), n;
105
130
  }
106
- function J(e, t, n) {
107
- return e.map((r, a) => {
108
- const o = t.map(() => 0);
131
+ function tt(t, e, n) {
132
+ return t.map((r, a) => {
133
+ const o = e.map(() => 0);
109
134
  if (r.avail <= 0)
110
135
  return o[n] = 100, {
111
136
  avail: r.avail,
112
137
  weights: o
113
138
  };
114
- const i = g[a];
115
- return t.forEach((l, c) => {
139
+ const i = b[a];
140
+ return e.forEach((l, u) => {
116
141
  const s = l?.get(i) ?? 0;
117
- o[c] = Math.round(s / r.avail * 100);
142
+ o[u] = Math.round(s / r.avail * 100);
118
143
  }), {
119
144
  avail: r.avail,
120
- weights: y(o, n)
145
+ weights: L(o, n)
121
146
  };
122
147
  });
123
148
  }
124
- function _(e) {
125
- const { cells: t, brush: n, tool: r, onCellsChange: a, onBrushChange: o, onToolChange: i } = e, l = w(!1);
126
- k(() => {
127
- const u = () => l.current = !1;
128
- return window.addEventListener("pointerup", u), () => window.removeEventListener("pointerup", u);
149
+ function G(t) {
150
+ const { cells: e, brush: n, tool: r, onCellsChange: a, onBrushChange: o, onToolChange: i } = t, l = v(null), u = g(() => {
151
+ l.current?.abort(), l.current = null;
129
152
  }, []);
130
- const c = v((u) => {
131
- const d = C.get(u);
132
- if (t[d].avail <= 0) return;
133
- const h = t.slice();
134
- h[d] = {
135
- avail: t[d].avail,
153
+ y(() => u, [u]);
154
+ const s = v(e);
155
+ I(() => {
156
+ s.current = e;
157
+ }, [e]);
158
+ const p = g((c) => {
159
+ const d = s.current, f = k.get(c);
160
+ if (d[f].avail <= 0) return;
161
+ const C = d.slice();
162
+ C[f] = {
163
+ avail: d[f].avail,
136
164
  weights: [...n]
137
- }, a(h);
138
- }, [
139
- t,
140
- n,
141
- a
142
- ]), s = v((u) => {
143
- const d = C.get(u);
144
- t[d].avail <= 0 || (o([...t[d].weights]), i("brush"));
165
+ }, s.current = C, a(C);
166
+ }, [n, a]), m = g((c) => {
167
+ const d = k.get(c);
168
+ e[d].avail <= 0 || (o([...e[d].weights]), i("brush"));
145
169
  }, [
146
- t,
170
+ e,
147
171
  o,
148
172
  i
149
173
  ]);
150
174
  return {
151
- onCellDown: v((u) => {
175
+ onCellDown: g((c) => {
152
176
  if (r === "eyedropper") {
153
- s(u);
177
+ m(c);
154
178
  return;
155
179
  }
156
- l.current = !0, c(u);
180
+ u();
181
+ const d = new AbortController();
182
+ l.current = d, window.addEventListener("pointerup", u, { signal: d.signal }), window.addEventListener("pointercancel", u, { signal: d.signal }), p(c);
157
183
  }, [
158
184
  r,
159
- s,
160
- c
185
+ m,
186
+ p,
187
+ u
161
188
  ]),
162
- onCellEnter: v((u) => {
163
- l.current && r === "brush" && c(u);
164
- }, [r, c])
189
+ onCellEnter: g((c) => {
190
+ l.current && r === "brush" && p(c);
191
+ }, [r, p])
165
192
  };
166
193
  }
167
- var E = (e, t, n) => {
194
+ var E = (t, e, n) => {
168
195
  const r = [];
169
196
  let a = 0;
170
197
  return n.forEach((o) => {
171
- const i = (e.weights[o] || 0) / 100 * e.avail;
198
+ const i = (t.weights[o] || 0) / 100;
172
199
  if (i <= 0) return;
173
- const l = t[o].color;
200
+ const l = e[o].color;
174
201
  r.push(`${l} ${a * 100}%`), r.push(`${l} ${(a + i) * 100}%`), a += i;
175
- }), $(r);
176
- }, K = ({ cells: e, actions: t, order: n, brush: r, tool: a, onCellsChange: o, onBrushChange: i, onToolChange: l, className: c }) => {
177
- const { onCellDown: s, onCellEnter: u } = _({
178
- cells: e,
202
+ }), r.length > 0 && a < 1 - 1e-9 && r.push(`transparent ${a * 100}% 100%`), N(r);
203
+ }, et = ({ cells: t, actions: e, order: n, brush: r, tool: a, onCellsChange: o, onBrushChange: i, onToolChange: l, className: u }) => {
204
+ const { onCellDown: s, onCellEnter: p } = G({
205
+ cells: t,
179
206
  brush: r,
180
207
  tool: a,
181
208
  onCellsChange: o,
182
209
  onBrushChange: i,
183
210
  onToolChange: l
184
211
  });
185
- return /* @__PURE__ */ p(N, {
186
- className: c,
187
- comboStyle: (d) => {
188
- const h = e[C.get(d)];
189
- if (h.avail <= 0) return {
212
+ return /* @__PURE__ */ h(R, {
213
+ className: u,
214
+ comboStyle: (m) => {
215
+ const c = t[k.get(m)];
216
+ if (c.avail <= 0) return {
190
217
  background: "var(--spotlab-hm-dead-hatch)",
191
218
  backgroundColor: "var(--spotlab-hm-muted-bg)",
192
219
  cursor: "not-allowed",
193
220
  color: "var(--spotlab-hm-muted-fg)"
194
221
  };
195
- const m = a === "eyedropper" ? "copy" : "pointer";
196
- if (h.avail < 1) {
197
- const f = `${h.avail * 100}%`;
222
+ const d = a === "eyedropper" ? "copy" : "pointer";
223
+ if (c.avail < 1) {
224
+ const f = `${c.avail * 100}%`;
198
225
  return {
199
- background: `repeating-linear-gradient(180deg, var(--spotlab-hm-muted-fg) 0 4px, transparent 4px 8px), var(--spotlab-hm-dead-hatch), ${E(h, t, n)}`,
200
- backgroundSize: `1px 100%, ${(1 - h.avail) * 100}% 100%, ${h.avail * 100}% 100%`,
226
+ background: `repeating-linear-gradient(180deg, var(--spotlab-hm-muted-fg) 0 4px, transparent 4px 8px), var(--spotlab-hm-dead-hatch), ${E(c, e, n)}`,
227
+ backgroundSize: `1px 100%, ${(1 - c.avail) * 100}% 100%, ${c.avail * 100}% 100%`,
201
228
  backgroundPosition: `${f} center, right center, left center`,
202
229
  backgroundRepeat: "no-repeat, no-repeat, no-repeat",
203
- cursor: m
230
+ cursor: d
204
231
  };
205
232
  }
206
233
  return {
207
- background: E(h, t, n),
208
- cursor: m
234
+ background: E(c, e, n),
235
+ cursor: d
209
236
  };
210
237
  },
211
- onMouseDown: s,
212
- onMouseEnter: u
238
+ onPointerDown: s,
239
+ onPointerEnter: p
213
240
  });
214
- }, A = (e) => /* @__PURE__ */ p("input", {
241
+ }, U = (t) => /* @__PURE__ */ h("input", {
215
242
  type: "number",
216
243
  className: "spotlab-hm-weight-input",
217
- ...e
218
- }), P = ({ value: e, color: t, passive: n, onChange: r }) => {
219
- const a = w(null), o = w(!1), i = v((l) => {
220
- const c = a.current;
221
- if (!c) return;
222
- const s = c.getBoundingClientRect();
244
+ ...t
245
+ }), Y = ({ value: t, color: e, passive: n, onChange: r }) => {
246
+ const a = v(null), o = v(!1), i = g((l) => {
247
+ const u = a.current;
248
+ if (!u) return;
249
+ const s = u.getBoundingClientRect();
223
250
  r((l - s.left) / s.width * 100);
224
251
  }, [r]);
225
- return k(() => {
252
+ return y(() => {
226
253
  const l = (s) => {
227
254
  o.current && i(s.clientX);
228
- }, c = () => o.current = !1;
229
- return window.addEventListener("pointermove", l), window.addEventListener("pointerup", c), () => {
230
- window.removeEventListener("pointermove", l), window.removeEventListener("pointerup", c);
255
+ }, u = () => o.current = !1;
256
+ return window.addEventListener("pointermove", l), window.addEventListener("pointerup", u), () => {
257
+ window.removeEventListener("pointermove", l), window.removeEventListener("pointerup", u);
231
258
  };
232
- }, [i]), /* @__PURE__ */ p("div", {
259
+ }, [i]), /* @__PURE__ */ h("div", {
233
260
  ref: a,
234
261
  role: "slider",
235
- "aria-valuenow": e,
262
+ "aria-valuenow": t,
236
263
  "aria-valuemin": 0,
237
264
  "aria-valuemax": 100,
238
265
  className: "spotlab-hm-slider-track",
239
- style: { background: `linear-gradient(to right, ${t} 0 ${e}%, var(--spotlab-hm-track-bg) ${e}% 100%)` },
266
+ style: { background: `linear-gradient(to right, ${e} 0 ${t}%, var(--spotlab-hm-track-bg) ${t}% 100%)` },
240
267
  onPointerDown: (l) => {
241
268
  o.current = !0, i(l.clientX);
242
269
  },
243
- children: /* @__PURE__ */ p("div", {
270
+ children: /* @__PURE__ */ h("div", {
244
271
  className: "spotlab-hm-slider-thumb",
245
272
  style: {
246
- left: `${e}%`,
247
- border: `2px solid ${n ? "var(--spotlab-hm-muted-fg)" : t}`
273
+ left: `${t}%`,
274
+ border: `2px solid ${n ? "var(--spotlab-hm-muted-fg)" : e}`
248
275
  }
249
276
  })
250
277
  });
251
- }, X = (e, t) => /* @__PURE__ */ b("span", {
278
+ }, H = (t, e) => /* @__PURE__ */ w("span", {
252
279
  className: "spotlab-hm-slider-label",
253
- children: [e.label, t && /* @__PURE__ */ p("span", {
280
+ children: [t.label, e && /* @__PURE__ */ h("span", {
254
281
  className: "spotlab-hm-slider-remainder",
255
282
  children: " · remainder"
256
283
  })]
257
- }), V = ({ actions: e, order: t, weights: n, passive: r, onWeightsChange: a, renderActionLabel: o, renderActionExtra: i, components: l, className: c }) => {
258
- const s = l?.WeightInput ?? A, u = (d, h) => {
259
- a(j(n, d, h, r));
284
+ }), rt = ({ actions: t, order: e, weights: n, passive: r, onWeightsChange: a, renderActionLabel: o, renderActionExtra: i, components: l, className: u }) => {
285
+ const s = l?.WeightInput ?? U, p = (m, c) => {
286
+ a(_(n, m, c, r));
260
287
  };
261
- return /* @__PURE__ */ p("div", {
262
- className: ["spotlab-hm-root spotlab-hm-sliders", c].filter(Boolean).join(" "),
263
- children: t.map((d) => {
264
- const h = e[d], m = d === r;
265
- return /* @__PURE__ */ b("div", {
288
+ return /* @__PURE__ */ h("div", {
289
+ className: ["spotlab-hm-root spotlab-hm-sliders", u].filter(Boolean).join(" "),
290
+ children: e.map((m) => {
291
+ const c = t[m], d = m === r;
292
+ return /* @__PURE__ */ w("div", {
266
293
  className: "spotlab-hm-slider-row",
267
- "data-passive": m,
268
- children: [/* @__PURE__ */ b("div", {
294
+ "data-passive": d,
295
+ children: [/* @__PURE__ */ w("div", {
269
296
  className: "spotlab-hm-slider-header",
270
297
  children: [
271
- /* @__PURE__ */ p("span", {
298
+ /* @__PURE__ */ h("span", {
272
299
  className: "spotlab-hm-slider-swatch",
273
- style: { background: h.color }
300
+ style: { background: c.color }
274
301
  }),
275
- o?.(h, d, m) ?? X(h, m),
276
- i?.(h, d)
302
+ o?.(c, m, d) ?? H(c, d),
303
+ i?.(c, m)
277
304
  ]
278
- }), /* @__PURE__ */ b("div", {
305
+ }), /* @__PURE__ */ w("div", {
279
306
  className: "spotlab-hm-slider-track-row",
280
- children: [/* @__PURE__ */ p(P, {
281
- value: n[d],
282
- color: h.color,
283
- passive: m,
284
- onChange: (f) => u(d, f)
285
- }), /* @__PURE__ */ p(s, {
286
- value: n[d],
287
- "aria-label": `${h.label} weight`,
288
- onChange: (f) => u(d, parseInt(f.target.value || "0", 10))
307
+ children: [/* @__PURE__ */ h(Y, {
308
+ value: n[m],
309
+ color: c.color,
310
+ passive: d,
311
+ onChange: (f) => p(m, f)
312
+ }), /* @__PURE__ */ h(s, {
313
+ value: n[m],
314
+ "aria-label": `${c.label} weight`,
315
+ onChange: (f) => p(m, parseInt(f.target.value || "0", 10))
289
316
  })]
290
317
  })]
291
- }, h.id);
318
+ }, c.id);
292
319
  })
293
320
  });
294
321
  };
295
322
  export {
296
- V as ActionWeightSliders,
323
+ rt as ActionWeightSliders,
297
324
  D as CELL_FILL_DIRECTION,
298
- N as HandMatrix,
299
- H as RangeGrid,
300
- K as RangeMatrixEditor,
301
- $ as cellFillGradient,
302
- Y as cellsToComboWeights,
303
- J as comboWeightsToCells,
304
- j as rebalance,
305
- Q as seedCells,
306
- _ as useRangePainting
325
+ R as HandMatrix,
326
+ V as RangeGrid,
327
+ et as RangeMatrixEditor,
328
+ N as cellFillGradient,
329
+ z as cellsToComboWeights,
330
+ tt as comboWeightsToCells,
331
+ _ as rebalance,
332
+ Z as seedCells,
333
+ G as useRangePainting
307
334
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spotlab/hand-matrix",
3
- "version": "0.1.0",
3
+ "version": "0.2.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",
@@ -40,12 +40,12 @@
40
40
  "LICENSE"
41
41
  ],
42
42
  "peerDependencies": {
43
- "@spotlab/range": "^0.1.0",
43
+ "@spotlab/range": "^0.2.0",
44
44
  "react": "^19.0.0",
45
45
  "react-dom": "^19.0.0"
46
46
  },
47
47
  "devDependencies": {
48
- "@spotlab/range": "0.1.0",
48
+ "@spotlab/range": "0.2.0",
49
49
  "@storybook/addon-vitest": "^10.5.5",
50
50
  "@storybook/react-vite": "^10.5.5",
51
51
  "@types/react": "^19.2.16",
@@ -73,6 +73,6 @@
73
73
  "test": "vitest run --project unit",
74
74
  "storybook": "storybook dev -p 6007",
75
75
  "build-storybook": "storybook build",
76
- "test-storybook": "vitest run --project storybook"
76
+ "test-storybook": "NODE_ENV=production vitest run --project storybook"
77
77
  }
78
78
  }