@tremolo-ui/react 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.
Files changed (62) hide show
  1. package/dist/index.cjs +1125 -1437
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +58 -93
  4. package/dist/index.d.cts +561 -243
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +561 -243
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1125 -1438
  9. package/dist/index.js.map +1 -1
  10. package/package.json +9 -10
  11. package/src/components/AnimationCanvas/index.tsx +84 -155
  12. package/src/components/Knob/ActiveLine.tsx +6 -6
  13. package/src/components/Knob/InactiveLine.tsx +9 -11
  14. package/src/components/Knob/SVGRoot.tsx +9 -18
  15. package/src/components/Knob/context.tsx +57 -89
  16. package/src/components/Knob/index.css +6 -2
  17. package/src/components/Knob/index.tsx +90 -61
  18. package/src/components/NumberInput/DecrementStepper.tsx +17 -50
  19. package/src/components/NumberInput/IncrementStepper.tsx +17 -50
  20. package/src/components/NumberInput/InputField.tsx +123 -0
  21. package/src/components/NumberInput/Stepper.tsx +90 -12
  22. package/src/components/NumberInput/context.tsx +74 -146
  23. package/src/components/NumberInput/index.css +38 -102
  24. package/src/components/NumberInput/index.tsx +252 -98
  25. package/src/components/NumberInput/stepperButton.tsx +90 -0
  26. package/src/components/Piano/index.tsx +272 -288
  27. package/src/components/Piano/keyboardShortcuts.ts +34 -4
  28. package/src/components/PointsEditor/Background.tsx +8 -2
  29. package/src/components/PointsEditor/Container.tsx +19 -9
  30. package/src/components/PointsEditor/Point.tsx +104 -24
  31. package/src/components/PointsEditor/context.tsx +28 -74
  32. package/src/components/PointsEditor/index.css +5 -0
  33. package/src/components/PointsEditor/index.tsx +77 -43
  34. package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
  35. package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
  36. package/src/components/Slider/Thumb.tsx +55 -68
  37. package/src/components/Slider/Track.tsx +15 -15
  38. package/src/components/Slider/context.tsx +36 -51
  39. package/src/components/Slider/index.css +18 -5
  40. package/src/components/Slider/index.tsx +127 -146
  41. package/src/components/Slider/type.ts +4 -4
  42. package/src/components/XYPad/Area.tsx +16 -7
  43. package/src/components/XYPad/Thumb.tsx +62 -74
  44. package/src/components/XYPad/context.tsx +46 -0
  45. package/src/components/XYPad/index.css +12 -0
  46. package/src/components/XYPad/index.tsx +190 -230
  47. package/src/hooks/useDrag.ts +1 -1
  48. package/src/hooks/useDragValue.ts +149 -0
  49. package/src/hooks/useEventListener.ts +1 -1
  50. package/src/hooks/useWheel.ts +29 -9
  51. package/src/index.ts +15 -9
  52. package/src/components/AnimationCanvas/canvas.ts +0 -46
  53. package/src/components/NumberInput/InternalInput.tsx +0 -216
  54. package/src/components/NumberInput/type.ts +0 -58
  55. package/src/components/Piano/KeyLabel.tsx +0 -51
  56. package/src/components/Piano/context.tsx +0 -73
  57. package/src/components/Piano/key.tsx +0 -205
  58. package/src/components/_util/type.ts +0 -1
  59. package/src/hooks/useDragWithElement.ts +0 -100
  60. package/src/hooks/usePianoDrag.ts +0 -75
  61. package/src/hooks/useRefCallbackEvent.ts +0 -34
  62. /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
package/dist/index.cjs CHANGED
@@ -25,147 +25,78 @@ let clsx = require("clsx");
25
25
  clsx = __toESM(clsx);
26
26
  let react = require("react");
27
27
  react = __toESM(react);
28
+ let _tremolo_ui_dom = require("@tremolo-ui/dom");
28
29
  let react_jsx_runtime = require("react/jsx-runtime");
29
30
  let _tremolo_ui_functions = require("@tremolo-ui/functions");
30
- let _tremolo_ui_dom = require("@tremolo-ui/dom");
31
- let zustand = require("zustand");
32
- //#region src/components/AnimationCanvas/canvas.ts
33
- const drawingState = [
34
- "strokeStyle",
35
- "fillStyle",
36
- "globalAlpha",
37
- "lineWidth",
38
- "lineCap",
39
- "lineJoin",
40
- "miterLimit",
41
- "lineDashOffset",
42
- "shadowOffsetX",
43
- "shadowOffsetY",
44
- "shadowBlur",
45
- "shadowColor",
46
- "globalCompositeOperation",
47
- "font",
48
- "textAlign",
49
- "textBaseline",
50
- "direction",
51
- "imageSmoothingEnabled"
52
- ];
53
- function setDprConfig(canvas, context, width, height, dpr) {
54
- canvas.width = width * dpr;
55
- canvas.height = height * dpr;
56
- context.setTransform(1, 0, 0, 1, 0, 0);
57
- context.scale(dpr, dpr);
58
- canvas.style.width = `${width}px`;
59
- canvas.style.height = `${height}px`;
60
- }
61
- //#endregion
62
31
  //#region src/components/AnimationCanvas/index.tsx
63
- function AnimationCanvas({ draw, init, animate = true, options, width: _width = 100, height: _height = 100, relativeSize, reduceFlickering = true, className, onContextMenu = (event) => event.preventDefault(), ...props }) {
64
- const canvasRef = (0, react.useRef)(null);
65
- const memoCanvasRef = (0, react.useRef)(null);
66
- const reqIdRef = (0, react.useRef)(-1);
67
- const widthRef = (0, react.useRef)(0);
68
- const heightRef = (0, react.useRef)(0);
69
- const deltaMemoRef = (0, react.useRef)(-1);
70
- const startTimeRef = (0, react.useRef)(-1);
71
- const loop = (0, react.useCallback)((context, width, height, count) => {
72
- const now = performance.now();
73
- const deltaTime = now - deltaMemoRef.current;
74
- const elapsedTime = now - startTimeRef.current;
75
- deltaMemoRef.current = now;
76
- if (animate) reqIdRef.current = requestAnimationFrame(() => loop(context, width, height, count + 1));
77
- draw(context, {
78
- width: width.current,
79
- height: height.current,
80
- count: count + 1,
81
- deltaTime,
82
- elapsedTime,
83
- fps: 1e3 / deltaTime
32
+ function AnimationCanvas({ draw, init, animate = true, options, width = 100, height = 100, relativeSize = false, reduceFlickering = true, className, onContextMenu = (event) => event.preventDefault(), ...props }) {
33
+ const [node, setNode] = (0, react.useState)(null);
34
+ const latest = (0, react.useRef)({
35
+ draw,
36
+ init,
37
+ animate,
38
+ width,
39
+ height,
40
+ reduceFlickering,
41
+ options
42
+ });
43
+ const instanceRef = (0, react.useRef)(null);
44
+ /** Set while the instance below is newer than the effect that pushes into it. */
45
+ const justCreated = (0, react.useRef)(false);
46
+ (0, react.useEffect)(() => {
47
+ if (!node) return;
48
+ const current = latest.current;
49
+ const instance = (0, _tremolo_ui_dom.createAnimationCanvas)(node, {
50
+ draw: (context, frame) => latest.current.draw(context, frame),
51
+ init: (context, size) => latest.current.init?.(context, size),
52
+ animate: current.animate,
53
+ size: {
54
+ width: current.width,
55
+ height: current.height
56
+ },
57
+ reduceFlickering: current.reduceFlickering,
58
+ relativeSize,
59
+ contextAttributes: current.options
84
60
  });
85
- }, [draw, animate]);
61
+ instanceRef.current = instance;
62
+ justCreated.current = true;
63
+ return () => {
64
+ instanceRef.current = null;
65
+ instance.destroy();
66
+ };
67
+ }, [node, relativeSize]);
86
68
  (0, react.useEffect)(() => {
87
- if (!canvasRef.current) return;
88
- const canvas = canvasRef.current;
89
- const context = canvas.getContext("2d", options);
90
- if (!context) throw new Error("Cannot get canvas context.");
91
- const dpr = globalThis.devicePixelRatio;
92
- const firstRendering = (width, height) => {
93
- setDprConfig(canvas, context, width, height, dpr);
94
- widthRef.current = width;
95
- heightRef.current = height;
96
- if (init) init(context, {
97
- width,
98
- height
99
- });
100
- const now = performance.now();
101
- deltaMemoRef.current = now;
102
- startTimeRef.current = now;
103
- loop(context, widthRef, heightRef, -1);
69
+ latest.current = {
70
+ draw,
71
+ init,
72
+ animate,
73
+ width,
74
+ height,
75
+ reduceFlickering,
76
+ options
104
77
  };
105
- if (relativeSize) {
106
- const parent = canvas.parentElement;
107
- if (!parent) throw new Error("Canvas doesn't have a parent element.");
108
- const memoCanvas = memoCanvasRef.current;
109
- const memoContext = memoCanvas?.getContext("2d", options);
110
- const ro = new ResizeObserver((entries) => {
111
- for (const entry of entries) {
112
- const w = entry.contentRect.width;
113
- const h = entry.contentRect.height;
114
- const contextMemo = {};
115
- if (reduceFlickering && memoCanvas && memoContext) {
116
- for (const prop of drawingState) contextMemo[prop] = context[prop];
117
- memoCanvas.width = w * dpr;
118
- memoCanvas.height = h * dpr;
119
- memoContext.scale(1 / dpr, 1 / dpr);
120
- if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
121
- }
122
- setDprConfig(canvas, context, w, h, dpr);
123
- widthRef.current = w;
124
- heightRef.current = h;
125
- if (reduceFlickering && memoContext && memoCanvas && memoCanvas.width > 0 && memoCanvas.height > 0) {
126
- for (const prop of drawingState) context[prop] = contextMemo[prop];
127
- context.drawImage(memoContext.canvas, 0, 0);
128
- }
129
- if (!animate) loop(context, widthRef, heightRef, -1);
130
- }
131
- });
132
- ro.observe(parent);
133
- const width = parent.clientWidth;
134
- const height = parent.clientHeight;
135
- firstRendering(width, height);
136
- return () => {
137
- if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
138
- ro.disconnect();
139
- };
140
- } else {
141
- const { width, height } = canvas.getBoundingClientRect();
142
- firstRendering(width, height);
143
- return () => {
144
- if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
145
- };
78
+ if (justCreated.current) {
79
+ justCreated.current = false;
80
+ return;
146
81
  }
147
- }, [
148
- loop,
149
- init,
150
- options,
151
- reduceFlickering,
152
- relativeSize,
153
- animate
154
- ]);
155
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
82
+ instanceRef.current?.update({
83
+ animate,
84
+ size: {
85
+ width,
86
+ height
87
+ },
88
+ reduceFlickering
89
+ });
90
+ });
91
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
156
92
  className: (0, clsx.default)("tremolo-animation-canvas", className),
157
- width: relativeSize ? 0 : _width,
158
- height: relativeSize ? 0 : _height,
159
- ref: canvasRef,
93
+ ref: setNode,
160
94
  onContextMenu,
161
95
  ...props
162
- }), relativeSize && reduceFlickering && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("canvas", {
163
- style: { display: "none" },
164
- ref: memoCanvasRef
165
- })] });
96
+ });
166
97
  }
167
98
  //#endregion
168
- //#region src/hooks/useCallbackRef.ts
99
+ //#region src/hooks/_internal/useCallbackRef.ts
169
100
  /**
170
101
  * Internal
171
102
  * @private
@@ -180,36 +111,71 @@ function useCallbackRef(callback, deps = []) {
180
111
  return (0, react.useCallback)((...args) => callbackRef.current?.(...args), deps);
181
112
  }
182
113
  //#endregion
183
- //#region src/hooks/useDrag.ts
114
+ //#region src/hooks/useDragValue.ts
184
115
  /**
185
- * Track a pointer drag on an element.
116
+ * Drive a value with a pointer drag.
186
117
  *
187
- * @returns a ref callback to attach to the element being dragged
118
+ * @returns a ref callback for the element that starts the drag, and whether a drag is in progress
188
119
  */
189
- function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
190
- const dragHandler = useCallbackRef(onDrag);
191
- const dragStartHandler = useCallbackRef(onDragStart);
192
- const dragEndHandler = useCallbackRef(onDragEnd);
120
+ function useDragValue(options) {
121
+ const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor } = options;
122
+ if (!baseElementRef && !options.getValue) throw new Error("useDragValue: give either baseElementRef or getValue, so that the drag has something to move");
123
+ const [dragging, setDragging] = (0, react.useState)(false);
124
+ const valueGetter = useCallbackRef(options.getValue);
125
+ const changeHandler = useCallbackRef(options.onChange);
126
+ const dragStartHandler = useCallbackRef(options.onDragStart);
127
+ const dragEndHandler = useCallbackRef(options.onDragEnd);
128
+ const latest = (0, react.useRef)(options);
129
+ const instanceRef = (0, react.useRef)(null);
193
130
  const [node, setNode] = (0, react.useState)(null);
131
+ const [pixelRangeX, pixelRangeY] = Array.isArray(pixelRange) ? pixelRange : [pixelRange, pixelRange];
194
132
  (0, react.useEffect)(() => {
195
133
  if (!node) return;
196
- const instance = (0, _tremolo_ui_dom.createDrag)(node, {
197
- threshold,
198
- cursor,
199
- onDragStart: () => dragStartHandler(),
200
- onDrag: ({ x, y, deltaX, deltaY }) => dragHandler(x, y, deltaX, deltaY),
201
- onDragEnd: () => dragEndHandler()
134
+ const instance = (0, _tremolo_ui_dom.createDragValue)(node, {
135
+ axis: latest.current.axis,
136
+ mapping: baseElementRef ? (0, _tremolo_ui_dom.elementMapping)(() => baseElementRef.current) : (0, _tremolo_ui_dom.relativeMapping)({ pixelRange: [pixelRangeX ?? 100, pixelRangeY ?? 100] }),
137
+ getValue: () => valueGetter() ?? [0, 0],
138
+ updateOnPointerDown: latest.current.updateOnPointerDown,
139
+ threshold: latest.current.threshold,
140
+ cursor: latest.current.cursor,
141
+ onChange: (value) => changeHandler(value),
142
+ onDragStart: (value) => {
143
+ setDragging(true);
144
+ dragStartHandler(value);
145
+ },
146
+ onDragEnd: (value) => {
147
+ setDragging(false);
148
+ dragEndHandler(value);
149
+ }
202
150
  });
203
- return () => instance.destroy();
151
+ instanceRef.current = instance;
152
+ return () => {
153
+ instanceRef.current = null;
154
+ instance.destroy();
155
+ };
204
156
  }, [
205
157
  node,
206
- threshold,
207
- cursor,
208
- dragHandler,
158
+ baseElementRef,
159
+ pixelRangeX,
160
+ pixelRangeY,
161
+ valueGetter,
162
+ changeHandler,
209
163
  dragStartHandler,
210
164
  dragEndHandler
211
165
  ]);
212
- return setNode;
166
+ (0, react.useEffect)(() => {
167
+ latest.current = options;
168
+ instanceRef.current?.update({
169
+ axis,
170
+ updateOnPointerDown,
171
+ threshold,
172
+ cursor
173
+ });
174
+ });
175
+ return {
176
+ refCallback: setNode,
177
+ dragging
178
+ };
213
179
  }
214
180
  //#endregion
215
181
  //#region src/hooks/useWheel.ts
@@ -218,16 +184,22 @@ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
218
184
  *
219
185
  * The listener is not passive, so the handler may call `preventDefault()`.
220
186
  *
221
- * @returns a ref callback to attach to the element
187
+ * @returns a ref callback to attach to the element, unused when `target` is given
222
188
  */
223
- function useWheel(onWheel) {
189
+ function useWheel(onWheel, { requireFocus = false, target } = {}) {
224
190
  const wheelHandler = useCallbackRef(onWheel);
225
191
  const [node, setNode] = (0, react.useState)(null);
226
192
  (0, react.useEffect)(() => {
227
- if (!node) return;
228
- const instance = (0, _tremolo_ui_dom.createWheel)(node, (event) => wheelHandler(event));
193
+ const element = target ? target.current : node;
194
+ if (!element) return;
195
+ const instance = (0, _tremolo_ui_dom.createWheel)(element, (event) => wheelHandler(event), { requireFocus });
229
196
  return () => instance.destroy();
230
- }, [node, wheelHandler]);
197
+ }, [
198
+ node,
199
+ target,
200
+ wheelHandler,
201
+ requireFocus
202
+ ]);
231
203
  return setNode;
232
204
  }
233
205
  //#endregion
@@ -238,68 +210,92 @@ function addUserSelectNone() {
238
210
  function removeUserSelectNone() {
239
211
  window.document.body.classList.remove("tremolo-user-select-none");
240
212
  }
241
- function calcDrawingState({ value, min, max, skew, startValue, angleRange }) {
242
- const p = (0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew);
243
- const s = (0, _tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
213
+ //#endregion
214
+ //#region src/components/_util/composeRefs.tsx
215
+ /**
216
+ * Set a given ref to a given value
217
+ * This utility takes care of different types of refs: callback refs and RefObject(s)
218
+ */
219
+ function setRef(ref, value) {
220
+ if (typeof ref === "function") return ref(value);
221
+ else if (ref !== null && ref !== void 0) ref.current = value;
222
+ }
223
+ /**
224
+ * A utility to compose multiple refs together
225
+ * Accepts callback refs and RefObject(s)
226
+ */
227
+ function composeRefs(...refs) {
228
+ return (node) => {
229
+ let hasCleanup = false;
230
+ const cleanups = refs.map((ref) => {
231
+ const cleanup = setRef(ref, node);
232
+ if (!hasCleanup && typeof cleanup == "function") hasCleanup = true;
233
+ return cleanup;
234
+ });
235
+ if (hasCleanup) return () => {
236
+ for (let i = 0; i < cleanups.length; i++) {
237
+ const cleanup = cleanups[i];
238
+ if (typeof cleanup == "function") cleanup();
239
+ else setRef(refs[i], null);
240
+ }
241
+ };
242
+ };
243
+ }
244
+ /**
245
+ * A custom hook that composes multiple refs
246
+ * Accepts callback refs and RefObject(s)
247
+ */
248
+ function useComposedRefs(...refs) {
249
+ return (0, react.useCallback)(composeRefs(...refs), refs);
250
+ }
251
+ /**
252
+ * 角度から円弧上の点を求める。
253
+ *
254
+ * 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
255
+ * それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
256
+ */
257
+ function pointOnArc(angle, radius) {
258
+ return {
259
+ x: 50 + radius * Math.cos((0, _tremolo_ui_functions.radian)(angle - 90)),
260
+ y: 50 + radius * Math.sin((0, _tremolo_ui_functions.radian)(angle - 90))
261
+ };
262
+ }
263
+ /** 線の太さが viewBox からはみ出さないようにした半径 */
264
+ function arcRadius(strokeWidth) {
265
+ const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
266
+ return Number.isFinite(width) ? 50 - width / 2 : 50;
267
+ }
268
+ /** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
269
+ function calcAngles({ value, min, max, scale, startValue, angleRange }) {
270
+ const p = scale.normalize(value, min, max);
271
+ const s = scale.normalize(startValue, min, max);
244
272
  const r1 = -angleRange / 2;
245
- const r2 = r1 + Math.min(p, s) * angleRange;
246
- const r3 = r1 + Math.max(p, s) * angleRange;
247
- const r4 = angleRange / 2;
248
273
  return {
249
274
  p,
250
275
  r1,
251
- r2,
252
- r3,
253
- r4,
254
- x1: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r1 - 90)),
255
- y1: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r1 - 90)),
256
- x2: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r2 - 90)),
257
- y2: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r2 - 90)),
258
- x3: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r3 - 90)),
259
- y3: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r3 - 90)),
260
- x4: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r4 - 90)),
261
- y4: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r4 - 90))
276
+ r2: r1 + Math.min(p, s) * angleRange,
277
+ r3: r1 + Math.max(p, s) * angleRange,
278
+ r4: angleRange / 2
262
279
  };
263
280
  }
264
- const createKnobStore = (initProps) => {
265
- return (0, zustand.createStore)()(() => ({
266
- ...initProps,
267
- ...calcDrawingState(initProps)
268
- }));
269
- };
270
281
  const KnobContext = (0, react.createContext)(null);
271
- function KnobProvider({ children, ...props }) {
272
- const storeRef = (0, react.useRef)(null);
273
- if (!storeRef.current) storeRef.current = createKnobStore(props);
274
- (0, react.useEffect)(() => {
275
- if (storeRef.current) storeRef.current.setState({
276
- ...props,
277
- ...calcDrawingState(props)
278
- });
279
- else storeRef.current = createKnobStore(props);
280
- }, [props]);
281
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnobContext.Provider, {
282
- value: storeRef.current,
283
- children
284
- });
285
- }
282
+ const KnobProvider = KnobContext.Provider;
286
283
  function useKnobContext(selector) {
287
- const store = (0, react.useContext)(KnobContext);
288
- if (!store) throw new Error("Missing KnobContext.Provider in the tree");
289
- return (0, zustand.useStore)(store, selector);
284
+ const context = (0, react.useContext)(KnobContext);
285
+ if (!context) throw new Error("Missing KnobContext.Provider in the tree");
286
+ return selector ? selector(context) : context;
290
287
  }
291
288
  //#endregion
292
289
  //#region src/components/Knob/ActiveLine.tsx
293
290
  function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
294
291
  const r2 = useKnobContext((s) => s.r2);
295
292
  const r3 = useKnobContext((s) => s.r3);
296
- const x2 = useKnobContext((s) => s.x2);
297
- const y2 = useKnobContext((s) => s.y2);
298
- const x3 = useKnobContext((s) => s.x3);
299
- const y3 = useKnobContext((s) => s.y3);
293
+ const radius = arcRadius(strokeWidth);
294
+ const start = pointOnArc(r2, radius);
295
+ const end = pointOnArc(r3, radius);
300
296
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
301
297
  className: (0, clsx.default)("tremolo-knob-active-line", className),
302
- d: `M ${x2} ${y2} A 50 50 -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3} ${y3}`,
298
+ d: `M ${start.x} ${start.y} A ${radius} ${radius} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${end.x} ${end.y}`,
303
299
  fill: "none",
304
300
  stroke,
305
301
  strokeWidth,
@@ -316,24 +312,21 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
316
312
  const r2 = useKnobContext((s) => s.r2);
317
313
  const r3 = useKnobContext((s) => s.r3);
318
314
  const r4 = useKnobContext((s) => s.r4);
319
- const x1 = useKnobContext((s) => s.x1);
320
- const y1 = useKnobContext((s) => s.y1);
321
- const x2 = useKnobContext((s) => s.x2);
322
- const y2 = useKnobContext((s) => s.y2);
323
- const x3 = useKnobContext((s) => s.x3);
324
- const y3 = useKnobContext((s) => s.y3);
325
- const x4 = useKnobContext((s) => s.x4);
326
- const y4 = useKnobContext((s) => s.y4);
315
+ const radius = arcRadius(strokeWidth);
316
+ const p1 = pointOnArc(r1, radius);
317
+ const p2 = pointOnArc(r2, radius);
318
+ const p3 = pointOnArc(r3, radius);
319
+ const p4 = pointOnArc(r4, radius);
327
320
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [startValue > min && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
328
321
  className: (0, clsx.default)("tremolo-knob-inactive-line", className),
329
- d: `M ${x1} ${y1} A 50 50 -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2} ${y2}`,
322
+ d: `M ${p1.x} ${p1.y} A ${radius} ${radius} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${p2.x} ${p2.y}`,
330
323
  fill: "none",
331
324
  stroke,
332
325
  strokeWidth,
333
326
  ...props
334
327
  }), startValue < max && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
335
328
  className: (0, clsx.default)("tremolo-knob-inactive-line", className),
336
- d: `M ${x3} ${y3} A 50 50 -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4} ${y4}`,
329
+ d: `M ${p3.x} ${p3.y} A ${radius} ${radius} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${p4.x} ${p4.y}`,
337
330
  fill: "none",
338
331
  stroke,
339
332
  strokeWidth,
@@ -341,6 +334,19 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
341
334
  })] });
342
335
  }
343
336
  //#endregion
337
+ //#region src/components/Knob/SVGRoot.tsx
338
+ function SVGRoot({ children, style, ...props }) {
339
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
340
+ viewBox: `0 0 100 100`,
341
+ style: {
342
+ display: "block",
343
+ ...style
344
+ },
345
+ ...props,
346
+ children
347
+ });
348
+ }
349
+ //#endregion
344
350
  //#region src/components/Knob/Thumb.tsx
345
351
  function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
346
352
  const angleRange = useKnobContext((s) => s.angleRange);
@@ -370,63 +376,36 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
370
376
  });
371
377
  }
372
378
  //#endregion
373
- //#region src/components/Knob/SVGRoot.tsx
374
- function SVGRoot({ children, style, ...props }) {
375
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
376
- viewBox: `0 0 100 100`,
377
- style: {
378
- display: "block",
379
- overflow: "visible",
380
- ...style
381
- },
382
- ...props,
383
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
384
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InactiveLine, {}),
385
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveLine, {}),
386
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$2, {})
387
- ] })
388
- });
389
- }
390
- //#endregion
391
379
  //#region src/components/Knob/index.tsx
392
- const defaultExternalStyles$3 = {
380
+ const defaultExternalStyles$2 = {
393
381
  userSelectNone: true,
394
382
  cursor: "grabbing"
395
383
  };
396
384
  /**
385
+ * The wheel only acts while the focus is inside, so that scrolling a page past
386
+ * the control does not change its value.
387
+ */
388
+ const WHEEL_OPTIONS$2 = { requireFocus: true };
389
+ /**
397
390
  * Interactive rotary knob component implemented in SVG.
398
391
  */
399
392
  const Knob = {
400
- Root: (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
401
- const [dragging, setDragging] = (0, react.useState)(false);
402
- const valueRef = (0, react.useRef)(0);
393
+ Root: (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
403
394
  const elmRef = (0, react.useRef)(null);
404
395
  const externalStyles = {
405
- ...defaultExternalStyles$3,
396
+ ...defaultExternalStyles$2,
406
397
  ..._externalStyles
407
398
  };
408
- const onDrag = (0, react.useCallback)((_x, y) => {
409
- if (!onChange || readonly) return;
410
- onChange((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew), step), min, max));
411
- }, [
412
- max,
399
+ const range = (0, react.useMemo)(() => ({
413
400
  min,
414
- onChange,
415
- readonly,
416
- skew,
417
- step
418
- ]);
419
- const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
420
- let newValue;
421
- if (eventType == "normalized") newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
422
- else newValue = value + x;
423
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
424
- }, [
425
401
  max,
402
+ step,
403
+ scale
404
+ }), [
426
405
  min,
427
- skew,
406
+ max,
428
407
  step,
429
- value
408
+ scale
430
409
  ]);
431
410
  const handleKeyDown = (0, react.useCallback)((event) => {
432
411
  if (!keyboard || !onChange || readonly) return;
@@ -438,35 +417,63 @@ const Knob = {
438
417
  "ArrowDown"
439
418
  ].includes(key)) {
440
419
  event.preventDefault();
441
- const x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
442
- onChange(updateValueByEvent(keyboard[0], x));
420
+ onChange((0, _tremolo_ui_functions.applyDelta)(value, key == "ArrowRight" || key == "ArrowUp" ? 1 : -1, keyboard, range));
443
421
  }
444
422
  }, [
445
423
  keyboard,
446
424
  onChange,
447
425
  readonly,
448
- updateValueByEvent
426
+ value,
427
+ range
449
428
  ]);
450
- const dragRefCallback = useDrag({
429
+ const { refCallback: dragRefCallback, dragging } = useDragValue({
430
+ axis: (0, react.useMemo)(() => [range, {
431
+ ...range,
432
+ reverse: true
433
+ }], [range]),
434
+ getValue: () => [value, value],
435
+ threshold: 1,
451
436
  cursor: readonly ? void 0 : externalStyles.cursor,
452
- onDrag,
437
+ onChange: (v) => {
438
+ if (readonly) return;
439
+ onChange?.(v[1]);
440
+ },
453
441
  onDragStart: () => {
454
- setDragging(true);
455
- valueRef.current = (0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew);
456
442
  if (externalStyles.userSelectNone) addUserSelectNone();
457
443
  },
458
444
  onDragEnd: () => {
459
- setDragging(false);
460
445
  if (externalStyles.userSelectNone) removeUserSelectNone();
461
446
  }
462
447
  });
463
- const wheelRefCallback = useWheel((event) => {
448
+ const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, useWheel((event) => {
464
449
  if (!wheel || readonly) return;
465
450
  event.preventDefault();
466
451
  if (!onChange || event.deltaY == 0) return;
467
- const x = Math.sign(event.deltaY) * -wheel[1];
468
- onChange(updateValueByEvent(wheel[0], x));
469
- });
452
+ onChange((0, _tremolo_ui_functions.applyDelta)(value, -Math.sign(event.deltaY), wheel, range));
453
+ }, WHEEL_OPTIONS$2));
454
+ const context = (0, react.useMemo)(() => {
455
+ const config = {
456
+ value,
457
+ min,
458
+ max,
459
+ step,
460
+ scale,
461
+ startValue,
462
+ angleRange
463
+ };
464
+ return {
465
+ ...config,
466
+ ...calcAngles(config)
467
+ };
468
+ }, [
469
+ value,
470
+ min,
471
+ max,
472
+ step,
473
+ scale,
474
+ startValue,
475
+ angleRange
476
+ ]);
470
477
  (0, react.useImperativeHandle)(forwardedRef, () => {
471
478
  return {
472
479
  focus() {
@@ -478,19 +485,9 @@ const Knob = {
478
485
  };
479
486
  }, []);
480
487
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnobProvider, {
481
- value,
482
- min,
483
- max,
484
- step,
485
- skew,
486
- startValue,
487
- angleRange,
488
+ value: context,
488
489
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
489
- ref: (elm) => {
490
- elmRef.current = elm;
491
- wheelRefCallback(elm);
492
- dragRefCallback(elm);
493
- },
490
+ ref: rootRefCallback,
494
491
  className: (0, clsx.default)("tremolo-knob", className),
495
492
  tabIndex: 0,
496
493
  role: "slider",
@@ -515,7 +512,7 @@ const Knob = {
515
512
  onKeyDown?.(event);
516
513
  },
517
514
  ...props,
518
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SVGRoot, {})
515
+ children
519
516
  })
520
517
  });
521
518
  }),
@@ -525,121 +522,18 @@ const Knob = {
525
522
  Thumb: Thumb$2
526
523
  };
527
524
  //#endregion
528
- //#region src/components/NumberInput/type.ts
529
- function selectUnit(units, value) {
530
- let i = 0;
531
- for (; i < units.length; i++) if (Math.abs(units[i][1]) > Math.abs(value)) break;
532
- return units[Math.max(0, i - 1)];
533
- }
534
- function parseValue(inputString, units, digit) {
535
- const str = inputString.trim();
536
- if (!units || typeof units == "string") {
537
- const m = str.match(/-?\d+(\.\d+)?/);
538
- let v = Number(m?.[0] ?? "0");
539
- v = isNaN(v) ? 0 : v;
540
- return {
541
- rawValue: v,
542
- formatValue: (digit != void 0 ? v.toFixed(digit) : v) + (units ?? ""),
543
- unit: units ?? ""
544
- };
545
- }
546
- const unitList = units.map(([u, _s]) => u);
547
- const scaleList = units.map(([_u, s]) => s);
548
- let rawValue = 0;
549
- let unit = unitList[0];
550
- let formatValue = (digit != void 0 ? rawValue.toFixed(digit) : rawValue) + unit;
551
- const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
552
- if (m) {
553
- rawValue = Number(m[1]) || 0;
554
- const uIndex = unitList.indexOf(m[3]);
555
- rawValue *= uIndex != -1 ? scaleList[uIndex] : 1;
556
- const [u, scale] = selectUnit(units, rawValue);
557
- const v = rawValue / scale;
558
- formatValue = (digit != void 0 ? v.toFixed(digit) : v) + u;
559
- unit = u;
560
- }
561
- return {
562
- rawValue,
563
- formatValue,
564
- unit
565
- };
566
- }
567
- //#endregion
568
525
  //#region src/components/NumberInput/context.tsx
569
- function safeClamp(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
570
- return (0, _tremolo_ui_functions.clamp)(value, min, max);
571
- }
572
- const createNumberInputStore = (initProps) => {
573
- const DEFAULT_PROPS = {
574
- value: "",
575
- valueAsNumber: 0,
576
- min: 0,
577
- max: 0,
578
- step: 1,
579
- readonly: false,
580
- keepWithinRange: true,
581
- onChange: (_v, _t) => {}
582
- };
583
- return (0, zustand.createStore)()((set) => ({
584
- ...DEFAULT_PROPS,
585
- ...initProps,
586
- value: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).formatValue,
587
- valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
588
- increment: () => set((state) => {
589
- if (state.readonly) return {};
590
- let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
591
- if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
592
- const v = parseValue(String(next), state.units, state.digit).formatValue;
593
- state?.onChange?.(next, v);
594
- return {
595
- value: v,
596
- valueAsNumber: next
597
- };
598
- }),
599
- decrement: () => set((state) => {
600
- if (state.readonly) return {};
601
- let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
602
- if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
603
- const v = parseValue(String(next), state.units, state.digit).formatValue;
604
- state?.onChange?.(next, v);
605
- return {
606
- value: v,
607
- valueAsNumber: next
608
- };
609
- }),
610
- change: (value) => set((state) => {
611
- if (state.readonly) return {};
612
- const v = value ?? "";
613
- return {
614
- value: v,
615
- valueAsNumber: parseValue(v, state.units, state.digit).rawValue
616
- };
617
- })
618
- }));
619
- };
620
526
  const NumberInputContext = (0, react.createContext)(null);
621
- function NumberInputProvider({ children, ...props }) {
622
- const storeRef = (0, react.useRef)(null);
623
- if (!storeRef.current) storeRef.current = createNumberInputStore(props);
624
- (0, react.useEffect)(() => {
625
- if (storeRef.current) {
626
- const parsed = parseValue(String(props?.value || ""), props?.units, props?.digit);
627
- storeRef.current.setState({
628
- ...props,
629
- value: parsed.formatValue,
630
- valueAsNumber: parsed.rawValue
631
- });
632
- } else storeRef.current = createNumberInputStore(props);
633
- }, [props]);
634
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputContext.Provider, {
635
- value: storeRef.current,
636
- children
637
- });
638
- }
527
+ const NumberInputProvider = NumberInputContext.Provider;
639
528
  function useNumberInputContext(selector) {
640
- const store = (0, react.useContext)(NumberInputContext);
641
- if (!store) throw new Error("Missing NumberInputContext.Provider in the tree");
642
- return (0, zustand.useStore)(store, selector);
529
+ const context = (0, react.useContext)(NumberInputContext);
530
+ if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
531
+ return selector ? selector(context) : context;
532
+ }
533
+ const StepperContext = (0, react.createContext)(null);
534
+ const StepperProvider = StepperContext.Provider;
535
+ function useStepperContext() {
536
+ return (0, react.useContext)(StepperContext);
643
537
  }
644
538
  //#endregion
645
539
  //#region src/hooks/useEventListener.ts
@@ -698,653 +592,554 @@ function useLongPress(callback, initialDelay = 500, interval = 40) {
698
592
  }, [callback]);
699
593
  }
700
594
  //#endregion
701
- //#region src/components/NumberInput/DecrementStepper.tsx
702
- function DecrementStepper({ size = 12, children, className, ...props }) {
703
- const min = useNumberInputContext((s) => s.min);
704
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
705
- const readonly = useNumberInputContext((s) => s.readonly);
706
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
707
- const press = useLongPress(useNumberInputContext((s) => s.decrement));
595
+ //#region src/components/NumberInput/stepperButton.tsx
596
+ /**
597
+ * Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
598
+ * in which way they move the value and which arrow they draw.
599
+ */
600
+ function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
601
+ const { step, readonly, atMin, atMax, nudge } = useNumberInputContext();
602
+ const stepper = useStepperContext();
603
+ const press = useLongPress((0, react.useCallback)(() => {
604
+ if (stepper?.draggingRef.current) return;
605
+ nudge(direction, ["raw", step]);
606
+ }, [
607
+ stepper,
608
+ nudge,
609
+ direction,
610
+ step
611
+ ]));
708
612
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
709
- className: (0, clsx.default)("tremolo-number-input-decrement-stepper", className),
613
+ className: (0, clsx.default)(`tremolo-number-input-${variant}-stepper`, className),
710
614
  role: "button",
711
615
  tabIndex: -1,
712
- "aria-disabled": keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER),
616
+ "aria-label": direction > 0 ? "Increment" : "Decrement",
617
+ "aria-disabled": direction > 0 ? atMax : atMin,
713
618
  "aria-readonly": readonly,
714
- onPointerDown: press,
619
+ style,
620
+ onPointerDown: (event) => {
621
+ if (!readonly) press();
622
+ onPointerDown?.(event);
623
+ },
715
624
  ...props,
716
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
717
- width: size,
718
- height: size,
719
- viewBox: "0 0 24 24",
720
- fill: "none",
721
- stroke: "currentColor",
722
- strokeWidth: "2",
723
- strokeLinecap: "round",
724
- strokeLinejoin: "round",
725
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "6 9 12 15 18 9" })
726
- })
625
+ children: children ?? icon
626
+ });
627
+ }
628
+ function StepperArrow({ up }) {
629
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
630
+ className: "tremolo-number-input-stepper-icon",
631
+ viewBox: "0 0 24 24",
632
+ fill: "none",
633
+ stroke: "currentColor",
634
+ strokeWidth: "2",
635
+ strokeLinecap: "round",
636
+ strokeLinejoin: "round",
637
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: up ? "18 15 12 9 6 15" : "6 9 12 15 18 9" })
727
638
  });
728
639
  }
729
640
  //#endregion
730
- //#region src/components/NumberInput/IncrementStepper.tsx
731
- function IncrementStepper({ size = 12, children, className, ...props }) {
732
- const max = useNumberInputContext((s) => s.max);
733
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
734
- const readonly = useNumberInputContext((s) => s.readonly);
735
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
736
- const press = useLongPress(useNumberInputContext((s) => s.increment));
737
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
738
- className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
739
- role: "button",
740
- tabIndex: -1,
741
- "aria-disabled": keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER),
742
- "aria-readonly": readonly,
743
- onPointerDown: press,
744
- ...props,
745
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
746
- width: size,
747
- height: size,
748
- viewBox: "0 0 24 24",
749
- fill: "none",
750
- stroke: "currentColor",
751
- strokeWidth: "2",
752
- strokeLinecap: "round",
753
- strokeLinejoin: "round",
754
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "18 15 12 9 6 15" })
755
- })
641
+ //#region src/components/NumberInput/DecrementStepper.tsx
642
+ /** Lowers the value by one `step`, repeating while held. */
643
+ function DecrementStepper(props) {
644
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperButton, {
645
+ direction: -1,
646
+ variant: "decrement",
647
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperArrow, { up: false }),
648
+ ...props
756
649
  });
757
650
  }
758
651
  //#endregion
759
- //#region src/components/NumberInput/InternalInput.tsx
760
- const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFocus, blurOnEnter, clampValueOnBlur, wheel, keyboard, className, onChange, onFocus, onBlur, onKeyDown, children: _children, ...props }, ref) => {
761
- const elmRef = (0, react.useRef)(null);
762
- const [editing, setEditing] = (0, react.useState)(false);
763
- const value = useNumberInputContext((s) => s.value);
764
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
765
- const min = useNumberInputContext((s) => s.min);
766
- const max = useNumberInputContext((s) => s.max);
767
- const step = useNumberInputContext((s) => s.step);
768
- const units = useNumberInputContext((s) => s.units);
769
- const digit = useNumberInputContext((s) => s.digit);
770
- const change = useNumberInputContext((s) => s.change);
771
- const error = (0, react.useMemo)(() => {
772
- if (editing) return false;
773
- const v = valueAsNumber;
774
- return (min ?? -Infinity) > v || v > (max ?? Infinity);
775
- }, [
776
- editing,
777
- max,
778
- min,
779
- valueAsNumber
780
- ]);
781
- const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
782
- let newValue;
783
- if (eventType == "normalized") {
784
- if (!min || !max) throw new Error("[NumberInput] \"min\" and \"max\" are required when InputEventOption[0] is set to \"normalized\".");
785
- newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max) + x, min, max);
786
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
787
- } else {
788
- newValue = valueAsNumber + x;
789
- return (0, _tremolo_ui_functions.stepValue)(newValue, step);
790
- }
791
- }, [
792
- max,
793
- min,
794
- step,
795
- valueAsNumber
796
- ]);
797
- const handleKeyDown = (0, react.useCallback)((event) => {
798
- if (!keyboard || !onChange || readonly) return;
799
- const key = event.key;
800
- if (!["ArrowUp", "ArrowDown"].includes(key)) return;
801
- event.preventDefault();
802
- const x = key == "ArrowUp" ? keyboard[1] : -keyboard[1];
803
- const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
804
- onChange?.(parsed.rawValue, parsed.formatValue);
805
- }, [
806
- keyboard,
807
- readonly,
808
- onChange,
809
- updateValueByEvent
810
- ]);
811
- const wheelRefCallback = useWheel((event) => {
812
- if (!wheel || readonly) return;
813
- event.preventDefault();
814
- if (!onChange || event.deltaY == 0) return;
815
- const x = Math.sign(event.deltaY) * -wheel[1];
816
- const parsed = parseValue(String(updateValueByEvent(wheel[0], x)));
817
- onChange?.(parsed.rawValue, parsed.formatValue);
652
+ //#region src/components/NumberInput/IncrementStepper.tsx
653
+ /** Raises the value by one `step`, repeating while held. */
654
+ function IncrementStepper(props) {
655
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperButton, {
656
+ direction: 1,
657
+ variant: "increment",
658
+ icon: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperArrow, { up: true }),
659
+ ...props
818
660
  });
819
- (0, react.useImperativeHandle)(ref, () => {
820
- return {
821
- focus() {
822
- elmRef.current?.focus();
823
- },
824
- blur() {
825
- elmRef.current?.blur();
826
- }
827
- };
828
- }, []);
661
+ }
662
+ //#endregion
663
+ //#region src/components/NumberInput/InputField.tsx
664
+ /** The leading number of the displayed text, whatever the format put around it. */
665
+ const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
666
+ /**
667
+ * The text field of a `NumberInput`, and the only place the value can be typed.
668
+ *
669
+ * While the user types, their own text stands rather than `format(value)`, so
670
+ * that a half-finished entry is not rewritten under the caret.
671
+ */
672
+ function InputField({ selectOnFocus = "none", blurOnEnter = true, className, style, ref, onFocus, onBlur, onKeyDown, ...props }) {
673
+ const { value, min, max, step, disabled, readonly, keyboard, text, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
829
674
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
830
- ref: (input) => {
831
- elmRef.current = input;
832
- wheelRefCallback(input);
833
- },
834
- className: (0, clsx.default)("tremolo-number-input", className),
835
- value,
836
- min,
837
- max,
838
- step,
675
+ ref: useComposedRefs(ref, inputRef),
676
+ className: (0, clsx.default)("tremolo-number-input-field", className),
839
677
  type: "text",
678
+ inputMode: "decimal",
679
+ role: "spinbutton",
680
+ value: text,
681
+ readOnly: readonly,
840
682
  "aria-disabled": disabled,
841
683
  "aria-readonly": readonly,
842
- tabIndex: -1,
843
- "data-error": error,
844
- onChange: (event) => {
845
- if (readonly) return;
846
- const v = event.currentTarget.value;
847
- const cursorPos = event.target.selectionStart;
848
- change(v);
849
- const valueAsNumber = parseValue(v, units, digit).rawValue;
850
- onChange?.(valueAsNumber, v);
851
- if (cursorPos) {
852
- event.target.selectionStart = cursorPos;
853
- event.target.selectionEnd = cursorPos;
854
- }
855
- },
684
+ "aria-valuenow": value,
685
+ "aria-valuemin": min,
686
+ "aria-valuemax": max,
687
+ "aria-valuetext": text,
688
+ step,
689
+ "data-out-of-range": outOfRange,
690
+ style,
691
+ onChange: (event) => setDraft(event.currentTarget.value),
856
692
  onFocus: (event) => {
857
- setEditing(true);
858
- const v = event.currentTarget.value;
859
- const parsed = parseValue(v, units, digit);
860
- if (selectWithFocus == "all") event.currentTarget.setSelectionRange(0, v.length);
861
- else if (selectWithFocus == "number") event.currentTarget.setSelectionRange(0, parsed.formatValue.length - parsed.unit.length);
862
- onFocus?.(parsed.rawValue, parsed.formatValue, event);
693
+ const input = event.currentTarget;
694
+ if (selectOnFocus == "all") input.setSelectionRange(0, input.value.length);
695
+ else if (selectOnFocus == "number") input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
696
+ onFocus?.(event);
863
697
  },
864
698
  onBlur: (event) => {
865
- setEditing(false);
866
- let v = parseValue(event.currentTarget.value, units, digit).rawValue;
867
- if (clampValueOnBlur) v = safeClamp(v, min, max);
868
- const parsed = parseValue(String(v), units, digit);
869
- change(parsed.formatValue);
870
- if (clampValueOnBlur) onChange?.(parsed.rawValue, parsed.formatValue);
871
- onBlur?.(parsed.rawValue, parsed.formatValue, event);
699
+ commitDraft();
700
+ onBlur?.(event);
872
701
  },
873
702
  onKeyDown: (event) => {
874
- if (blurOnEnter && event.key == "Enter") event.currentTarget.blur();
875
- handleKeyDown(event);
703
+ const key = event.key;
704
+ if (key == "Enter") {
705
+ commitDraft();
706
+ if (blurOnEnter) event.currentTarget.blur();
707
+ } else if (keyboard && !readonly && (key == "ArrowUp" || key == "ArrowDown")) {
708
+ event.preventDefault();
709
+ nudge(key == "ArrowUp" ? 1 : -1, keyboard);
710
+ }
876
711
  onKeyDown?.(event);
877
712
  },
878
713
  ...props
879
714
  });
880
- });
715
+ }
716
+ //#endregion
717
+ //#region src/hooks/useDrag.ts
718
+ /**
719
+ * Track a pointer drag on an element.
720
+ *
721
+ * @returns a ref callback to attach to the element being dragged
722
+ */
723
+ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
724
+ const dragHandler = useCallbackRef(onDrag);
725
+ const dragStartHandler = useCallbackRef(onDragStart);
726
+ const dragEndHandler = useCallbackRef(onDragEnd);
727
+ const [node, setNode] = (0, react.useState)(null);
728
+ (0, react.useEffect)(() => {
729
+ if (!node) return;
730
+ const instance = (0, _tremolo_ui_dom.createDrag)(node, {
731
+ threshold,
732
+ cursor,
733
+ onDragStart: () => dragStartHandler(),
734
+ onDrag: ({ x, y, deltaX, deltaY }) => dragHandler(x, y, deltaX, deltaY),
735
+ onDragEnd: () => dragEndHandler()
736
+ });
737
+ return () => instance.destroy();
738
+ }, [
739
+ node,
740
+ threshold,
741
+ cursor,
742
+ dragHandler,
743
+ dragStartHandler,
744
+ dragEndHandler
745
+ ]);
746
+ return setNode;
747
+ }
881
748
  //#endregion
882
749
  //#region src/components/NumberInput/Stepper.tsx
883
- function Stepper({ dynamic = true, children, className, ...props }) {
884
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
885
- className: (0, clsx.default)("tremolo-number-input-stepper", className),
886
- "data-dynamic": dynamic,
887
- ...props,
888
- children
750
+ /**
751
+ * The area the steppers sit in, and a drag handle in its own right: dragging it
752
+ * up and down moves the value one `step` every `drag` pixels.
753
+ *
754
+ * The drag lives here rather than on `InputField` because `createDrag` turns
755
+ * off text selection on whatever element it is attached to.
756
+ */
757
+ function Stepper({ className, style, children, ref, ...props }) {
758
+ const { value, step, readonly, drag, range, changeValue } = useNumberInputContext();
759
+ /**
760
+ * Set once the drag has actually moved the value, so that the press-and-hold
761
+ * repeat of a stepper stands down and leaves the value to the drag. A ref,
762
+ * because it is read from inside the repeat rather than rendered.
763
+ */
764
+ const draggingRef = (0, react.useRef)(false);
765
+ /**
766
+ * Where the drag started, taken on the first move rather than on pointer
767
+ * down: the steppers act on pointer down, so by then the value may already
768
+ * have been nudged once, and the drag should carry on from there.
769
+ */
770
+ const originRef = (0, react.useRef)(null);
771
+ const dragRefCallback = useDrag({
772
+ threshold: 1,
773
+ cursor: readonly ? void 0 : "ns-resize",
774
+ onDragStart: () => {
775
+ originRef.current = null;
776
+ draggingRef.current = false;
777
+ },
778
+ onDrag: (_x, y) => {
779
+ if (readonly || drag == null) return;
780
+ if (!originRef.current) {
781
+ originRef.current = {
782
+ y,
783
+ value
784
+ };
785
+ return;
786
+ }
787
+ const steps = Math.round(-(y - originRef.current.y) / drag);
788
+ if (steps == 0) return;
789
+ draggingRef.current = true;
790
+ changeValue((0, _tremolo_ui_functions.applyDelta)(originRef.current.value, steps, ["raw", step], range));
791
+ },
792
+ onDragEnd: () => {
793
+ draggingRef.current = false;
794
+ }
795
+ });
796
+ const composedRef = useComposedRefs(ref, drag != null && !readonly ? dragRefCallback : void 0);
797
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperProvider, {
798
+ value: (0, react.useMemo)(() => ({ draggingRef }), []),
799
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
800
+ ref: composedRef,
801
+ className: (0, clsx.default)("tremolo-number-input-stepper", className),
802
+ style,
803
+ ...props,
804
+ children
805
+ })
889
806
  });
890
807
  }
891
808
  /**
892
809
  * Input with some useful functions for entering numerical values.
893
810
  */
894
811
  const NumberInput = {
895
- Root: (0, react.forwardRef)(({ value, min, max, step = 1, units, readonly = false, disabled = false, digit, variant = "outline", selectWithFocus = "none", blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ["raw", 1], keyboard = ["raw", 1], activeColor, wrapperClassName, className, onChange, onFocus, onBlur, children, ...props }, forwardedRef) => {
896
- const colors = { "--active-color": activeColor };
897
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
898
- value: String(value),
812
+ Root: (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, units, digit, format: formatProp, parse: parseProp, clampValue = true, wheel = ["raw", 1], keyboard = ["raw", 1], drag = 1, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
813
+ const inputRef = (0, react.useRef)(null);
814
+ /**
815
+ * The text being typed. The only state here: it is not a copy of `value`,
816
+ * but the half-finished entry that has no value to be derived from yet.
817
+ */
818
+ const [draft, setDraft] = (0, react.useState)(null);
819
+ const format = (0, react.useCallback)((v) => formatProp ? formatProp(v) : (0, _tremolo_ui_functions.formatValue)(v, units, digit), [
820
+ formatProp,
821
+ units,
822
+ digit
823
+ ]);
824
+ const parse = (0, react.useCallback)((t) => parseProp ? parseProp(t) : (0, _tremolo_ui_functions.parseValue)(t, units), [parseProp, units]);
825
+ const range = (0, react.useMemo)(() => ({
826
+ min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
827
+ max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
828
+ step,
829
+ scale
830
+ }), [
831
+ clampValue,
899
832
  min,
900
833
  max,
901
834
  step,
902
- units,
903
- digit,
835
+ scale
836
+ ]);
837
+ const text = draft ?? format(value);
838
+ const editing = draft != null;
839
+ const outOfRange = !editing && (min != void 0 && value < min || max != void 0 && value > max);
840
+ const handleDraft = (0, react.useCallback)((next) => {
841
+ if (readonly) return;
842
+ setDraft(next);
843
+ const parsed = parse(next);
844
+ if (Number.isFinite(parsed)) onChange?.(parsed);
845
+ }, [
904
846
  readonly,
905
- keepWithinRange,
906
- onChange,
907
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
908
- className: (0, clsx.default)("tremolo-number-input-wrapper", wrapperClassName),
909
- tabIndex: 0,
910
- style: { ...colors },
911
- "data-stepper": !!children,
912
- "data-variant": variant,
913
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InternalInput, {
914
- ref: forwardedRef,
915
- readonly,
916
- disabled,
917
- selectWithFocus,
918
- blurOnEnter,
919
- clampValueOnBlur,
920
- wheel,
921
- keyboard,
922
- className,
923
- onChange,
924
- onFocus,
925
- onBlur,
926
- ...props
927
- }), children]
847
+ parse,
848
+ onChange
849
+ ]);
850
+ const changeValue = (0, react.useCallback)((next) => {
851
+ if (readonly) return;
852
+ setDraft(null);
853
+ if (next != value) onChange?.(next);
854
+ }, [
855
+ readonly,
856
+ value,
857
+ onChange
858
+ ]);
859
+ const commitDraft = (0, react.useCallback)(() => {
860
+ if (draft == null || readonly) return;
861
+ changeValue((0, _tremolo_ui_functions.clamp)(parse(draft), range.min, range.max));
862
+ }, [
863
+ draft,
864
+ readonly,
865
+ parse,
866
+ range,
867
+ changeValue
868
+ ]);
869
+ const nudge = (0, react.useCallback)((direction, option) => {
870
+ changeValue((0, _tremolo_ui_functions.applyDelta)(value, direction, option, range));
871
+ }, [
872
+ changeValue,
873
+ value,
874
+ range
875
+ ]);
876
+ const wheelRefCallback = useWheel((event) => {
877
+ if (!wheel || readonly || event.deltaY == 0) return;
878
+ event.preventDefault();
879
+ nudge(-Math.sign(event.deltaY), wheel);
880
+ }, { requireFocus: true });
881
+ const context = (0, react.useMemo)(() => ({
882
+ value,
883
+ min,
884
+ max,
885
+ step,
886
+ scale,
887
+ disabled,
888
+ readonly,
889
+ clampValue,
890
+ range,
891
+ keyboard,
892
+ text,
893
+ editing,
894
+ outOfRange,
895
+ atMin: clampValue && min != void 0 && value <= min,
896
+ atMax: clampValue && max != void 0 && value >= max,
897
+ drag,
898
+ format,
899
+ parse,
900
+ setDraft: handleDraft,
901
+ commitDraft,
902
+ changeValue,
903
+ nudge,
904
+ inputRef
905
+ }), [
906
+ value,
907
+ min,
908
+ max,
909
+ step,
910
+ scale,
911
+ disabled,
912
+ readonly,
913
+ clampValue,
914
+ range,
915
+ keyboard,
916
+ text,
917
+ editing,
918
+ outOfRange,
919
+ drag,
920
+ format,
921
+ parse,
922
+ handleDraft,
923
+ commitDraft,
924
+ changeValue,
925
+ nudge
926
+ ]);
927
+ (0, react.useImperativeHandle)(forwardedRef, () => {
928
+ return {
929
+ focus() {
930
+ inputRef.current?.focus();
931
+ },
932
+ blur() {
933
+ inputRef.current?.blur();
934
+ }
935
+ };
936
+ }, []);
937
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
938
+ value: context,
939
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
940
+ ref: wheelRefCallback,
941
+ className: (0, clsx.default)("tremolo-number-input", className),
942
+ "aria-disabled": disabled,
943
+ "aria-readonly": readonly,
944
+ style,
945
+ ...props,
946
+ children
928
947
  })
929
948
  });
930
949
  }),
950
+ InputField,
931
951
  Stepper,
932
952
  IncrementStepper,
933
953
  DecrementStepper
934
954
  };
935
955
  //#endregion
936
- //#region src/hooks/useRefCallbackEvent.ts
937
- function useRefCallbackEvent(event, handler, options, deps = []) {
938
- return (0, react.useMemo)(() => {
939
- let cleanup;
940
- return (node) => {
941
- if (!node) {
942
- cleanup?.();
943
- return;
944
- }
945
- node.addEventListener(event, handler, options);
946
- cleanup = () => {
947
- node.removeEventListener(event, handler, options);
948
- };
949
- };
950
- }, deps);
951
- }
952
- //#endregion
953
- //#region src/hooks/usePianoDrag.ts
956
+ //#region src/components/Piano/keyboardShortcuts.ts
954
957
  /**
955
- * Internal
956
- * @returns [refCallback, pointerDownHandler]
957
- * @private
958
+ * Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
958
959
  */
959
- function usePianoDrag({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
960
- const dragged = (0, react.useRef)(false);
961
- const normalizedX = (0, react.useRef)(0);
962
- const normalizedY = (0, react.useRef)(0);
963
- const handleDrag = (0, react.useCallback)((event) => {
964
- if (!baseElementRef.current || !dragged.current) return;
965
- const isTouch = event instanceof TouchEvent;
966
- if (isTouch && event.cancelable) event.preventDefault();
967
- const { left, top, right, bottom } = baseElementRef.current.getBoundingClientRect();
968
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
969
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
970
- const nx = (0, _tremolo_ui_functions.normalizeValue)(mouseX, left, right);
971
- const ny = (0, _tremolo_ui_functions.normalizeValue)(mouseY, top, bottom);
972
- normalizedX.current = nx;
973
- normalizedY.current = ny;
974
- onDrag(nx, ny);
975
- }, [onDrag, baseElementRef]);
976
- const refHandler = useRefCallbackEvent("touchmove", handleDrag, { passive: false }, [onDrag]);
977
- const pointerDownHandler = (0, react.useCallback)((event) => {
978
- dragged.current = true;
979
- handleDrag(event);
980
- onDragStart?.(normalizedX.current, normalizedY.current);
981
- }, [handleDrag, onDragStart]);
982
- useEventListener(globalThis.window, "pointermove", handleDrag);
983
- useEventListener(globalThis.window, "pointerup", () => {
984
- if (!dragged.current) return;
985
- dragged.current = false;
986
- onDragEnd?.(normalizedX.current, normalizedY.current);
987
- });
988
- return {
989
- refHandler,
990
- pointerDownHandler
991
- };
992
- }
993
- //#endregion
994
- //#region src/components/Piano/context.tsx
995
- const createPianoStore = (initProps) => {
996
- const DEFAULT_PROPS = {
997
- noteRange: {
998
- first: 0,
999
- last: 127
1000
- },
1001
- glissando: true,
1002
- midiMax: 127,
1003
- fill: false,
1004
- label: () => void 0,
1005
- onPlayNote: () => {},
1006
- onStopNote: () => {}
1007
- };
1008
- return (0, zustand.createStore)()(() => ({
1009
- ...DEFAULT_PROPS,
1010
- ...initProps,
1011
- notePosition: initProps.notePosition
1012
- }));
960
+ const SHORTCUTS = {
961
+ HOME_ROW: { keys: [
962
+ "a",
963
+ "w",
964
+ "s",
965
+ "e",
966
+ "d",
967
+ "f",
968
+ "t",
969
+ "g",
970
+ "y",
971
+ "h",
972
+ "u",
973
+ "j",
974
+ "k",
975
+ "o",
976
+ "l",
977
+ "p",
978
+ ";"
979
+ ] },
980
+ HOME_ROW_NATURAL: { keys: [
981
+ "a",
982
+ "",
983
+ "s",
984
+ "",
985
+ "d",
986
+ "f",
987
+ "",
988
+ "g",
989
+ "",
990
+ "h",
991
+ "",
992
+ "j",
993
+ "k",
994
+ "",
995
+ "l",
996
+ "",
997
+ ";"
998
+ ] }
1013
999
  };
1014
- const PianoContext = (0, react.createContext)(null);
1015
- function PianoProvider({ children, ...props }) {
1016
- const storeRef = (0, react.useRef)(null);
1017
- if (!storeRef.current) storeRef.current = createPianoStore(props);
1018
- (0, react.useEffect)(() => {
1019
- if (storeRef.current) storeRef.current.setState(props);
1020
- else storeRef.current = createPianoStore(props);
1021
- }, [props]);
1022
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoContext.Provider, {
1023
- value: storeRef.current,
1024
- children
1025
- });
1026
- }
1027
- function usePianoContext(selector) {
1028
- const store = (0, react.useContext)(PianoContext);
1029
- if (!store) throw new Error("Missing PianoContext.Provider in the tree");
1030
- return (0, zustand.useStore)(store, selector);
1031
- }
1032
- //#endregion
1033
- //#region src/components/Piano/KeyLabel.tsx
1034
- function KeyLabel({ label, className, wrapperClassName, wrapperStyle, __note, __label, ...props }) {
1035
- const index = getNoteRangeArray(usePianoContext((s) => s.noteRange)).indexOf(__note);
1036
- const content = label ? label(__note, index) : __label && __label(__note, index);
1037
- return (content != void 0 || content != null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1038
- className: (0, clsx.default)("tremolo-piano-key-label-wrapper", wrapperClassName),
1039
- style: wrapperStyle,
1040
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1041
- className: (0, clsx.default)("tremolo-piano-key-label", className),
1042
- ...props,
1043
- children: content
1044
- })
1000
+ /**
1001
+ * Customizable piano component.
1002
+ */
1003
+ const Piano = { Root: (0, react.forwardRef)(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, height = fill ? "100%" : 160, style, className, label, keyProps, onPlayNote, onStopNote, ...props }, forwardedRef) {
1004
+ const [node, setNode] = (0, react.useState)(null);
1005
+ const [activeNotes, setActiveNotes] = (0, react.useState)([]);
1006
+ /** Set while `fill` is on, measured from the parent. */
1007
+ const [filledKeyWidth, setFilledKeyWidth] = (0, react.useState)(whiteKeyWidth);
1008
+ const notes = (0, react.useMemo)(() => (0, _tremolo_ui_functions.getNoteRangeArray)(noteRange), [noteRange]);
1009
+ const whiteKeyCount = (0, react.useMemo)(() => notes.filter(_tremolo_ui_functions.isWhiteKey).length, [notes]);
1010
+ const layout = (0, react.useMemo)(() => ({
1011
+ noteRange,
1012
+ whiteKeyWidth: fill ? filledKeyWidth : whiteKeyWidth,
1013
+ keyGap,
1014
+ blackKeyWidthRatio,
1015
+ blackKeyHeightRatio
1016
+ }), [
1017
+ noteRange,
1018
+ fill,
1019
+ filledKeyWidth,
1020
+ whiteKeyWidth,
1021
+ keyGap,
1022
+ blackKeyWidthRatio,
1023
+ blackKeyHeightRatio
1024
+ ]);
1025
+ const latest = (0, react.useRef)({
1026
+ layout,
1027
+ glissando,
1028
+ midiMax,
1029
+ onPlayNote,
1030
+ onStopNote
1045
1031
  });
1046
- }
1047
- const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg, color, activeBg, activeColor, className, style, children, __width, ...props }, ref) => {
1048
- const [played, setPlayed] = (0, react.useState)(false);
1049
- const midiMax = usePianoContext((s) => s.midiMax);
1050
- const fill = usePianoContext((s) => s.fill);
1051
- const onPlayNote = usePianoContext((s) => s.onPlayNote);
1052
- const onStopNote = usePianoContext((s) => s.onStopNote);
1053
- const label = usePianoContext((s) => s.label);
1054
- const position = usePianoContext((s) => s.notePosition)(noteNumber);
1055
- const disabled = noteNumber > midiMax;
1056
- const colors = {
1057
- "--bg": bg,
1058
- "--color": color,
1059
- "--active-bg": activeBg,
1060
- "--active-color": activeColor
1061
- };
1062
- (0, react.useImperativeHandle)(ref, () => {
1063
- return {
1064
- play(velocity) {
1065
- if (disabled) return;
1066
- setPlayed(true);
1067
- if (onPlayNote) onPlayNote(noteNumber, velocity);
1068
- },
1069
- stop() {
1070
- setPlayed(false);
1071
- if (onStopNote) onStopNote(noteNumber);
1072
- },
1073
- played() {
1074
- return played;
1075
- }
1032
+ const instanceRef = (0, react.useRef)(null);
1033
+ (0, react.useEffect)(() => {
1034
+ if (!node) return;
1035
+ const instance = (0, _tremolo_ui_dom.createPianoInput)(node, {
1036
+ layout: latest.current.layout,
1037
+ glissando: latest.current.glissando,
1038
+ midiMax: latest.current.midiMax,
1039
+ onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
1040
+ onStopNote: (note) => latest.current.onStopNote?.(note),
1041
+ onActiveNotesChange: setActiveNotes
1042
+ });
1043
+ instanceRef.current = instance;
1044
+ return () => {
1045
+ instanceRef.current = null;
1046
+ instance.destroy();
1076
1047
  };
1048
+ }, [node]);
1049
+ (0, react.useEffect)(() => {
1050
+ latest.current = {
1051
+ layout,
1052
+ glissando,
1053
+ midiMax,
1054
+ onPlayNote,
1055
+ onStopNote
1056
+ };
1057
+ instanceRef.current?.update({
1058
+ layout,
1059
+ glissando,
1060
+ midiMax
1061
+ });
1062
+ });
1063
+ (0, react.useEffect)(() => {
1064
+ if (!fill || !node) return;
1065
+ const parent = node.parentElement;
1066
+ if (!parent) throw new Error("doesn't have a parent element.");
1067
+ const resizeObserver = new ResizeObserver(() => {
1068
+ setFilledKeyWidth(node.clientWidth / whiteKeyCount - keyGap);
1069
+ });
1070
+ resizeObserver.observe(parent);
1071
+ return () => resizeObserver.disconnect();
1077
1072
  }, [
1078
- disabled,
1079
- noteNumber,
1080
- onPlayNote,
1081
- onStopNote,
1082
- played
1073
+ fill,
1074
+ node,
1075
+ whiteKeyCount,
1076
+ keyGap
1083
1077
  ]);
1084
- let keyLabelProps = {};
1085
- if (children != void 0) react.default.Children.forEach(children, (child) => {
1086
- if (react.default.isValidElement(child)) if (child.type == KeyLabel) keyLabelProps = child.props;
1087
- else throw new Error("only <KeyLabel>");
1088
- else throw new Error("children is an invalid element.");
1078
+ /** The note a shortcut key plays, or null when it has none. */
1079
+ function shortcutNote(key) {
1080
+ if (!keyboardShortcuts || key === "") return null;
1081
+ const index = keyboardShortcuts.keys.indexOf(key);
1082
+ return index === -1 ? null : noteRange.first + index;
1083
+ }
1084
+ useEventListener(globalThis.window, "keydown", (e) => {
1085
+ if (e.repeat) return;
1086
+ const note = shortcutNote(e.key);
1087
+ if (note !== null) instanceRef.current?.noteOn(note, { source: "keyboard" });
1089
1088
  });
1089
+ useEventListener(globalThis.window, "keyup", (e) => {
1090
+ const note = shortcutNote(e.key);
1091
+ if (note !== null) instanceRef.current?.noteOff(note, { source: "keyboard" });
1092
+ });
1093
+ (0, react.useImperativeHandle)(forwardedRef, () => ({
1094
+ playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
1095
+ source: "api",
1096
+ velocity
1097
+ }),
1098
+ stopNote: (note) => instanceRef.current?.noteOff(note, { source: "api" })
1099
+ }), []);
1090
1100
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1091
- className: (0, clsx.default)(`tremolo-piano-${keyType}-key`, className),
1092
- "aria-disabled": disabled,
1093
- "data-active": played,
1101
+ ref: setNode,
1102
+ className: (0, clsx.default)("tremolo-piano", className),
1094
1103
  style: {
1095
- ...colors,
1096
- left: position,
1097
- width: fill ? __width : width,
1104
+ width: fill ? "100%" : (0, _tremolo_ui_functions.pianoWidth)(layout),
1098
1105
  height,
1099
1106
  ...style
1100
1107
  },
1101
1108
  ...props,
1102
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyLabel, {
1103
- label,
1104
- __note: noteNumber,
1105
- ...keyLabelProps
1106
- })
1107
- });
1108
- });
1109
- const WhiteKey = (0, react.forwardRef)(({ width = 40, height = "100%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
1110
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyImpl, {
1111
- ref,
1112
- keyType: "white",
1113
- width,
1114
- height,
1115
- onContextMenu,
1116
- ...props
1117
- });
1118
- });
1119
- const BlackKey = (0, react.forwardRef)(({ width = 26, height = "60%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
1120
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyImpl, {
1121
- ref,
1122
- keyType: "black",
1123
- width,
1124
- height,
1125
- onContextMenu,
1126
- ...props
1127
- });
1128
- });
1129
- //#endregion
1130
- //#region src/components/Piano/keyboardShortcuts.ts
1131
- const SHORTCUTS = { HOME_ROW: { keys: [
1132
- "a",
1133
- "w",
1134
- "s",
1135
- "e",
1136
- "d",
1137
- "f",
1138
- "t",
1139
- "g",
1140
- "y",
1141
- "h",
1142
- "u",
1143
- "j",
1144
- "k",
1145
- "o",
1146
- "l",
1147
- "p",
1148
- ";"
1149
- ] } };
1150
- //#endregion
1151
- //#region src/components/Piano/index.tsx
1152
- /**
1153
- * [noteRange.first, noteRange.first + 1 ..., noteRange.last]
1154
- */
1155
- function getNoteRangeArray(noteRange) {
1156
- return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1157
- }
1158
- const blackPerWhiteWidth = 26 / 40;
1159
- /**
1160
- * Customizable piano component.
1161
- */
1162
- const Piano = {
1163
- Root: (0, react.forwardRef)(({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? "100%" : 160, whiteNoteWidth: _whiteNoteWidth = 40, style, className, onPlayNote, onStopNote, label, children, onPointerDown, ...props }, forwardedRef) => {
1164
- const [whiteNoteWidth, setWhiteNoteWidth] = (0, react.useState)(_whiteNoteWidth);
1165
- const keyRefs = (0, react.useRef)([]);
1166
- for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) keyRefs.current[i] = (0, react.createRef)();
1167
- const pianoRef = (0, react.useRef)(null);
1168
- const hitKeyIndex = (0, react.useRef)(-1);
1169
- const noteRangeArray = getNoteRangeArray(noteRange);
1170
- const whiteNotes = noteRangeArray.filter((v) => (0, _tremolo_ui_functions.isWhiteKey)(v));
1171
- const blackNotes = noteRangeArray.filter((v) => (0, _tremolo_ui_functions.isBlackKey)(v));
1172
- const whiteNoteCount = whiteNotes.length;
1173
- const padding = 1;
1174
- const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount;
1175
- const childrenWithProps = (0, react.useMemo)(() => {
1176
- return children && react.default.Children.map(children, (child, index) => {
1177
- if (react.default.isValidElement(child)) {
1178
- const __width = child.type == WhiteKey ? whiteNoteWidth : whiteNoteWidth * .65;
1179
- const props = {
1180
- ref: keyRefs.current[index],
1181
- __width
1182
- };
1183
- return react.default.cloneElement(child, props);
1184
- }
1185
- return child;
1186
- });
1187
- }, [children, whiteNoteWidth]);
1188
- const notePosition = (0, react.useCallback)((note) => {
1189
- const pitchPositions = {
1190
- C: 0,
1191
- "C#": 1,
1192
- D: 1,
1193
- "D#": 2,
1194
- E: 2,
1195
- F: 3,
1196
- "F#": 4,
1197
- G: 4,
1198
- "G#": 5,
1199
- A: 5,
1200
- "A#": 6,
1201
- B: 6
1109
+ children: notes.map((note, index) => {
1110
+ const keyType = (0, _tremolo_ui_functions.isWhiteKey)(note) ? "white" : "black";
1111
+ const state = {
1112
+ index,
1113
+ keyType,
1114
+ active: activeNotes.includes(note),
1115
+ disabled: note > midiMax
1202
1116
  };
1203
- const blackNoteWidth = whiteNoteWidth * blackPerWhiteWidth;
1204
- const padding = 1;
1205
- const targetNoteKey = (0, _tremolo_ui_functions.noteKey)(note);
1206
- const firstNoteKey = (0, _tremolo_ui_functions.noteKey)(noteRange.first);
1207
- const octave = Math.floor((note - noteRange.first) / 12);
1208
- const octaveOffset = _tremolo_ui_functions.noteKeys.indexOf(firstNoteKey) > _tremolo_ui_functions.noteKeys.indexOf(targetNoteKey) ? 1 : 0;
1209
- const w = whiteNoteWidth + padding;
1210
- const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
1211
- const blackKeyOffset = (0, _tremolo_ui_functions.isBlackKey)(note) ? blackNoteWidth / 2 : 0;
1212
- return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
1213
- }, [noteRange.first, whiteNoteWidth]);
1214
- const getHitKeyIndex = (0, react.useCallback)((x, y) => {
1215
- if (!pianoRef.current) return -1;
1216
- const containerHeight = pianoRef.current.clientHeight;
1217
- const notes = [...blackNotes, ...whiteNotes];
1218
- for (let i = 0; i < notes.length; i++) {
1219
- const note = notes[i];
1220
- const pos = notePosition(note);
1221
- const w = (0, _tremolo_ui_functions.isWhiteKey)(note) ? whiteNoteWidth : whiteNoteWidth * blackPerWhiteWidth;
1222
- const h = (0, _tremolo_ui_functions.isWhiteKey)(note) ? containerHeight : containerHeight * .6;
1223
- if (pos <= x && x < pos + w && 0 <= y && y < h) return note;
1224
- }
1225
- return -1;
1226
- }, [
1227
- blackNotes,
1228
- notePosition,
1229
- whiteNoteWidth,
1230
- whiteNotes
1231
- ]);
1232
- const onDrag = (0, react.useCallback)((perX, perY) => {
1233
- if (!pianoRef.current) return;
1234
- const note = getHitKeyIndex(perX * staticWidth, perY * pianoRef.current.clientHeight);
1235
- const index = noteRangeArray.indexOf(note);
1236
- if (index == -1) return;
1237
- if (hitKeyIndex.current != index) {
1238
- keyRefs.current[hitKeyIndex.current]?.current?.stop();
1239
- keyRefs.current[index]?.current?.play();
1240
- hitKeyIndex.current = index;
1241
- }
1242
- }, [
1243
- getHitKeyIndex,
1244
- noteRangeArray,
1245
- staticWidth
1246
- ]);
1247
- const onDragEnd = (0, react.useCallback)(() => {
1248
- if (keyRefs.current[hitKeyIndex.current]?.current?.played()) keyRefs.current[hitKeyIndex.current]?.current?.stop();
1249
- hitKeyIndex.current = -1;
1250
- }, [keyRefs]);
1251
- (0, react.useEffect)(() => {
1252
- if (fill && pianoRef.current) {
1253
- const parent = pianoRef.current.parentElement;
1254
- if (!parent) throw new Error("doesn't have a parent element.");
1255
- const resizeObserver = new ResizeObserver(() => {
1256
- const w = pianoRef.current.clientWidth;
1257
- setWhiteNoteWidth(w / whiteNoteCount - padding);
1258
- });
1259
- resizeObserver.observe(parent);
1260
- return () => {
1261
- resizeObserver.unobserve(parent);
1262
- };
1263
- } else setWhiteNoteWidth(_whiteNoteWidth);
1264
- }, [
1265
- fill,
1266
- _whiteNoteWidth,
1267
- whiteNoteCount
1268
- ]);
1269
- const { refHandler: touchMoveRefCallback, pointerDownHandler } = usePianoDrag({
1270
- baseElementRef: pianoRef,
1271
- onDrag,
1272
- onDragEnd
1273
- });
1274
- useEventListener(globalThis.window, "keydown", (e) => {
1275
- if (e.repeat) return;
1276
- if (!keyboardShortcuts) return;
1277
- const index = keyboardShortcuts.keys.indexOf(e.key);
1278
- if (index != -1) {
1279
- if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play();
1280
- }
1281
- });
1282
- useEventListener(globalThis.window, "keyup", (e) => {
1283
- if (e.repeat) return;
1284
- if (!keyboardShortcuts) return;
1285
- const index = keyboardShortcuts.keys.indexOf(e.key);
1286
- if (index != -1) keyRefs.current[index]?.current?.stop();
1287
- });
1288
- (0, react.useImperativeHandle)(forwardedRef, () => {
1289
- return {
1290
- playNote(note, velocity) {
1291
- const index = noteRangeArray.indexOf(note);
1292
- if (index != -1) {
1293
- if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play(velocity);
1294
- } else onPlayNote?.(note, velocity);
1295
- },
1296
- stopNote(note) {
1297
- const index = noteRangeArray.indexOf(note);
1298
- if (index != -1) keyRefs.current[index]?.current?.stop();
1299
- else onStopNote?.(note);
1300
- }
1301
- };
1302
- }, [
1303
- noteRangeArray,
1304
- onPlayNote,
1305
- onStopNote
1306
- ]);
1307
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoProvider, {
1308
- notePosition,
1309
- noteRange,
1310
- glissando,
1311
- midiMax,
1312
- fill,
1313
- onPlayNote,
1314
- onStopNote,
1315
- label,
1316
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1317
- ref: (div) => {
1318
- pianoRef.current = div;
1319
- touchMoveRefCallback(div);
1320
- },
1321
- className: (0, clsx.default)("tremolo-piano", className),
1117
+ const { className: keyClassName, style: keyStyle, ...rest } = keyProps?.(note, state) ?? {};
1118
+ const content = label?.(note, state);
1119
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1120
+ className: (0, clsx.default)(`tremolo-piano-${keyType}-key`, keyClassName),
1121
+ "data-note": note,
1122
+ "data-note-key": (0, _tremolo_ui_functions.noteKey)(note),
1123
+ "data-active": state.active,
1124
+ "aria-disabled": state.disabled,
1125
+ ...rest,
1322
1126
  style: {
1323
- width: fill ? "100%" : staticWidth,
1324
- height,
1325
- ...style
1326
- },
1327
- onPointerDown: (event) => {
1328
- pointerDownHandler(event);
1329
- onPointerDown?.(event);
1127
+ ...keyStyle,
1128
+ left: (0, _tremolo_ui_functions.notePosition)(note, layout),
1129
+ width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_functions.blackKeyWidth)(layout),
1130
+ height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
1330
1131
  },
1331
- ...props,
1332
- children: childrenWithProps || noteRangeArray.map((note, index) => (0, _tremolo_ui_functions.isWhiteKey)(note) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WhiteKey, {
1333
- ref: keyRefs.current[index],
1334
- noteNumber: note,
1335
- __width: whiteNoteWidth
1336
- }, note) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlackKey, {
1337
- ref: keyRefs.current[index],
1338
- noteNumber: note,
1339
- __width: whiteNoteWidth * blackPerWhiteWidth
1340
- }, note))
1341
- })
1342
- });
1343
- }),
1344
- WhiteKey,
1345
- BlackKey,
1346
- KeyLabel
1347
- };
1132
+ children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1133
+ className: "tremolo-piano-key-label-wrapper",
1134
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1135
+ className: "tremolo-piano-key-label",
1136
+ children: content
1137
+ })
1138
+ })
1139
+ }, note);
1140
+ })
1141
+ });
1142
+ }) };
1348
1143
  //#endregion
1349
1144
  //#region src/components/PointsEditor/Background.tsx
1350
1145
  function Background({ className, children, ...props }) {
@@ -1356,111 +1151,24 @@ function Background({ className, children, ...props }) {
1356
1151
  }
1357
1152
  //#endregion
1358
1153
  //#region src/components/PointsEditor/context.tsx
1359
- const createPointsEditorStore = (initProps) => {
1360
- const DEFAULT_PROPS = {
1361
- containerElementRef: (0, react.createRef)(),
1362
- disabled: false,
1363
- readonly: false,
1364
- externalStyles: {
1365
- userSelectNone: true,
1366
- cursor: "grabbing"
1367
- }
1368
- };
1369
- return (0, zustand.createStore)()((set) => ({
1370
- ...DEFAULT_PROPS,
1371
- ...initProps,
1372
- setContainerElementRef: (containerElementRef) => {
1373
- set(() => ({ containerElementRef }));
1374
- }
1375
- }));
1376
- };
1377
1154
  const PointsEditorContext = (0, react.createContext)(null);
1378
- function PointsEditorProvider({ children, ...props }) {
1379
- const storeRef = (0, react.useRef)(null);
1380
- if (!storeRef.current) storeRef.current = createPointsEditorStore(props);
1381
- (0, react.useEffect)(() => {
1382
- if (storeRef.current) storeRef.current.setState(props);
1383
- else storeRef.current = createPointsEditorStore(props);
1384
- }, [props]);
1385
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorContext.Provider, {
1386
- value: storeRef.current,
1387
- children
1388
- });
1389
- }
1155
+ const PointsEditorProvider = PointsEditorContext.Provider;
1390
1156
  function usePointsEditorContext(selector) {
1391
- const store = (0, react.useContext)(PointsEditorContext);
1392
- if (!store) throw new Error("Missing PointsEditorContext.Provider in the tree");
1393
- return (0, zustand.useStore)(store, selector);
1157
+ const context = (0, react.useContext)(PointsEditorContext);
1158
+ if (!context) throw new Error("Missing PointsEditorContext.Provider in the tree");
1159
+ return selector ? selector(context) : context;
1394
1160
  }
1395
1161
  //#endregion
1396
1162
  //#region src/components/PointsEditor/Container.tsx
1397
- function Container({ className, children, ...props }) {
1398
- const containerElementRef = (0, react.useRef)(null);
1399
- usePointsEditorContext((s) => s.setContainerElementRef)(containerElementRef);
1163
+ function Container({ children, className, ref, ...props }) {
1400
1164
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1401
- ref: containerElementRef,
1165
+ ref: useComposedRefs(ref, usePointsEditorContext((s) => s.containerRef)),
1402
1166
  className: (0, clsx.default)("tremolo-points-editor-container", className),
1403
1167
  ...props,
1404
1168
  children
1405
1169
  });
1406
1170
  }
1407
1171
  //#endregion
1408
- //#region src/hooks/useDragWithElement.ts
1409
- /**
1410
- * Track a pointer drag, reporting the position normalized against
1411
- * the bounding rect of `baseElementRef`.
1412
- *
1413
- * @returns a ref callback for the element that starts the drag, and whether a drag is in progress
1414
- */
1415
- function useDragWithElement({ baseElementRef, updateOnPointerDown = false, cursor, onDrag, onDragStart, onDragEnd }) {
1416
- const [dragging, setDragging] = (0, react.useState)(false);
1417
- const normalizedX = (0, react.useRef)(0);
1418
- const normalizedY = (0, react.useRef)(0);
1419
- const dragHandler = useCallbackRef(onDrag);
1420
- const dragStartHandler = useCallbackRef(onDragStart);
1421
- const dragEndHandler = useCallbackRef(onDragEnd);
1422
- const update = (0, react.useCallback)((clientX, clientY) => {
1423
- const base = baseElementRef.current;
1424
- if (!base) return false;
1425
- const { left, top, right, bottom } = base.getBoundingClientRect();
1426
- normalizedX.current = (0, _tremolo_ui_functions.normalizeValue)(clientX, left, right);
1427
- normalizedY.current = (0, _tremolo_ui_functions.normalizeValue)(clientY, top, bottom);
1428
- return true;
1429
- }, [baseElementRef]);
1430
- const [node, setNode] = (0, react.useState)(null);
1431
- (0, react.useEffect)(() => {
1432
- if (!node) return;
1433
- const instance = (0, _tremolo_ui_dom.createDrag)(node, {
1434
- cursor,
1435
- onDragStart: ({ clientX, clientY }) => {
1436
- setDragging(true);
1437
- if (update(clientX, clientY) && updateOnPointerDown) dragHandler(normalizedX.current, normalizedY.current);
1438
- dragStartHandler(normalizedX.current, normalizedY.current);
1439
- },
1440
- onDrag: ({ clientX, clientY }) => {
1441
- if (update(clientX, clientY)) dragHandler(normalizedX.current, normalizedY.current);
1442
- },
1443
- onDragEnd: () => {
1444
- setDragging(false);
1445
- dragEndHandler(normalizedX.current, normalizedY.current);
1446
- }
1447
- });
1448
- return () => instance.destroy();
1449
- }, [
1450
- node,
1451
- update,
1452
- updateOnPointerDown,
1453
- cursor,
1454
- dragHandler,
1455
- dragStartHandler,
1456
- dragEndHandler
1457
- ]);
1458
- return {
1459
- refCallback: setNode,
1460
- dragging
1461
- };
1462
- }
1463
- //#endregion
1464
1172
  //#region src/components/PointsEditor/Point.tsx
1465
1173
  function clampPoint(point, min, max) {
1466
1174
  const { x, y } = point;
@@ -1469,22 +1177,33 @@ function clampPoint(point, min, max) {
1469
1177
  y: (0, _tremolo_ui_functions.clamp)(y, min?.y ?? 0, max?.y ?? 1)
1470
1178
  };
1471
1179
  }
1472
- function Point({ value, min, max, size, width = 16, height = 16, color, disabled, readonly, onChange, onDragStart, onDragEnd, className, style, onPointerDown, ...props }) {
1473
- const containerElementRef = usePointsEditorContext((s) => s.containerElementRef);
1474
- const externalStyles = usePointsEditorContext((s) => s.externalStyles);
1475
- const __disabled = usePointsEditorContext((s) => s.disabled);
1476
- const __readonly = usePointsEditorContext((s) => s.readonly);
1477
- const { refCallback: dragRefCallback, dragging } = useDragWithElement({
1478
- baseElementRef: containerElementRef,
1180
+ /**
1181
+ * A point is placed by its position within the container, so its value is a
1182
+ * position: 0..1 on each axis, with y growing downwards.
1183
+ */
1184
+ const AXIS = {
1185
+ min: 0,
1186
+ max: 1
1187
+ };
1188
+ function Point({ value, min, max, size, width = 16, height = 16, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, ...props }) {
1189
+ const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard } = usePointsEditorContext();
1190
+ const disabled = _disabled ?? rootDisabled;
1191
+ const readonly = _readonly ?? rootReadonly;
1192
+ const wheel = _wheel === void 0 ? rootWheel : _wheel;
1193
+ const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
1194
+ const [element, setElement] = (0, react.useState)(null);
1195
+ const { refCallback: dragRefCallback, dragging } = useDragValue({
1196
+ axis: AXIS,
1197
+ baseElementRef: containerRef,
1479
1198
  cursor: readonly ? void 0 : externalStyles.cursor,
1480
- onDrag: (x, y) => {
1199
+ onChange: ([x, y]) => {
1481
1200
  if (readonly) return;
1482
1201
  onChange?.(clampPoint({
1483
1202
  x,
1484
1203
  y
1485
1204
  }, min, max));
1486
1205
  },
1487
- onDragStart: (x, y) => {
1206
+ onDragStart: ([x, y]) => {
1488
1207
  if (readonly) return;
1489
1208
  if (externalStyles.userSelectNone) addUserSelectNone();
1490
1209
  onDragStart?.(clampPoint({
@@ -1492,7 +1211,7 @@ function Point({ value, min, max, size, width = 16, height = 16, color, disabled
1492
1211
  y
1493
1212
  }, min, max));
1494
1213
  },
1495
- onDragEnd: (x, y) => {
1214
+ onDragEnd: ([x, y]) => {
1496
1215
  if (readonly) return;
1497
1216
  if (externalStyles.userSelectNone) removeUserSelectNone();
1498
1217
  onDragEnd?.(clampPoint({
@@ -1501,13 +1220,45 @@ function Point({ value, min, max, size, width = 16, height = 16, color, disabled
1501
1220
  }, min, max));
1502
1221
  }
1503
1222
  });
1223
+ const nudge = (0, react.useCallback)((axis, direction, option) => {
1224
+ const next = (0, _tremolo_ui_functions.applyDelta)(value[axis], direction, option, AXIS);
1225
+ onChange?.(clampPoint({
1226
+ ...value,
1227
+ [axis]: next
1228
+ }, min, max));
1229
+ }, [
1230
+ value,
1231
+ min,
1232
+ max,
1233
+ onChange
1234
+ ]);
1235
+ useWheel((event) => {
1236
+ if (!onChange || readonly || !wheel) return;
1237
+ if (!element || element.ownerDocument.activeElement !== element) return;
1238
+ event.preventDefault();
1239
+ nudge(event.shiftKey ? "x" : "y", event.deltaY < 0 ? -1 : 1, wheel);
1240
+ }, { target: containerRef });
1241
+ const refCallback = useComposedRefs(dragRefCallback, setElement);
1242
+ const handleKeyDown = (event) => {
1243
+ if (!onChange || readonly || !keyboard) return;
1244
+ const key = event.key;
1245
+ if ([
1246
+ "ArrowRight",
1247
+ "ArrowLeft",
1248
+ "ArrowUp",
1249
+ "ArrowDown"
1250
+ ].includes(key)) {
1251
+ event.preventDefault();
1252
+ nudge(key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y", key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1, keyboard);
1253
+ }
1254
+ };
1504
1255
  const colors = { "--color": color };
1505
1256
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1506
- ref: dragRefCallback,
1257
+ ref: refCallback,
1507
1258
  className: (0, clsx.default)("tremolo-points-editor-point", className),
1508
1259
  tabIndex: 0,
1509
- "aria-disabled": disabled != void 0 ? disabled : __disabled,
1510
- "aria-readonly": readonly != void 0 ? readonly : __readonly,
1260
+ "aria-disabled": disabled,
1261
+ "aria-readonly": readonly,
1511
1262
  "data-dragging": dragging,
1512
1263
  style: {
1513
1264
  ...colors,
@@ -1518,30 +1269,51 @@ function Point({ value, min, max, size, width = 16, height = 16, color, disabled
1518
1269
  ...style
1519
1270
  },
1520
1271
  onPointerDown,
1272
+ onKeyDown: (event) => {
1273
+ handleKeyDown(event);
1274
+ onKeyDown?.(event);
1275
+ },
1521
1276
  ...props
1522
1277
  });
1523
1278
  }
1524
1279
  //#endregion
1525
1280
  //#region src/components/PointsEditor/index.tsx
1526
- const defaultExternalStyles$2 = {
1527
- userSelectNone: true,
1528
- cursor: "grabbing"
1529
- };
1281
+ /**
1282
+ * A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
1283
+ * in a hundred steps whatever its pixel size.
1284
+ */
1285
+ const DEFAULT_INPUT_EVENT_OPTION = ["normalized", .01];
1530
1286
  /**
1531
1287
  * Multiple Point Controller
1532
1288
  */
1533
1289
  const PointsEditor = {
1534
- Root: (0, react.forwardRef)(({ width = 200, height = 100, disabled = false, readonly = false, externalStyles: _externalStyles, style, className, children, ...props }, forwardedRef) => {
1290
+ Root: (0, react.forwardRef)(({ width = 200, height = 100, disabled = false, readonly = false, wheel = DEFAULT_INPUT_EVENT_OPTION, keyboard = DEFAULT_INPUT_EVENT_OPTION, externalStyles, style, className, children, ...props }, forwardedRef) => {
1291
+ const containerRef = (0, react.useRef)(null);
1292
+ const { userSelectNone = true, cursor = "grabbing" } = externalStyles ?? {};
1535
1293
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorProvider, {
1536
- disabled,
1537
- readonly,
1538
- externalStyles: {
1539
- ...defaultExternalStyles$2,
1540
- ..._externalStyles
1541
- },
1294
+ value: (0, react.useMemo)(() => ({
1295
+ disabled,
1296
+ readonly,
1297
+ wheel,
1298
+ keyboard,
1299
+ externalStyles: {
1300
+ userSelectNone,
1301
+ cursor
1302
+ },
1303
+ containerRef
1304
+ }), [
1305
+ disabled,
1306
+ readonly,
1307
+ wheel,
1308
+ keyboard,
1309
+ userSelectNone,
1310
+ cursor
1311
+ ]),
1542
1312
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1543
1313
  ref: forwardedRef,
1544
1314
  className: (0, clsx.default)("tremolo-points-editor", className),
1315
+ "aria-disabled": disabled,
1316
+ "aria-readonly": readonly,
1545
1317
  style: {
1546
1318
  width,
1547
1319
  height,
@@ -1558,59 +1330,33 @@ const PointsEditor = {
1558
1330
  };
1559
1331
  //#endregion
1560
1332
  //#region src/components/Slider/context.tsx
1561
- const createSliderStore = (initProps) => {
1562
- const DEFAULT_PROPS = {
1563
- min: 0,
1564
- max: 0,
1565
- step: 0,
1566
- skew: 0,
1567
- vertical: false,
1568
- reverse: false,
1569
- disabled: false,
1570
- readonly: false
1571
- };
1572
- return (0, zustand.createStore)()(() => ({
1573
- ...DEFAULT_PROPS,
1574
- ...initProps
1575
- }));
1576
- };
1577
1333
  const SliderContext = (0, react.createContext)(null);
1578
- function SliderProvider({ children, ...props }) {
1579
- const storeRef = (0, react.useRef)(null);
1580
- if (!storeRef.current) storeRef.current = createSliderStore(props);
1581
- (0, react.useEffect)(() => {
1582
- if (storeRef.current) storeRef.current.setState(props);
1583
- else storeRef.current = createSliderStore(props);
1584
- }, [props]);
1585
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderContext.Provider, {
1586
- value: storeRef.current,
1587
- children
1588
- });
1589
- }
1334
+ const SliderProvider = SliderContext.Provider;
1590
1335
  function useSliderContext(selector) {
1591
- const store = (0, react.useContext)(SliderContext);
1592
- if (!store) throw new Error("Missing SliderContext.Provider in the tree");
1593
- return (0, zustand.useStore)(store, selector);
1336
+ const context = (0, react.useContext)(SliderContext);
1337
+ if (!context) throw new Error("Missing SliderContext.Provider in the tree");
1338
+ return selector ? selector(context) : context;
1594
1339
  }
1595
1340
  //#endregion
1596
- //#region src/components/Slider/ScaleOption.tsx
1597
- function ScaleOption({ value, type = "mark-number", label, thickness = 1, length = "0.5rem", gap = 2, labelWidth, classes, styles, className, style, ...props }) {
1341
+ //#region src/components/Slider/MarksOption.tsx
1342
+ function MarksOption({ value, type = "mark-number", label, thickness = 1, length = "0.5rem", gap = 2, labelWidth, classes, styles, className, style, ...props }) {
1598
1343
  const min = useSliderContext((s) => s.min);
1599
1344
  const max = useSliderContext((s) => s.max);
1600
- const skew = useSliderContext((s) => s.skew);
1345
+ const scale = useSliderContext((s) => s.scale);
1601
1346
  const vertical = useSliderContext((s) => s.vertical);
1602
1347
  const reverse = useSliderContext((s) => s.reverse);
1603
- const calcPercent = (0, react.useCallback)((rawValue) => {
1604
- return (0, _tremolo_ui_functions.toFixed)((0, _tremolo_ui_functions.xor)(vertical, reverse) ? 100 - (0, _tremolo_ui_functions.normalizeValue)(rawValue, min, max, skew) * 100 : (0, _tremolo_ui_functions.normalizeValue)(rawValue, min, max, skew) * 100);
1348
+ const calcPercent = (0, react.useCallback)((value) => {
1349
+ const percent = scale.normalize(value, min, max) * 100;
1350
+ return (0, _tremolo_ui_functions.toFixed)((0, _tremolo_ui_functions.xor)(vertical, reverse) ? 100 - percent : percent);
1605
1351
  }, [
1606
1352
  vertical,
1607
1353
  reverse,
1608
1354
  max,
1609
1355
  min,
1610
- skew
1356
+ scale
1611
1357
  ]);
1612
1358
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1613
- className: (0, clsx.default)("tremolo-slider-scale-option", className),
1359
+ className: (0, clsx.default)("tremolo-slider-marks-option", className),
1614
1360
  style: {
1615
1361
  left: !vertical ? `${calcPercent(value)}%` : void 0,
1616
1362
  top: vertical ? `${calcPercent(value)}%` : void 0,
@@ -1619,7 +1365,7 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
1619
1365
  "data-vertical": vertical,
1620
1366
  ...props,
1621
1367
  children: [type != "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1622
- className: (0, clsx.default)("tremolo-slider-scale-option-mark", classes?.mark),
1368
+ className: (0, clsx.default)("tremolo-slider-marks-option-mark", classes?.mark),
1623
1369
  style: {
1624
1370
  width: !vertical ? thickness : length,
1625
1371
  height: vertical ? thickness : length,
@@ -1629,7 +1375,7 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
1629
1375
  },
1630
1376
  "data-vertical": vertical
1631
1377
  }), type != "mark" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1632
- className: (0, clsx.default)("tremolo-slider-scale-option-label", classes?.label),
1378
+ className: (0, clsx.default)("tremolo-slider-marks-option-label", classes?.label),
1633
1379
  style: {
1634
1380
  width: labelWidth,
1635
1381
  ...styles?.label
@@ -1654,8 +1400,8 @@ function generateOptionsList(options, min, max, step) {
1654
1400
  return optionsList;
1655
1401
  }
1656
1402
  //#endregion
1657
- //#region src/components/Slider/Scale.tsx
1658
- function Scale({ gap = 6, options, children, className, style, ...props }) {
1403
+ //#region src/components/Slider/Marks.tsx
1404
+ function Marks({ gap = 6, options, children, className, style, ...props }) {
1659
1405
  const min = useSliderContext((s) => s.min);
1660
1406
  const max = useSliderContext((s) => s.max);
1661
1407
  const step = useSliderContext((s) => s.step);
@@ -1664,7 +1410,7 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
1664
1410
  const optionsList = options ? generateOptionsList(options, min, max, step) : [];
1665
1411
  if ((0, _tremolo_ui_functions.xor)(vertical, reverse)) optionsList.reverse();
1666
1412
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1667
- className: (0, clsx.default)("tremolo-slider-scale", className),
1413
+ className: (0, clsx.default)("tremolo-slider-marks", className),
1668
1414
  style: {
1669
1415
  marginLeft: vertical ? gap : void 0,
1670
1416
  marginTop: !vertical ? gap : void 0,
@@ -1672,72 +1418,70 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
1672
1418
  },
1673
1419
  ...props,
1674
1420
  children: options ? optionsList.map(({ value, type }, index) => {
1675
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScaleOption, {
1421
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarksOption, {
1676
1422
  value,
1677
1423
  type
1678
1424
  }, index);
1679
1425
  }) : children
1680
1426
  });
1681
1427
  }
1682
- const Thumb$1 = (0, react.forwardRef)(({ size, width = 22, height = 22, color, children, className, style, __percent }, ref) => {
1683
- const wrapperRef = (0, react.useRef)(null);
1684
- const vertical = useSliderContext((s) => s.vertical);
1685
- const disabled = useSliderContext((s) => s.disabled);
1686
- const readonly = useSliderContext((s) => s.readonly);
1687
- (0, react.useImperativeHandle)(ref, () => {
1688
- return {
1689
- focus() {
1690
- wrapperRef.current?.focus();
1691
- },
1692
- blur() {
1693
- wrapperRef.current?.blur();
1694
- }
1695
- };
1696
- }, []);
1428
+ //#endregion
1429
+ //#region src/components/Slider/Thumb.tsx
1430
+ function Thumb$1({ color, children, className, style, ref }) {
1431
+ const elementRef = (0, react.useRef)(null);
1432
+ const { vertical, disabled, readonly, percent, thumbRef } = useSliderContext();
1433
+ const methods = () => ({
1434
+ focus() {
1435
+ elementRef.current?.focus();
1436
+ },
1437
+ blur() {
1438
+ elementRef.current?.blur();
1439
+ }
1440
+ });
1441
+ (0, react.useImperativeHandle)(ref, methods, []);
1442
+ (0, react.useImperativeHandle)(thumbRef, methods, []);
1697
1443
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1698
1444
  className: "tremolo-slider-thumb-wrapper",
1699
1445
  style: {
1700
- top: vertical ? `${__percent}%` : "50%",
1701
- left: !vertical ? `${__percent}%` : "50%"
1446
+ top: vertical ? `${percent}%` : "50%",
1447
+ left: !vertical ? `${percent}%` : "50%"
1702
1448
  },
1703
1449
  children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1704
- ref: wrapperRef,
1450
+ ref: elementRef,
1705
1451
  className: (0, clsx.default)("tremolo-slider-thumb", className),
1706
1452
  tabIndex: 0,
1707
1453
  "aria-disabled": disabled,
1708
1454
  "aria-readonly": readonly,
1709
1455
  style: {
1710
1456
  "--color": color,
1711
- width: size ?? width,
1712
- height: size ?? height,
1713
1457
  ...style
1714
1458
  }
1715
1459
  })
1716
1460
  });
1717
- });
1718
- function Track({ length = 140, thickness = 10, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0, ...props }) {
1719
- const vertical = useSliderContext((s) => s.vertical);
1720
- const reverse = useSliderContext((s) => s.reverse);
1721
- const disabled = useSliderContext((s) => s.disabled);
1461
+ }
1462
+ function Track({ length = 140, thickness = 10, active, inactive, children, className, style, defaultStyle = true, ref, ...props }) {
1463
+ const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
1464
+ const composedRef = useComposedRefs(ref, trackRef);
1722
1465
  const direction = vertical ? "bottom" : "right";
1723
1466
  const colors = {
1724
1467
  "--active": active,
1725
1468
  "--inactive": inactive
1726
1469
  };
1727
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1470
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1471
+ ref: composedRef,
1728
1472
  className: (0, clsx.default)("tremolo-slider-track", className),
1729
1473
  "aria-disabled": disabled,
1730
1474
  "data-vertical": vertical,
1731
1475
  style: !defaultStyle ? style : {
1732
1476
  ...colors,
1733
- background: (0, _tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive) ${__percent}%, var(--active) ${__percent}%)` : `linear-gradient(to ${direction}, var(--active) ${__percent}%, var(--inactive) ${__percent}%)`,
1477
+ background: (0, _tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive) ${percent}%, var(--active) ${percent}%)` : `linear-gradient(to ${direction}, var(--active) ${percent}%, var(--inactive) ${percent}%)`,
1734
1478
  borderRadius: (0, _tremolo_ui_functions.styleHelper)(thickness, "/", 2),
1735
1479
  width: !vertical ? length : thickness,
1736
1480
  height: vertical ? length : thickness,
1737
1481
  ...style
1738
1482
  },
1739
1483
  ...props,
1740
- children: [children, __thumb]
1484
+ children
1741
1485
  });
1742
1486
  }
1743
1487
  //#endregion
@@ -1747,55 +1491,37 @@ const defaultExternalStyles$1 = {
1747
1491
  cursor: "pointer"
1748
1492
  };
1749
1493
  /**
1494
+ * The wheel only acts while the focus is inside, so that scrolling a page past
1495
+ * the control does not change its value.
1496
+ */
1497
+ const WHEEL_OPTIONS$1 = { requireFocus: true };
1498
+ /**
1750
1499
  * Customizable slider
1751
1500
  */
1752
1501
  const Slider = {
1753
- Root: (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
1754
- const trackElementRef = (0, react.useRef)(null);
1502
+ Root: (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
1503
+ const trackRef = (0, react.useRef)(null);
1755
1504
  const thumbRef = (0, react.useRef)(null);
1756
1505
  const externalStyles = {
1757
1506
  ...defaultExternalStyles$1,
1758
1507
  ..._externalStyles
1759
1508
  };
1760
- const p = (0, _tremolo_ui_functions.toFixed)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) * 100);
1509
+ const p = (0, _tremolo_ui_functions.toFixed)(scale.normalize(value, min, max) * 100);
1761
1510
  const rev = (0, _tremolo_ui_functions.toFixed)(100 - p);
1762
1511
  const displayReversed = (0, react.useMemo)(() => (0, _tremolo_ui_functions.xor)(vertical, reverse), [vertical, reverse]);
1763
1512
  const percent = displayReversed ? rev : p;
1764
- let trackProps = {};
1765
- let thumbProps = {};
1766
- let scaleComponent = void 0;
1767
- if (children != void 0) react.default.Children.forEach(children, (child) => {
1768
- if (react.default.isValidElement(child)) if (child.type == Thumb$1) thumbProps = child.props;
1769
- else if (child.type == Track) trackProps = child.props;
1770
- else if (child.type == Scale) scaleComponent = child;
1771
- else throw new Error("only <SliderThumb> or <SliderTrack>");
1772
- else throw new Error("children is an invalid element.");
1773
- });
1774
- const onDrag = (0, react.useCallback)((nx, ny) => {
1775
- if (!onChange || readonly) return;
1776
- const n = vertical ? ny : nx;
1777
- onChange((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew), step), min, max));
1778
- }, [
1779
- displayReversed,
1780
- max,
1513
+ const axis = (0, react.useMemo)(() => ({
1781
1514
  min,
1782
- onChange,
1783
- readonly,
1784
- skew,
1785
- step,
1786
- vertical
1787
- ]);
1788
- const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
1789
- let newValue;
1790
- if (eventType == "normalized") newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
1791
- else newValue = value + x;
1792
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
1793
- }, [
1794
1515
  max,
1516
+ step,
1517
+ scale,
1518
+ reverse: displayReversed
1519
+ }), [
1795
1520
  min,
1796
- skew,
1521
+ max,
1797
1522
  step,
1798
- value
1523
+ scale,
1524
+ displayReversed
1799
1525
  ]);
1800
1526
  const handleKeyDown = (0, react.useCallback)((event) => {
1801
1527
  if (!keyboard || !onChange || readonly) return;
@@ -1807,46 +1533,77 @@ const Slider = {
1807
1533
  "ArrowDown"
1808
1534
  ].includes(key)) {
1809
1535
  event.preventDefault();
1810
- let x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
1811
- if (reverse) x *= -1;
1812
- onChange(updateValueByEvent(keyboard[0], x));
1536
+ let direction = key == "ArrowRight" || key == "ArrowUp" ? 1 : -1;
1537
+ if (reverse) direction *= -1;
1538
+ onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, keyboard, axis));
1813
1539
  }
1814
1540
  }, [
1815
1541
  keyboard,
1816
1542
  onChange,
1817
1543
  readonly,
1818
1544
  reverse,
1819
- updateValueByEvent
1545
+ value,
1546
+ axis
1820
1547
  ]);
1821
- const { refCallback: dragRefCallback } = useDragWithElement({
1822
- baseElementRef: trackElementRef,
1548
+ const valueOf = (v) => v[vertical ? 1 : 0];
1549
+ const { refCallback: dragRefCallback } = useDragValue({
1550
+ axis,
1551
+ baseElementRef: trackRef,
1823
1552
  cursor: readonly ? void 0 : externalStyles.cursor,
1824
1553
  updateOnPointerDown: true,
1825
- onDrag,
1826
- onDragStart: (nx, ny) => {
1554
+ onChange: (v) => {
1555
+ if (readonly) return;
1556
+ onChange?.(valueOf(v));
1557
+ },
1558
+ onDragStart: (v) => {
1827
1559
  if (readonly) return;
1828
1560
  if (externalStyles.userSelectNone) addUserSelectNone();
1829
1561
  thumbRef.current?.focus();
1830
- onDragStart?.((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(vertical ? ny : nx, min, max, skew), step), min, max));
1562
+ onDragStart?.(valueOf(v));
1831
1563
  },
1832
- onDragEnd: (nx, ny) => {
1564
+ onDragEnd: (v) => {
1833
1565
  if (readonly) return;
1834
1566
  if (externalStyles.userSelectNone) removeUserSelectNone();
1835
- onDragEnd?.((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(vertical ? ny : nx, min, max, skew), step), min, max));
1567
+ onDragEnd?.(valueOf(v));
1836
1568
  }
1837
1569
  });
1838
- const wheelRefCallback = useWheel((event) => {
1570
+ const rootRefCallback = useComposedRefs(dragRefCallback, useWheel((event) => {
1839
1571
  if (!wheel || !onChange || readonly) return;
1840
1572
  event.preventDefault();
1841
- let x;
1842
- if (!vertical && event.deltaX != 0) x = event.deltaX > 0 ? wheel[1] : -wheel[1];
1573
+ let direction;
1574
+ if (!vertical && event.deltaX != 0) direction = event.deltaX > 0 ? 1 : -1;
1843
1575
  else {
1844
1576
  if (event.deltaY == 0) return;
1845
- x = event.deltaY > 0 ? -wheel[1] : wheel[1];
1577
+ direction = event.deltaY > 0 ? -1 : 1;
1846
1578
  }
1847
- if (vertical && reverse) x *= -1;
1848
- onChange(updateValueByEvent(wheel[0], x));
1849
- });
1579
+ if (vertical && reverse) direction *= -1;
1580
+ onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, wheel, axis));
1581
+ }, WHEEL_OPTIONS$1));
1582
+ const context = (0, react.useMemo)(() => ({
1583
+ value,
1584
+ min,
1585
+ max,
1586
+ step,
1587
+ scale,
1588
+ vertical,
1589
+ reverse,
1590
+ disabled,
1591
+ readonly,
1592
+ percent,
1593
+ trackRef,
1594
+ thumbRef
1595
+ }), [
1596
+ value,
1597
+ min,
1598
+ max,
1599
+ step,
1600
+ scale,
1601
+ vertical,
1602
+ reverse,
1603
+ disabled,
1604
+ readonly,
1605
+ percent
1606
+ ]);
1850
1607
  (0, react.useImperativeHandle)(forwardedRef, () => {
1851
1608
  return {
1852
1609
  focus() {
@@ -1858,20 +1615,10 @@ const Slider = {
1858
1615
  };
1859
1616
  }, []);
1860
1617
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderProvider, {
1861
- min,
1862
- max,
1863
- step,
1864
- skew,
1865
- vertical,
1866
- reverse,
1867
- disabled,
1868
- readonly,
1618
+ value: context,
1869
1619
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1870
1620
  className: (0, clsx.default)("tremolo-slider", className),
1871
- ref: (div) => {
1872
- wheelRefCallback(div);
1873
- dragRefCallback(div);
1874
- },
1621
+ ref: rootRefCallback,
1875
1622
  tabIndex: -1,
1876
1623
  role: "slider",
1877
1624
  "aria-valuenow": value,
@@ -1880,10 +1627,8 @@ const Slider = {
1880
1627
  "aria-orientation": vertical ? "vertical" : "horizontal",
1881
1628
  "aria-disabled": disabled,
1882
1629
  "aria-readonly": readonly,
1883
- style: {
1884
- margin: `calc(${(0, _tremolo_ui_functions.styleHelper)(thumbProps?.size ?? 22)} / 2)`,
1885
- ...style
1886
- },
1630
+ "data-vertical": vertical,
1631
+ style,
1887
1632
  onPointerDown,
1888
1633
  onKeyDown: (event) => {
1889
1634
  handleKeyDown(event);
@@ -1898,70 +1643,30 @@ const Slider = {
1898
1643
  onBlur?.(event);
1899
1644
  },
1900
1645
  ...props,
1901
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1902
- style: {
1903
- display: "flex",
1904
- flexDirection: vertical ? "row" : "column",
1905
- width: !vertical ? "100%" : void 0,
1906
- height: vertical ? "100%" : void 0
1907
- },
1908
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1909
- className: "tremolo-slider-track-wrapper",
1910
- ref: trackElementRef,
1911
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Track, {
1912
- __percent: percent,
1913
- __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$1, {
1914
- ref: thumbRef,
1915
- __percent: percent,
1916
- ...thumbProps
1917
- }),
1918
- ...trackProps
1919
- })
1920
- }), scaleComponent]
1921
- })
1646
+ children
1922
1647
  })
1923
1648
  });
1924
1649
  }),
1925
1650
  Thumb: Thumb$1,
1926
1651
  Track,
1927
- Scale,
1928
- ScaleOption
1652
+ Marks,
1653
+ MarksOption
1929
1654
  };
1930
1655
  //#endregion
1931
- //#region src/components/_util/composeRefs.tsx
1932
- /**
1933
- * Set a given ref to a given value
1934
- * This utility takes care of different types of refs: callback refs and RefObject(s)
1935
- */
1936
- function setRef(ref, value) {
1937
- if (typeof ref === "function") return ref(value);
1938
- else if (ref !== null && ref !== void 0) ref.current = value;
1939
- }
1940
- /**
1941
- * A utility to compose multiple refs together
1942
- * Accepts callback refs and RefObject(s)
1943
- */
1944
- function composeRefs(...refs) {
1945
- return (node) => {
1946
- let hasCleanup = false;
1947
- const cleanups = refs.map((ref) => {
1948
- const cleanup = setRef(ref, node);
1949
- if (!hasCleanup && typeof cleanup == "function") hasCleanup = true;
1950
- return cleanup;
1951
- });
1952
- if (hasCleanup) return () => {
1953
- for (let i = 0; i < cleanups.length; i++) {
1954
- const cleanup = cleanups[i];
1955
- if (typeof cleanup == "function") cleanup();
1956
- else setRef(refs[i], null);
1957
- }
1958
- };
1959
- };
1656
+ //#region src/components/XYPad/context.tsx
1657
+ const XYPadContext = (0, react.createContext)(null);
1658
+ const XYPadProvider = XYPadContext.Provider;
1659
+ function useXYPadContext(selector) {
1660
+ const context = (0, react.useContext)(XYPadContext);
1661
+ if (!context) throw new Error("Missing XYPadContext.Provider in the tree");
1662
+ return selector ? selector(context) : context;
1960
1663
  }
1961
1664
  //#endregion
1962
1665
  //#region src/components/XYPad/Area.tsx
1963
- function Area({ width = 120, height = 120, color, children, className, style, __thumb, ...props }) {
1964
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1666
+ function Area({ width = 120, height = 120, color, children, className, style, ref, ...props }) {
1667
+ const { areaRef } = useXYPadContext();
1668
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1669
+ ref: useComposedRefs(ref, areaRef),
1965
1670
  className: (0, clsx.default)("tremolo-xy-pad-area", className),
1966
1671
  style: {
1967
1672
  "--color": color,
@@ -1970,121 +1675,105 @@ function Area({ width = 120, height = 120, color, children, className, style, __
1970
1675
  ...style
1971
1676
  },
1972
1677
  ...props,
1973
- children: [children, __thumb]
1678
+ children
1974
1679
  });
1975
1680
  }
1976
1681
  //#endregion
1977
1682
  //#region src/components/XYPad/Thumb.tsx
1978
- const Thumb = (0, react.forwardRef)(({ size, width = 22, height = 22, color, children, wrapperClassName, wrapperStyle, className, style, __disabled, __readonly, __css, ...props }, ref) => {
1979
- const wrapperRef = (0, react.useRef)(null);
1980
- (0, react.useImperativeHandle)(ref, () => {
1981
- return {
1982
- focus() {
1983
- wrapperRef.current?.focus();
1984
- },
1985
- blur() {
1986
- wrapperRef.current?.blur();
1987
- }
1988
- };
1989
- }, []);
1683
+ function Thumb({ color, children, wrapperClassName, wrapperStyle, className, style, ref, ...props }) {
1684
+ const elementRef = (0, react.useRef)(null);
1685
+ const { disabled, readonly, percent, thumbRef } = useXYPadContext();
1686
+ const methods = () => ({
1687
+ focus() {
1688
+ elementRef.current?.focus();
1689
+ },
1690
+ blur() {
1691
+ elementRef.current?.blur();
1692
+ }
1693
+ });
1694
+ (0, react.useImperativeHandle)(ref, methods, []);
1695
+ (0, react.useImperativeHandle)(thumbRef, methods, []);
1990
1696
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1991
1697
  className: (0, clsx.default)("tremolo-xy-pad-thumb-wrapper", wrapperClassName),
1992
1698
  style: {
1993
- ...__css,
1699
+ left: `${percent[0]}%`,
1700
+ top: `${percent[1]}%`,
1994
1701
  ...wrapperStyle
1995
1702
  },
1996
1703
  ...props,
1997
1704
  children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1998
- ref: wrapperRef,
1705
+ ref: elementRef,
1999
1706
  className: (0, clsx.default)("tremolo-xy-pad-thumb", className),
2000
1707
  tabIndex: 0,
2001
- "aria-disabled": __disabled,
2002
- "aria-readonly": __readonly,
1708
+ "aria-disabled": disabled,
1709
+ "aria-readonly": readonly,
2003
1710
  style: {
2004
1711
  "--color": color,
2005
- width: size ?? width,
2006
- height: size ?? height,
2007
1712
  ...style
2008
1713
  }
2009
1714
  })
2010
1715
  });
2011
- });
1716
+ }
2012
1717
  //#endregion
2013
1718
  //#region src/components/XYPad/index.tsx
2014
- const defaultValueOptions = {
2015
- skew: 1,
2016
- step: 1,
2017
- reverse: false,
2018
- wheel: ["raw", 1],
2019
- keyboard: ["raw", 1]
2020
- };
2021
1719
  const defaultExternalStyles = {
2022
1720
  userSelectNone: true,
2023
1721
  cursor: "pointer"
2024
1722
  };
2025
1723
  /**
1724
+ * The wheel only acts while the focus is inside, so that scrolling a page past
1725
+ * the control does not change its value.
1726
+ */
1727
+ const WHEEL_OPTIONS = { requireFocus: true };
1728
+ /**
2026
1729
  * Simple XYPad
2027
1730
  */
2028
1731
  const XYPad = {
2029
- Root: (0, react.forwardRef)(({ x: _x, y: _y, className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
2030
- const x = (0, react.useMemo)(() => {
2031
- return {
2032
- ...defaultValueOptions,
2033
- ..._x
2034
- };
2035
- }, [_x]);
2036
- const y = (0, react.useMemo)(() => {
2037
- return {
2038
- ...defaultValueOptions,
2039
- ..._y
2040
- };
2041
- }, [_y]);
1732
+ Root: (0, react.forwardRef)(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = _tremolo_ui_functions.linearScale, reverse: _reverse = false, wheel = ["raw", 1], keyboard = ["raw", 1], className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
2042
1733
  const rootRef = (0, react.useRef)(null);
2043
- const areaElementRef = (0, react.useRef)(null);
1734
+ const areaRef = (0, react.useRef)(null);
2044
1735
  const thumbRef = (0, react.useRef)(null);
2045
1736
  const externalStyles = {
2046
1737
  ...defaultExternalStyles,
2047
1738
  ..._externalStyles
2048
1739
  };
2049
- const nx = (0, _tremolo_ui_functions.normalizeValue)(x.value, x.min, x.max, x.skew);
2050
- const ny = (0, _tremolo_ui_functions.normalizeValue)(y.value, y.min, y.max, y.skew);
2051
- const percentX = (0, _tremolo_ui_functions.toFixed)((x.reverse ? 1 - nx : nx) * 100);
2052
- const percentY = (0, _tremolo_ui_functions.toFixed)((y.reverse ? 1 - ny : ny) * 100);
2053
- let areaProps = {};
2054
- let thumbProps = {};
2055
- if (children != void 0) react.default.Children.forEach(children, (child) => {
2056
- if (react.default.isValidElement(child)) if (child.type == Thumb) thumbProps = child.props;
2057
- else if (child.type == Area) areaProps = child.props;
2058
- else throw new Error("only <XYPadThumb> or <XYPadArea>");
2059
- else throw new Error("children is an invalid element.");
2060
- });
2061
- const onDrag = (0, react.useCallback)((nx, ny) => {
2062
- if (!onChange || readonly) return;
2063
- const vx = (0, _tremolo_ui_functions.rawValue)(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew);
2064
- const vy = (0, _tremolo_ui_functions.rawValue)(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew);
2065
- onChange((0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max), (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max));
1740
+ const min = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_min), [_min]);
1741
+ const max = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_max), [_max]);
1742
+ const step = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_step), [_step]);
1743
+ const scale = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_scale), [_scale]);
1744
+ const reverse = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_reverse), [_reverse]);
1745
+ const percent = (0, react.useMemo)(() => {
1746
+ const normalized = [0, 1].map((i) => scale[i].normalize(value[i], min[i], max[i]));
1747
+ return [0, 1].map((i) => (0, _tremolo_ui_functions.toFixed)((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100));
2066
1748
  }, [
2067
- onChange,
2068
- readonly,
2069
- x.max,
2070
- x.min,
2071
- x.reverse,
2072
- x.skew,
2073
- x.step,
2074
- y.max,
2075
- y.min,
2076
- y.reverse,
2077
- y.skew,
2078
- y.step
1749
+ value,
1750
+ min,
1751
+ max,
1752
+ scale,
1753
+ reverse
1754
+ ]);
1755
+ const axis = (0, react.useMemo)(() => [0, 1].map((i) => ({
1756
+ min: min[i],
1757
+ max: max[i],
1758
+ step: step[i],
1759
+ scale: scale[i],
1760
+ reverse: reverse[i]
1761
+ })), [
1762
+ min,
1763
+ max,
1764
+ step,
1765
+ scale,
1766
+ reverse
1767
+ ]);
1768
+ const withAxis = (0, react.useCallback)((axis, next) => axis == 0 ? [next, value[1]] : [value[0], next], [value]);
1769
+ /** @param i 0 = x, 1 = y */
1770
+ const nudge = (0, react.useCallback)((i, direction, option) => withAxis(i, (0, _tremolo_ui_functions.applyDelta)(value[i], direction, option, axis[i])), [
1771
+ value,
1772
+ axis,
1773
+ withAxis
2079
1774
  ]);
2080
- const updateValueByEvent = (0, react.useCallback)((eventType, target, x) => {
2081
- let v;
2082
- if (eventType == "normalized") v = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew) + x, target.min, target.max, target.skew);
2083
- else v = target.value + x;
2084
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(v, target.step), target.min, target.max);
2085
- }, []);
2086
1775
  const handleKeyDown = (0, react.useCallback)((event) => {
2087
- if (!onChange || readonly) return;
1776
+ if (!onChange || readonly || !keyboard) return;
2088
1777
  const key = event.key;
2089
1778
  if ([
2090
1779
  "ArrowRight",
@@ -2092,57 +1781,73 @@ const XYPad = {
2092
1781
  "ArrowUp",
2093
1782
  "ArrowDown"
2094
1783
  ].includes(key)) {
2095
- const isHorizontal = key == "ArrowRight" || key == "ArrowLeft";
2096
- const target = isHorizontal ? x : y;
2097
- if (!target.keyboard) return;
1784
+ const i = key == "ArrowRight" || key == "ArrowLeft" ? 0 : 1;
2098
1785
  event.preventDefault();
2099
- let delta = target.keyboard[1];
2100
- if (key == "ArrowLeft" || key == "ArrowUp") delta *= -1;
2101
- if (target.reverse) delta *= -1;
2102
- const newValue = updateValueByEvent(target.keyboard[0], target, delta);
2103
- if (isHorizontal) onChange(newValue, y.value);
2104
- else onChange(x.value, newValue);
1786
+ let direction = 1;
1787
+ if (key == "ArrowLeft" || key == "ArrowUp") direction *= -1;
1788
+ if (reverse[i]) direction *= -1;
1789
+ onChange(nudge(i, direction, keyboard));
2105
1790
  }
2106
1791
  }, [
2107
1792
  onChange,
2108
1793
  readonly,
2109
- x,
2110
- y,
2111
- updateValueByEvent
1794
+ keyboard,
1795
+ reverse,
1796
+ nudge
2112
1797
  ]);
2113
- const { refCallback: dragRefCallback } = useDragWithElement({
2114
- baseElementRef: areaElementRef,
2115
- cursor: readonly ? void 0 : externalStyles.cursor,
1798
+ const { refCallback: dragRefCallback } = useDragValue({
1799
+ axis,
1800
+ baseElementRef: areaRef,
2116
1801
  updateOnPointerDown: true,
2117
- onDrag,
2118
- onDragStart: (nx, ny) => {
1802
+ cursor: readonly ? void 0 : externalStyles.cursor,
1803
+ onChange: (v) => {
1804
+ if (readonly) return;
1805
+ onChange?.(v);
1806
+ },
1807
+ onDragStart: (v) => {
2119
1808
  if (readonly) return;
2120
1809
  if (externalStyles.userSelectNone) addUserSelectNone();
2121
1810
  thumbRef.current?.focus();
2122
- const valueX = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
2123
- const valueY = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
2124
- onDragStart?.(valueX, valueY);
1811
+ onDragStart?.(v);
2125
1812
  },
2126
- onDragEnd: (nx, ny) => {
1813
+ onDragEnd: (v) => {
2127
1814
  if (readonly) return;
2128
1815
  if (externalStyles.userSelectNone) removeUserSelectNone();
2129
- const valueX = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
2130
- const valueY = (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)((0, _tremolo_ui_functions.rawValue)(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
2131
- onDragEnd?.(valueX, valueY);
1816
+ onDragEnd?.(v);
2132
1817
  }
2133
1818
  });
2134
- const wheelRefCallback = useWheel((event) => {
2135
- if (!onChange || readonly) return;
2136
- const target = event.shiftKey ? x : y;
2137
- if (!target.wheel) return;
1819
+ const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, useWheel((event) => {
1820
+ if (!onChange || readonly || !wheel) return;
1821
+ const i = event.shiftKey ? 0 : 1;
2138
1822
  event.preventDefault();
2139
- let delta = target.wheel[1];
2140
- if (event.deltaY < 0) delta *= -1;
2141
- if (target.reverse) delta *= -1;
2142
- const newValue = updateValueByEvent(target.wheel[0], target, delta);
2143
- if (event.shiftKey) onChange(newValue, y.value);
2144
- else onChange(x.value, newValue);
2145
- });
1823
+ let direction = 1;
1824
+ if (event.deltaY < 0) direction *= -1;
1825
+ if (reverse[i]) direction *= -1;
1826
+ onChange(nudge(i, direction, wheel));
1827
+ }, WHEEL_OPTIONS));
1828
+ const context = (0, react.useMemo)(() => ({
1829
+ value,
1830
+ min,
1831
+ max,
1832
+ step,
1833
+ scale,
1834
+ reverse,
1835
+ disabled,
1836
+ readonly,
1837
+ percent,
1838
+ areaRef,
1839
+ thumbRef
1840
+ }), [
1841
+ value,
1842
+ min,
1843
+ max,
1844
+ step,
1845
+ scale,
1846
+ reverse,
1847
+ disabled,
1848
+ readonly,
1849
+ percent
1850
+ ]);
2146
1851
  (0, react.useImperativeHandle)(forwardedRef, () => {
2147
1852
  return {
2148
1853
  focus() {
@@ -2154,48 +1859,30 @@ const XYPad = {
2154
1859
  original: rootRef
2155
1860
  };
2156
1861
  }, []);
2157
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2158
- className: (0, clsx.default)("tremolo-xy-pad", className),
2159
- ref: composeRefs(rootRef, wheelRefCallback, dragRefCallback),
2160
- tabIndex: -1,
2161
- "aria-disabled": disabled,
2162
- "aria-readonly": readonly,
2163
- style: {
2164
- margin: `calc(${(0, _tremolo_ui_functions.styleHelper)(thumbProps?.size ?? 22)} / 2)`,
2165
- cursor: readonly ? "not-allowed" : "pointer",
2166
- WebkitTapHighlightColor: "transparent",
2167
- ...style
2168
- },
2169
- onPointerDown,
2170
- onKeyDown: (event) => {
2171
- handleKeyDown(event);
2172
- onKeyDown?.(event);
2173
- },
2174
- onFocus: (event) => {
2175
- thumbRef.current?.focus();
2176
- onFocus?.(event);
2177
- },
2178
- onBlur: (event) => {
2179
- thumbRef.current?.blur();
2180
- onBlur?.(event);
2181
- },
2182
- ...props,
1862
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadProvider, {
1863
+ value: context,
2183
1864
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2184
- className: "tremolo-xy-pad-area-wrapper",
2185
- ref: areaElementRef,
2186
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Area, {
2187
- __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb, {
2188
- ref: thumbRef,
2189
- __disabled: disabled,
2190
- __readonly: readonly,
2191
- __css: {
2192
- top: `${percentY}%`,
2193
- left: `${percentX}%`
2194
- },
2195
- ...thumbProps
2196
- }),
2197
- ...areaProps
2198
- })
1865
+ className: (0, clsx.default)("tremolo-xy-pad", className),
1866
+ ref: rootRefCallback,
1867
+ tabIndex: -1,
1868
+ "aria-disabled": disabled,
1869
+ "aria-readonly": readonly,
1870
+ style,
1871
+ onPointerDown,
1872
+ onKeyDown: (event) => {
1873
+ handleKeyDown(event);
1874
+ onKeyDown?.(event);
1875
+ },
1876
+ onFocus: (event) => {
1877
+ thumbRef.current?.focus();
1878
+ onFocus?.(event);
1879
+ },
1880
+ onBlur: (event) => {
1881
+ thumbRef.current?.blur();
1882
+ onBlur?.(event);
1883
+ },
1884
+ ...props,
1885
+ children
2199
1886
  })
2200
1887
  });
2201
1888
  }),
@@ -2288,16 +1975,17 @@ exports.SHORTCUTS = SHORTCUTS;
2288
1975
  exports.Slider = Slider;
2289
1976
  exports.XYPad = XYPad;
2290
1977
  exports.clampPoint = clampPoint;
2291
- exports.getNoteRangeArray = getNoteRangeArray;
2292
1978
  exports.useAnimationFrame = useAnimationFrame;
2293
1979
  exports.useDrag = useDrag;
2294
- exports.useDragWithElement = useDragWithElement;
1980
+ exports.useDragValue = useDragValue;
2295
1981
  exports.useEventListener = useEventListener;
2296
1982
  exports.useInterval = useInterval;
2297
1983
  exports.useLongPress = useLongPress;
2298
1984
  exports.useMIDIAccess = useMIDIAccess;
2299
1985
  exports.useMIDIInput = useMIDIInput;
2300
1986
  exports.useMIDIMessage = useMIDIMessage;
1987
+ exports.useNumberInputContext = useNumberInputContext;
1988
+ exports.usePointsEditorContext = usePointsEditorContext;
2301
1989
  exports.useSliderContext = useSliderContext;
2302
1990
  exports.useWheel = useWheel;
2303
1991