@tremolo-ui/react 0.3.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 (64) hide show
  1. package/dist/index.cjs +1162 -1554
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +58 -93
  4. package/dist/index.d.cts +592 -278
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +592 -278
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1163 -1555
  9. package/dist/index.js.map +1 -1
  10. package/package.json +12 -13
  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 +95 -83
  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 +117 -52
  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 +136 -170
  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 +193 -250
  47. package/src/hooks/useDrag.ts +41 -76
  48. package/src/hooks/useDragValue.ts +149 -0
  49. package/src/hooks/useEventListener.ts +1 -1
  50. package/src/hooks/useWheel.ts +51 -0
  51. package/src/index.ts +15 -13
  52. package/src/components/AnimationCanvas/canvas.ts +0 -46
  53. package/src/components/DragObserver/index.tsx +0 -58
  54. package/src/components/NumberInput/InternalInput.tsx +0 -221
  55. package/src/components/NumberInput/type.ts +0 -58
  56. package/src/components/Piano/KeyLabel.tsx +0 -51
  57. package/src/components/Piano/context.tsx +0 -73
  58. package/src/components/Piano/key.tsx +0 -205
  59. package/src/components/WheelObserver/index.tsx +0 -48
  60. package/src/components/_util/type.ts +0 -1
  61. package/src/hooks/useDragWithElement.ts +0 -72
  62. package/src/hooks/usePianoDrag.ts +0 -75
  63. package/src/hooks/useRefCallbackEvent.ts +0 -34
  64. /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 zustand = require("zustand");
31
- let _tremolo_ui_dom = require("@tremolo-ui/dom");
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,110 +111,96 @@ function useCallbackRef(callback, deps = []) {
180
111
  return (0, react.useCallback)((...args) => callbackRef.current?.(...args), deps);
181
112
  }
182
113
  //#endregion
183
- //#region src/hooks/useEventListener.ts
184
- function useEventListener(target, event, handler, options) {
185
- const listener = useCallbackRef(handler);
114
+ //#region src/hooks/useDragValue.ts
115
+ /**
116
+ * Drive a value with a pointer drag.
117
+ *
118
+ * @returns a ref callback for the element that starts the drag, and whether a drag is in progress
119
+ */
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);
130
+ const [node, setNode] = (0, react.useState)(null);
131
+ const [pixelRangeX, pixelRangeY] = Array.isArray(pixelRange) ? pixelRange : [pixelRange, pixelRange];
186
132
  (0, react.useEffect)(() => {
187
- const node = typeof target === "function" ? target() : target ?? document;
188
133
  if (!node) return;
189
- node.addEventListener(event, listener, options);
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
+ }
150
+ });
151
+ instanceRef.current = instance;
190
152
  return () => {
191
- node.removeEventListener(event, listener, options);
153
+ instanceRef.current = null;
154
+ instance.destroy();
192
155
  };
193
156
  }, [
194
- event,
195
- target,
196
- options,
197
- listener
157
+ node,
158
+ baseElementRef,
159
+ pixelRangeX,
160
+ pixelRangeY,
161
+ valueGetter,
162
+ changeHandler,
163
+ dragStartHandler,
164
+ dragEndHandler
198
165
  ]);
199
- return () => {
200
- (typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
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
201
178
  };
202
179
  }
203
180
  //#endregion
204
- //#region src/hooks/useRefCallbackEvent.ts
205
- function useRefCallbackEvent(event, handler, options, deps = []) {
206
- return (0, react.useMemo)(() => {
207
- let cleanup;
208
- return (node) => {
209
- if (!node) {
210
- cleanup?.();
211
- return;
212
- }
213
- node.addEventListener(event, handler, options);
214
- cleanup = () => {
215
- node.removeEventListener(event, handler, options);
216
- };
217
- };
218
- }, deps);
219
- }
220
- //#endregion
221
- //#region src/hooks/useDrag.ts
181
+ //#region src/hooks/useWheel.ts
222
182
  /**
223
- * @returns [refCallback, pointerDownHandler]
183
+ * Listen to wheel events on an element.
184
+ *
185
+ * The listener is not passive, so the handler may call `preventDefault()`.
186
+ *
187
+ * @returns a ref callback to attach to the element, unused when `target` is given
224
188
  */
225
- function useDrag({ threshold: _threshold = 1, onDrag, onDragStart, onDragEnd }) {
226
- const dragOffsetX = (0, react.useRef)(void 0);
227
- const dragOffsetY = (0, react.useRef)(void 0);
228
- const dragStartX = (0, react.useRef)(0);
229
- const dragStartY = (0, react.useRef)(0);
230
- const threshold = Math.max(_threshold, 1);
231
- const handleDrag = (0, react.useCallback)((event, first = false) => {
232
- const isTouch = event instanceof TouchEvent;
233
- if (isTouch && event.cancelable) event.preventDefault();
234
- const screenX = isTouch ? event.touches[0].screenX : event.screenX;
235
- const screenY = isTouch ? event.touches[0].screenY : event.screenY;
236
- let deltaX = 0;
237
- let deltaY = 0;
238
- if (first) {
239
- dragStartX.current = screenX;
240
- dragStartY.current = screenY;
241
- }
242
- if (dragOffsetX.current) {
243
- deltaX = screenX - dragOffsetX.current;
244
- dragOffsetX.current = screenX;
245
- }
246
- if (dragOffsetY.current) {
247
- deltaY = screenY - dragOffsetY.current;
248
- dragOffsetY.current = screenY;
249
- }
250
- if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return;
251
- onDrag?.(screenX - dragStartX.current, screenY - dragStartY.current, deltaX, deltaY);
252
- }, [threshold, onDrag]);
253
- const pointerDownHandler = (0, react.useCallback)((event) => {
254
- dragOffsetX.current = event.screenX;
255
- dragOffsetY.current = event.screenY;
256
- handleDrag(event, true);
257
- onDragStart?.();
258
- }, [handleDrag, onDragStart]);
259
- const refHandler = useRefCallbackEvent("touchmove", handleDrag, { passive: false }, [onDrag]);
260
- useEventListener(globalThis.window, "mousemove", handleDrag);
261
- useEventListener(globalThis.window, "pointerup", () => {
262
- dragOffsetX.current = void 0;
263
- dragOffsetY.current = void 0;
264
- onDragEnd?.();
265
- });
266
- return [refHandler, pointerDownHandler];
267
- }
268
- //#endregion
269
- //#region src/components/DragObserver/index.tsx
270
- function DragObserver(props) {
271
- const { as: Component = "div", threshold = 1, onDrag, onDragStart, onDragEnd, children, onPointerDown, ...attributes } = props;
272
- const [refCallback, pointerDownHandler] = useDrag({
273
- threshold,
274
- onDrag,
275
- onDragStart,
276
- onDragEnd
277
- });
278
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
279
- ref: refCallback,
280
- onPointerDown: (e) => {
281
- pointerDownHandler(e);
282
- onPointerDown?.(e);
283
- },
284
- ...attributes,
285
- children
286
- });
189
+ function useWheel(onWheel, { requireFocus = false, target } = {}) {
190
+ const wheelHandler = useCallbackRef(onWheel);
191
+ const [node, setNode] = (0, react.useState)(null);
192
+ (0, react.useEffect)(() => {
193
+ const element = target ? target.current : node;
194
+ if (!element) return;
195
+ const instance = (0, _tremolo_ui_dom.createWheel)(element, (event) => wheelHandler(event), { requireFocus });
196
+ return () => instance.destroy();
197
+ }, [
198
+ node,
199
+ target,
200
+ wheelHandler,
201
+ requireFocus
202
+ ]);
203
+ return setNode;
287
204
  }
288
205
  //#endregion
289
206
  //#region src/components/_util/index.ts
@@ -293,84 +210,92 @@ function addUserSelectNone() {
293
210
  function removeUserSelectNone() {
294
211
  window.document.body.classList.remove("tremolo-user-select-none");
295
212
  }
296
- const cursorStyles = {
297
- grabbing: "tremolo-cursor-grabbing",
298
- grab: "tremolo-cursor-grab",
299
- pointer: "tremolo-cursor-pointer",
300
- move: "tremolo-cursor-move",
301
- none: "tremolo-cursor-none"
302
- };
303
- function setCursorStyle(type) {
304
- resetCursorStyle();
305
- window.document.body.classList.add(cursorStyles[type]);
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;
306
222
  }
307
- function resetCursorStyle() {
308
- Object.values(cursorStyles).forEach((s) => {
309
- window.document.body.classList.remove(s);
310
- });
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
+ };
311
262
  }
312
- function calcDrawingState({ value, min, max, skew, startValue, angleRange }) {
313
- const p = (0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew);
314
- const s = (0, _tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
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);
315
272
  const r1 = -angleRange / 2;
316
- const r2 = r1 + Math.min(p, s) * angleRange;
317
- const r3 = r1 + Math.max(p, s) * angleRange;
318
- const r4 = angleRange / 2;
319
273
  return {
320
274
  p,
321
275
  r1,
322
- r2,
323
- r3,
324
- r4,
325
- x1: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r1 - 90)),
326
- y1: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r1 - 90)),
327
- x2: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r2 - 90)),
328
- y2: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r2 - 90)),
329
- x3: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r3 - 90)),
330
- y3: 50 + 50 * Math.sin((0, _tremolo_ui_functions.radian)(r3 - 90)),
331
- x4: 50 + 50 * Math.cos((0, _tremolo_ui_functions.radian)(r4 - 90)),
332
- 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
333
279
  };
334
280
  }
335
- const createKnobStore = (initProps) => {
336
- return (0, zustand.createStore)()(() => ({
337
- ...initProps,
338
- ...calcDrawingState(initProps)
339
- }));
340
- };
341
281
  const KnobContext = (0, react.createContext)(null);
342
- function KnobProvider({ children, ...props }) {
343
- const storeRef = (0, react.useRef)(null);
344
- if (!storeRef.current) storeRef.current = createKnobStore(props);
345
- (0, react.useEffect)(() => {
346
- if (storeRef.current) storeRef.current.setState({
347
- ...props,
348
- ...calcDrawingState(props)
349
- });
350
- else storeRef.current = createKnobStore(props);
351
- }, [props]);
352
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnobContext.Provider, {
353
- value: storeRef.current,
354
- children
355
- });
356
- }
282
+ const KnobProvider = KnobContext.Provider;
357
283
  function useKnobContext(selector) {
358
- const store = (0, react.useContext)(KnobContext);
359
- if (!store) throw new Error("Missing KnobContext.Provider in the tree");
360
- 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;
361
287
  }
362
288
  //#endregion
363
289
  //#region src/components/Knob/ActiveLine.tsx
364
290
  function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
365
291
  const r2 = useKnobContext((s) => s.r2);
366
292
  const r3 = useKnobContext((s) => s.r3);
367
- const x2 = useKnobContext((s) => s.x2);
368
- const y2 = useKnobContext((s) => s.y2);
369
- const x3 = useKnobContext((s) => s.x3);
370
- const y3 = useKnobContext((s) => s.y3);
293
+ const radius = arcRadius(strokeWidth);
294
+ const start = pointOnArc(r2, radius);
295
+ const end = pointOnArc(r3, radius);
371
296
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
372
297
  className: (0, clsx.default)("tremolo-knob-active-line", className),
373
- 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}`,
374
299
  fill: "none",
375
300
  stroke,
376
301
  strokeWidth,
@@ -387,24 +312,21 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
387
312
  const r2 = useKnobContext((s) => s.r2);
388
313
  const r3 = useKnobContext((s) => s.r3);
389
314
  const r4 = useKnobContext((s) => s.r4);
390
- const x1 = useKnobContext((s) => s.x1);
391
- const y1 = useKnobContext((s) => s.y1);
392
- const x2 = useKnobContext((s) => s.x2);
393
- const y2 = useKnobContext((s) => s.y2);
394
- const x3 = useKnobContext((s) => s.x3);
395
- const y3 = useKnobContext((s) => s.y3);
396
- const x4 = useKnobContext((s) => s.x4);
397
- 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);
398
320
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [startValue > min && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
399
321
  className: (0, clsx.default)("tremolo-knob-inactive-line", className),
400
- 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}`,
401
323
  fill: "none",
402
324
  stroke,
403
325
  strokeWidth,
404
326
  ...props
405
327
  }), startValue < max && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
406
328
  className: (0, clsx.default)("tremolo-knob-inactive-line", className),
407
- 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}`,
408
330
  fill: "none",
409
331
  stroke,
410
332
  strokeWidth,
@@ -412,6 +334,19 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
412
334
  })] });
413
335
  }
414
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
415
350
  //#region src/components/Knob/Thumb.tsx
416
351
  function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
417
352
  const angleRange = useKnobContext((s) => s.angleRange);
@@ -441,63 +376,36 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
441
376
  });
442
377
  }
443
378
  //#endregion
444
- //#region src/components/Knob/SVGRoot.tsx
445
- function SVGRoot({ children, style, ...props }) {
446
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
447
- viewBox: `0 0 100 100`,
448
- style: {
449
- display: "block",
450
- overflow: "visible",
451
- ...style
452
- },
453
- ...props,
454
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
455
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InactiveLine, {}),
456
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActiveLine, {}),
457
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$2, {})
458
- ] })
459
- });
460
- }
461
- //#endregion
462
379
  //#region src/components/Knob/index.tsx
463
- const defaultExternalStyles$3 = {
380
+ const defaultExternalStyles$2 = {
464
381
  userSelectNone: true,
465
382
  cursor: "grabbing"
466
383
  };
467
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
+ /**
468
390
  * Interactive rotary knob component implemented in SVG.
469
391
  */
470
392
  const Knob = {
471
- 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) => {
472
- const [dragging, setDragging] = (0, react.useState)(false);
473
- 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) => {
474
394
  const elmRef = (0, react.useRef)(null);
475
395
  const externalStyles = {
476
- ...defaultExternalStyles$3,
396
+ ...defaultExternalStyles$2,
477
397
  ..._externalStyles
478
398
  };
479
- const onDrag = (0, react.useCallback)((_x, y) => {
480
- if (!onChange || readonly) return;
481
- 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));
482
- }, [
483
- max,
399
+ const range = (0, react.useMemo)(() => ({
484
400
  min,
485
- onChange,
486
- readonly,
487
- skew,
488
- step
489
- ]);
490
- const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
491
- let newValue;
492
- if (eventType == "normalized") newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
493
- else newValue = value + x;
494
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
495
- }, [
496
401
  max,
402
+ step,
403
+ scale
404
+ }), [
497
405
  min,
498
- skew,
406
+ max,
499
407
  step,
500
- value
408
+ scale
501
409
  ]);
502
410
  const handleKeyDown = (0, react.useCallback)((event) => {
503
411
  if (!keyboard || !onChange || readonly) return;
@@ -509,40 +417,62 @@ const Knob = {
509
417
  "ArrowDown"
510
418
  ].includes(key)) {
511
419
  event.preventDefault();
512
- const x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
513
- onChange(updateValueByEvent(keyboard[0], x));
420
+ onChange((0, _tremolo_ui_functions.applyDelta)(value, key == "ArrowRight" || key == "ArrowUp" ? 1 : -1, keyboard, range));
514
421
  }
515
422
  }, [
516
423
  keyboard,
517
424
  onChange,
518
425
  readonly,
519
- updateValueByEvent
426
+ value,
427
+ range
520
428
  ]);
521
- const [touchMoveRefCallback, pointerDownHandler] = useDrag({
522
- onDrag,
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,
436
+ cursor: readonly ? void 0 : externalStyles.cursor,
437
+ onChange: (v) => {
438
+ if (readonly) return;
439
+ onChange?.(v[1]);
440
+ },
523
441
  onDragStart: () => {
524
- setDragging(true);
525
- valueRef.current = (0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew);
526
442
  if (externalStyles.userSelectNone) addUserSelectNone();
527
- if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
528
443
  },
529
444
  onDragEnd: () => {
530
- setDragging(false);
531
445
  if (externalStyles.userSelectNone) removeUserSelectNone();
532
- if (externalStyles.cursor) resetCursorStyle();
533
446
  }
534
447
  });
535
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
448
+ const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, useWheel((event) => {
536
449
  if (!wheel || readonly) return;
537
450
  event.preventDefault();
538
451
  if (!onChange || event.deltaY == 0) return;
539
- const x = Math.sign(event.deltaY) * -wheel[1];
540
- onChange(updateValueByEvent(wheel[0], x));
541
- }, { passive: false }, [
542
- wheel,
543
- onChange,
544
- readonly,
545
- updateValueByEvent
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
546
476
  ]);
547
477
  (0, react.useImperativeHandle)(forwardedRef, () => {
548
478
  return {
@@ -555,19 +485,9 @@ const Knob = {
555
485
  };
556
486
  }, []);
557
487
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KnobProvider, {
558
- value,
559
- min,
560
- max,
561
- step,
562
- skew,
563
- startValue,
564
- angleRange,
488
+ value: context,
565
489
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
566
- ref: (elm) => {
567
- elmRef.current = elm;
568
- wheelRefCallback(elm);
569
- touchMoveRefCallback(elm);
570
- },
490
+ ref: rootRefCallback,
571
491
  className: (0, clsx.default)("tremolo-knob", className),
572
492
  tabIndex: 0,
573
493
  role: "slider",
@@ -582,10 +502,7 @@ const Knob = {
582
502
  height: size,
583
503
  ...style
584
504
  },
585
- onPointerDown: (event) => {
586
- pointerDownHandler(event);
587
- onPointerDown?.(event);
588
- },
505
+ onPointerDown,
589
506
  onDoubleClick: (event) => {
590
507
  if (enableDoubleClickDefault && onChange) onChange(defaultValue);
591
508
  onDoubleClick?.(event);
@@ -595,7 +512,7 @@ const Knob = {
595
512
  onKeyDown?.(event);
596
513
  },
597
514
  ...props,
598
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SVGRoot, {})
515
+ children
599
516
  })
600
517
  });
601
518
  }),
@@ -605,121 +522,39 @@ const Knob = {
605
522
  Thumb: Thumb$2
606
523
  };
607
524
  //#endregion
608
- //#region src/components/NumberInput/type.ts
609
- function selectUnit(units, value) {
610
- let i = 0;
611
- for (; i < units.length; i++) if (Math.abs(units[i][1]) > Math.abs(value)) break;
612
- return units[Math.max(0, i - 1)];
525
+ //#region src/components/NumberInput/context.tsx
526
+ const NumberInputContext = (0, react.createContext)(null);
527
+ const NumberInputProvider = NumberInputContext.Provider;
528
+ function useNumberInputContext(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;
613
532
  }
614
- function parseValue(inputString, units, digit) {
615
- const str = inputString.trim();
616
- if (!units || typeof units == "string") {
617
- const m = str.match(/-?\d+(\.\d+)?/);
618
- let v = Number(m?.[0] ?? "0");
619
- v = isNaN(v) ? 0 : v;
620
- return {
621
- rawValue: v,
622
- formatValue: (digit != void 0 ? v.toFixed(digit) : v) + (units ?? ""),
623
- unit: units ?? ""
624
- };
625
- }
626
- const unitList = units.map(([u, _s]) => u);
627
- const scaleList = units.map(([_u, s]) => s);
628
- let rawValue = 0;
629
- let unit = unitList[0];
630
- let formatValue = (digit != void 0 ? rawValue.toFixed(digit) : rawValue) + unit;
631
- const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
632
- if (m) {
633
- rawValue = Number(m[1]) || 0;
634
- const uIndex = unitList.indexOf(m[3]);
635
- rawValue *= uIndex != -1 ? scaleList[uIndex] : 1;
636
- const [u, scale] = selectUnit(units, rawValue);
637
- const v = rawValue / scale;
638
- formatValue = (digit != void 0 ? v.toFixed(digit) : v) + u;
639
- unit = u;
640
- }
641
- return {
642
- rawValue,
643
- formatValue,
644
- unit
645
- };
533
+ const StepperContext = (0, react.createContext)(null);
534
+ const StepperProvider = StepperContext.Provider;
535
+ function useStepperContext() {
536
+ return (0, react.useContext)(StepperContext);
646
537
  }
647
538
  //#endregion
648
- //#region src/components/NumberInput/context.tsx
649
- function safeClamp(value, min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER) {
650
- return (0, _tremolo_ui_functions.clamp)(value, min, max);
651
- }
652
- const createNumberInputStore = (initProps) => {
653
- const DEFAULT_PROPS = {
654
- value: "",
655
- valueAsNumber: 0,
656
- min: 0,
657
- max: 0,
658
- step: 1,
659
- readonly: false,
660
- keepWithinRange: true,
661
- onChange: (_v, _t) => {}
662
- };
663
- return (0, zustand.createStore)()((set) => ({
664
- ...DEFAULT_PROPS,
665
- ...initProps,
666
- value: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).formatValue,
667
- valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
668
- increment: () => set((state) => {
669
- if (state.readonly) return {};
670
- let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
671
- if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
672
- const v = parseValue(String(next), state.units, state.digit).formatValue;
673
- state?.onChange?.(next, v);
674
- return {
675
- value: v,
676
- valueAsNumber: next
677
- };
678
- }),
679
- decrement: () => set((state) => {
680
- if (state.readonly) return {};
681
- let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
682
- if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
683
- const v = parseValue(String(next), state.units, state.digit).formatValue;
684
- state?.onChange?.(next, v);
685
- return {
686
- value: v,
687
- valueAsNumber: next
688
- };
689
- }),
690
- change: (value) => set((state) => {
691
- if (state.readonly) return {};
692
- const v = value ?? "";
693
- return {
694
- value: v,
695
- valueAsNumber: parseValue(v, state.units, state.digit).rawValue
696
- };
697
- })
698
- }));
699
- };
700
- const NumberInputContext = (0, react.createContext)(null);
701
- function NumberInputProvider({ children, ...props }) {
702
- const storeRef = (0, react.useRef)(null);
703
- if (!storeRef.current) storeRef.current = createNumberInputStore(props);
539
+ //#region src/hooks/useEventListener.ts
540
+ function useEventListener(target, event, handler, options) {
541
+ const listener = useCallbackRef(handler);
704
542
  (0, react.useEffect)(() => {
705
- if (storeRef.current) {
706
- const parsed = parseValue(String(props?.value || ""), props?.units, props?.digit);
707
- storeRef.current.setState({
708
- ...props,
709
- value: parsed.formatValue,
710
- valueAsNumber: parsed.rawValue
711
- });
712
- } else storeRef.current = createNumberInputStore(props);
713
- }, [props]);
714
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputContext.Provider, {
715
- value: storeRef.current,
716
- children
717
- });
718
- }
719
- function useNumberInputContext(selector) {
720
- const store = (0, react.useContext)(NumberInputContext);
721
- if (!store) throw new Error("Missing NumberInputContext.Provider in the tree");
722
- return (0, zustand.useStore)(store, selector);
543
+ const node = typeof target === "function" ? target() : target ?? document;
544
+ if (!node) return;
545
+ node.addEventListener(event, listener, options);
546
+ return () => {
547
+ node.removeEventListener(event, listener, options);
548
+ };
549
+ }, [
550
+ event,
551
+ target,
552
+ options,
553
+ listener
554
+ ]);
555
+ return () => {
556
+ (typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
557
+ };
723
558
  }
724
559
  //#endregion
725
560
  //#region src/hooks/useInterval.ts
@@ -757,641 +592,554 @@ function useLongPress(callback, initialDelay = 500, interval = 40) {
757
592
  }, [callback]);
758
593
  }
759
594
  //#endregion
760
- //#region src/components/NumberInput/DecrementStepper.tsx
761
- function DecrementStepper({ size = 12, children, className, ...props }) {
762
- const min = useNumberInputContext((s) => s.min);
763
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
764
- const readonly = useNumberInputContext((s) => s.readonly);
765
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
766
- 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
+ ]));
767
612
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
768
- className: (0, clsx.default)("tremolo-number-input-decrement-stepper", className),
613
+ className: (0, clsx.default)(`tremolo-number-input-${variant}-stepper`, className),
769
614
  role: "button",
770
615
  tabIndex: -1,
771
- "aria-disabled": keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER),
616
+ "aria-label": direction > 0 ? "Increment" : "Decrement",
617
+ "aria-disabled": direction > 0 ? atMax : atMin,
772
618
  "aria-readonly": readonly,
773
- onPointerDown: press,
619
+ style,
620
+ onPointerDown: (event) => {
621
+ if (!readonly) press();
622
+ onPointerDown?.(event);
623
+ },
774
624
  ...props,
775
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
776
- width: size,
777
- height: size,
778
- viewBox: "0 0 24 24",
779
- fill: "none",
780
- stroke: "currentColor",
781
- strokeWidth: "2",
782
- strokeLinecap: "round",
783
- strokeLinejoin: "round",
784
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "6 9 12 15 18 9" })
785
- })
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" })
638
+ });
639
+ }
640
+ //#endregion
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
786
649
  });
787
650
  }
788
651
  //#endregion
789
652
  //#region src/components/NumberInput/IncrementStepper.tsx
790
- function IncrementStepper({ size = 12, children, className, ...props }) {
791
- const max = useNumberInputContext((s) => s.max);
792
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
793
- const readonly = useNumberInputContext((s) => s.readonly);
794
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
795
- const press = useLongPress(useNumberInputContext((s) => s.increment));
796
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
797
- className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
798
- role: "button",
799
- tabIndex: -1,
800
- "aria-disabled": keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER),
801
- "aria-readonly": readonly,
802
- onPointerDown: press,
803
- ...props,
804
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
805
- width: size,
806
- height: size,
807
- viewBox: "0 0 24 24",
808
- fill: "none",
809
- stroke: "currentColor",
810
- strokeWidth: "2",
811
- strokeLinecap: "round",
812
- strokeLinejoin: "round",
813
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "18 15 12 9 6 15" })
814
- })
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
815
660
  });
816
661
  }
817
662
  //#endregion
818
- //#region src/components/NumberInput/InternalInput.tsx
819
- const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFocus, blurOnEnter, clampValueOnBlur, wheel, keyboard, className, onChange, onFocus, onBlur, onKeyDown, children: _children, ...props }, ref) => {
820
- const elmRef = (0, react.useRef)(null);
821
- const [editing, setEditing] = (0, react.useState)(false);
822
- const value = useNumberInputContext((s) => s.value);
823
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
824
- const min = useNumberInputContext((s) => s.min);
825
- const max = useNumberInputContext((s) => s.max);
826
- const step = useNumberInputContext((s) => s.step);
827
- const units = useNumberInputContext((s) => s.units);
828
- const digit = useNumberInputContext((s) => s.digit);
829
- const change = useNumberInputContext((s) => s.change);
830
- const error = (0, react.useMemo)(() => {
831
- if (editing) return false;
832
- const v = valueAsNumber;
833
- return (min ?? -Infinity) > v || v > (max ?? Infinity);
834
- }, [
835
- editing,
836
- max,
837
- min,
838
- valueAsNumber
839
- ]);
840
- const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
841
- let newValue;
842
- if (eventType == "normalized") {
843
- if (!min || !max) throw new Error("[NumberInput] \"min\" and \"max\" are required when InputEventOption[0] is set to \"normalized\".");
844
- newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max) + x, min, max);
845
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
846
- } else {
847
- newValue = valueAsNumber + x;
848
- return (0, _tremolo_ui_functions.stepValue)(newValue, step);
849
- }
850
- }, [
851
- max,
852
- min,
853
- step,
854
- valueAsNumber
855
- ]);
856
- const handleKeyDown = (0, react.useCallback)((event) => {
857
- if (!keyboard || !onChange || readonly) return;
858
- const key = event.key;
859
- if (!["ArrowUp", "ArrowDown"].includes(key)) return;
860
- event.preventDefault();
861
- const x = key == "ArrowUp" ? keyboard[1] : -keyboard[1];
862
- const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
863
- onChange?.(parsed.rawValue, parsed.formatValue);
864
- }, [
865
- keyboard,
866
- readonly,
867
- onChange,
868
- updateValueByEvent
869
- ]);
870
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
871
- if (!wheel || readonly) return;
872
- event.preventDefault();
873
- if (!onChange || event.deltaY == 0) return;
874
- const x = Math.sign(event.deltaY) * -wheel[1];
875
- const parsed = parseValue(String(updateValueByEvent(wheel[0], x)));
876
- onChange?.(parsed.rawValue, parsed.formatValue);
877
- }, { passive: false }, [
878
- wheel,
879
- readonly,
880
- onChange,
881
- updateValueByEvent
882
- ]);
883
- (0, react.useImperativeHandle)(ref, () => {
884
- return {
885
- focus() {
886
- elmRef.current?.focus();
887
- },
888
- blur() {
889
- elmRef.current?.blur();
890
- }
891
- };
892
- }, []);
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();
893
674
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
894
- ref: (input) => {
895
- elmRef.current = input;
896
- wheelRefCallback(input);
897
- },
898
- className: (0, clsx.default)("tremolo-number-input", className),
899
- value,
900
- min,
901
- max,
902
- step,
675
+ ref: useComposedRefs(ref, inputRef),
676
+ className: (0, clsx.default)("tremolo-number-input-field", className),
903
677
  type: "text",
678
+ inputMode: "decimal",
679
+ role: "spinbutton",
680
+ value: text,
681
+ readOnly: readonly,
904
682
  "aria-disabled": disabled,
905
683
  "aria-readonly": readonly,
906
- tabIndex: -1,
907
- "data-error": error,
908
- onChange: (event) => {
909
- if (readonly) return;
910
- const v = event.currentTarget.value;
911
- const cursorPos = event.target.selectionStart;
912
- change(v);
913
- const valueAsNumber = parseValue(v, units, digit).rawValue;
914
- onChange?.(valueAsNumber, v);
915
- if (cursorPos) {
916
- event.target.selectionStart = cursorPos;
917
- event.target.selectionEnd = cursorPos;
918
- }
919
- },
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),
920
692
  onFocus: (event) => {
921
- setEditing(true);
922
- const v = event.currentTarget.value;
923
- const parsed = parseValue(v, units, digit);
924
- if (selectWithFocus == "all") event.currentTarget.setSelectionRange(0, v.length);
925
- else if (selectWithFocus == "number") event.currentTarget.setSelectionRange(0, parsed.formatValue.length - parsed.unit.length);
926
- 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);
927
697
  },
928
698
  onBlur: (event) => {
929
- setEditing(false);
930
- let v = parseValue(event.currentTarget.value, units, digit).rawValue;
931
- if (clampValueOnBlur) v = safeClamp(v, min, max);
932
- const parsed = parseValue(String(v), units, digit);
933
- change(parsed.formatValue);
934
- if (clampValueOnBlur) onChange?.(parsed.rawValue, parsed.formatValue);
935
- onBlur?.(parsed.rawValue, parsed.formatValue, event);
699
+ commitDraft();
700
+ onBlur?.(event);
936
701
  },
937
702
  onKeyDown: (event) => {
938
- if (blurOnEnter && event.key == "Enter") event.currentTarget.blur();
939
- 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
+ }
940
711
  onKeyDown?.(event);
941
712
  },
942
713
  ...props
943
714
  });
944
- });
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
+ }
945
748
  //#endregion
946
749
  //#region src/components/NumberInput/Stepper.tsx
947
- function Stepper({ dynamic = true, children, className, ...props }) {
948
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
949
- className: (0, clsx.default)("tremolo-number-input-stepper", className),
950
- "data-dynamic": dynamic,
951
- ...props,
952
- 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
+ })
953
806
  });
954
807
  }
955
808
  /**
956
809
  * Input with some useful functions for entering numerical values.
957
810
  */
958
811
  const NumberInput = {
959
- 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) => {
960
- const colors = { "--active-color": activeColor };
961
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
962
- 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,
963
832
  min,
964
833
  max,
965
834
  step,
966
- units,
967
- 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
+ }, [
968
846
  readonly,
969
- keepWithinRange,
970
- onChange,
971
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
972
- className: (0, clsx.default)("tremolo-number-input-wrapper", wrapperClassName),
973
- tabIndex: 0,
974
- style: { ...colors },
975
- "data-stepper": !!children,
976
- "data-variant": variant,
977
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InternalInput, {
978
- ref: forwardedRef,
979
- readonly,
980
- disabled,
981
- selectWithFocus,
982
- blurOnEnter,
983
- clampValueOnBlur,
984
- wheel,
985
- keyboard,
986
- className,
987
- onChange,
988
- onFocus,
989
- onBlur,
990
- ...props
991
- }), 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
992
947
  })
993
948
  });
994
949
  }),
950
+ InputField,
995
951
  Stepper,
996
952
  IncrementStepper,
997
953
  DecrementStepper
998
954
  };
999
955
  //#endregion
1000
- //#region src/hooks/usePianoDrag.ts
956
+ //#region src/components/Piano/keyboardShortcuts.ts
1001
957
  /**
1002
- * Internal
1003
- * @returns [refCallback, pointerDownHandler]
1004
- * @private
958
+ * Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
1005
959
  */
1006
- function usePianoDrag({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
1007
- const dragged = (0, react.useRef)(false);
1008
- const normalizedX = (0, react.useRef)(0);
1009
- const normalizedY = (0, react.useRef)(0);
1010
- const handleDrag = (0, react.useCallback)((event) => {
1011
- if (!baseElementRef.current || !dragged.current) return;
1012
- const isTouch = event instanceof TouchEvent;
1013
- if (isTouch && event.cancelable) event.preventDefault();
1014
- const { left, top, right, bottom } = baseElementRef.current.getBoundingClientRect();
1015
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1016
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1017
- const nx = (0, _tremolo_ui_functions.normalizeValue)(mouseX, left, right);
1018
- const ny = (0, _tremolo_ui_functions.normalizeValue)(mouseY, top, bottom);
1019
- normalizedX.current = nx;
1020
- normalizedY.current = ny;
1021
- onDrag(nx, ny);
1022
- }, [onDrag, baseElementRef]);
1023
- const refHandler = useRefCallbackEvent("touchmove", handleDrag, { passive: false }, [onDrag]);
1024
- const pointerDownHandler = (0, react.useCallback)((event) => {
1025
- dragged.current = true;
1026
- handleDrag(event);
1027
- onDragStart?.(normalizedX.current, normalizedY.current);
1028
- }, [handleDrag, onDragStart]);
1029
- useEventListener(globalThis.window, "pointermove", handleDrag);
1030
- useEventListener(globalThis.window, "pointerup", () => {
1031
- if (!dragged.current) return;
1032
- dragged.current = false;
1033
- onDragEnd?.(normalizedX.current, normalizedY.current);
1034
- });
1035
- return {
1036
- refHandler,
1037
- pointerDownHandler
1038
- };
1039
- }
1040
- //#endregion
1041
- //#region src/components/Piano/context.tsx
1042
- const createPianoStore = (initProps) => {
1043
- const DEFAULT_PROPS = {
1044
- noteRange: {
1045
- first: 0,
1046
- last: 127
1047
- },
1048
- glissando: true,
1049
- midiMax: 127,
1050
- fill: false,
1051
- label: () => void 0,
1052
- onPlayNote: () => {},
1053
- onStopNote: () => {}
1054
- };
1055
- return (0, zustand.createStore)()(() => ({
1056
- ...DEFAULT_PROPS,
1057
- ...initProps,
1058
- notePosition: initProps.notePosition
1059
- }));
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
+ ] }
1060
999
  };
1061
- const PianoContext = (0, react.createContext)(null);
1062
- function PianoProvider({ children, ...props }) {
1063
- const storeRef = (0, react.useRef)(null);
1064
- if (!storeRef.current) storeRef.current = createPianoStore(props);
1065
- (0, react.useEffect)(() => {
1066
- if (storeRef.current) storeRef.current.setState(props);
1067
- else storeRef.current = createPianoStore(props);
1068
- }, [props]);
1069
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoContext.Provider, {
1070
- value: storeRef.current,
1071
- children
1072
- });
1073
- }
1074
- function usePianoContext(selector) {
1075
- const store = (0, react.useContext)(PianoContext);
1076
- if (!store) throw new Error("Missing PianoContext.Provider in the tree");
1077
- return (0, zustand.useStore)(store, selector);
1078
- }
1079
- //#endregion
1080
- //#region src/components/Piano/KeyLabel.tsx
1081
- function KeyLabel({ label, className, wrapperClassName, wrapperStyle, __note, __label, ...props }) {
1082
- const index = getNoteRangeArray(usePianoContext((s) => s.noteRange)).indexOf(__note);
1083
- const content = label ? label(__note, index) : __label && __label(__note, index);
1084
- return (content != void 0 || content != null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1085
- className: (0, clsx.default)("tremolo-piano-key-label-wrapper", wrapperClassName),
1086
- style: wrapperStyle,
1087
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1088
- className: (0, clsx.default)("tremolo-piano-key-label", className),
1089
- ...props,
1090
- children: content
1091
- })
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
1092
1031
  });
1093
- }
1094
- const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg, color, activeBg, activeColor, className, style, children, __width, ...props }, ref) => {
1095
- const [played, setPlayed] = (0, react.useState)(false);
1096
- const midiMax = usePianoContext((s) => s.midiMax);
1097
- const fill = usePianoContext((s) => s.fill);
1098
- const onPlayNote = usePianoContext((s) => s.onPlayNote);
1099
- const onStopNote = usePianoContext((s) => s.onStopNote);
1100
- const label = usePianoContext((s) => s.label);
1101
- const position = usePianoContext((s) => s.notePosition)(noteNumber);
1102
- const disabled = noteNumber > midiMax;
1103
- const colors = {
1104
- "--bg": bg,
1105
- "--color": color,
1106
- "--active-bg": activeBg,
1107
- "--active-color": activeColor
1108
- };
1109
- (0, react.useImperativeHandle)(ref, () => {
1110
- return {
1111
- play(velocity) {
1112
- if (disabled) return;
1113
- setPlayed(true);
1114
- if (onPlayNote) onPlayNote(noteNumber, velocity);
1115
- },
1116
- stop() {
1117
- setPlayed(false);
1118
- if (onStopNote) onStopNote(noteNumber);
1119
- },
1120
- played() {
1121
- return played;
1122
- }
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();
1123
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();
1124
1072
  }, [
1125
- disabled,
1126
- noteNumber,
1127
- onPlayNote,
1128
- onStopNote,
1129
- played
1073
+ fill,
1074
+ node,
1075
+ whiteKeyCount,
1076
+ keyGap
1130
1077
  ]);
1131
- let keyLabelProps = {};
1132
- if (children != void 0) react.default.Children.forEach(children, (child) => {
1133
- if (react.default.isValidElement(child)) if (child.type == KeyLabel) keyLabelProps = child.props;
1134
- else throw new Error("only <KeyLabel>");
1135
- 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" });
1136
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
+ }), []);
1137
1100
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1138
- className: (0, clsx.default)(`tremolo-piano-${keyType}-key`, className),
1139
- "aria-disabled": disabled,
1140
- "data-active": played,
1101
+ ref: setNode,
1102
+ className: (0, clsx.default)("tremolo-piano", className),
1141
1103
  style: {
1142
- ...colors,
1143
- left: position,
1144
- width: fill ? __width : width,
1104
+ width: fill ? "100%" : (0, _tremolo_ui_functions.pianoWidth)(layout),
1145
1105
  height,
1146
1106
  ...style
1147
- },
1148
- ...props,
1149
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyLabel, {
1150
- label,
1151
- __note: noteNumber,
1152
- ...keyLabelProps
1153
- })
1154
- });
1155
- });
1156
- const WhiteKey = (0, react.forwardRef)(({ width = 40, height = "100%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
1157
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyImpl, {
1158
- ref,
1159
- keyType: "white",
1160
- width,
1161
- height,
1162
- onContextMenu,
1163
- ...props
1164
- });
1165
- });
1166
- const BlackKey = (0, react.forwardRef)(({ width = 26, height = "60%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
1167
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(KeyImpl, {
1168
- ref,
1169
- keyType: "black",
1170
- width,
1171
- height,
1172
- onContextMenu,
1173
- ...props
1174
- });
1175
- });
1176
- //#endregion
1177
- //#region src/components/Piano/keyboardShortcuts.ts
1178
- const SHORTCUTS = { HOME_ROW: { keys: [
1179
- "a",
1180
- "w",
1181
- "s",
1182
- "e",
1183
- "d",
1184
- "f",
1185
- "t",
1186
- "g",
1187
- "y",
1188
- "h",
1189
- "u",
1190
- "j",
1191
- "k",
1192
- "o",
1193
- "l",
1194
- "p",
1195
- ";"
1196
- ] } };
1197
- //#endregion
1198
- //#region src/components/Piano/index.tsx
1199
- /**
1200
- * [noteRange.first, noteRange.first + 1 ..., noteRange.last]
1201
- */
1202
- function getNoteRangeArray(noteRange) {
1203
- return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1204
- }
1205
- const blackPerWhiteWidth = 26 / 40;
1206
- /**
1207
- * Customizable piano component.
1208
- */
1209
- const Piano = {
1210
- 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) => {
1211
- const [whiteNoteWidth, setWhiteNoteWidth] = (0, react.useState)(_whiteNoteWidth);
1212
- const keyRefs = (0, react.useRef)([]);
1213
- for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) keyRefs.current[i] = (0, react.createRef)();
1214
- const pianoRef = (0, react.useRef)(null);
1215
- const hitKeyIndex = (0, react.useRef)(-1);
1216
- const noteRangeArray = getNoteRangeArray(noteRange);
1217
- const whiteNotes = noteRangeArray.filter((v) => (0, _tremolo_ui_functions.isWhiteKey)(v));
1218
- const blackNotes = noteRangeArray.filter((v) => (0, _tremolo_ui_functions.isBlackKey)(v));
1219
- const whiteNoteCount = whiteNotes.length;
1220
- const padding = 1;
1221
- const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount;
1222
- const childrenWithProps = (0, react.useMemo)(() => {
1223
- return children && react.default.Children.map(children, (child, index) => {
1224
- if (react.default.isValidElement(child)) {
1225
- const __width = child.type == WhiteKey ? whiteNoteWidth : whiteNoteWidth * .65;
1226
- const props = {
1227
- ref: keyRefs.current[index],
1228
- __width
1229
- };
1230
- return react.default.cloneElement(child, props);
1231
- }
1232
- return child;
1233
- });
1234
- }, [children, whiteNoteWidth]);
1235
- const notePosition = (0, react.useCallback)((note) => {
1236
- const pitchPositions = {
1237
- C: 0,
1238
- "C#": 1,
1239
- D: 1,
1240
- "D#": 2,
1241
- E: 2,
1242
- F: 3,
1243
- "F#": 4,
1244
- G: 4,
1245
- "G#": 5,
1246
- A: 5,
1247
- "A#": 6,
1248
- B: 6
1249
- };
1250
- const blackNoteWidth = whiteNoteWidth * blackPerWhiteWidth;
1251
- const padding = 1;
1252
- const targetNoteKey = (0, _tremolo_ui_functions.noteKey)(note);
1253
- const firstNoteKey = (0, _tremolo_ui_functions.noteKey)(noteRange.first);
1254
- const octave = Math.floor((note - noteRange.first) / 12);
1255
- const octaveOffset = _tremolo_ui_functions.noteKeys.indexOf(firstNoteKey) > _tremolo_ui_functions.noteKeys.indexOf(targetNoteKey) ? 1 : 0;
1256
- const w = whiteNoteWidth + padding;
1257
- const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey];
1258
- const blackKeyOffset = (0, _tremolo_ui_functions.isBlackKey)(note) ? blackNoteWidth / 2 : 0;
1259
- return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset;
1260
- }, [noteRange.first, whiteNoteWidth]);
1261
- const getHitKeyIndex = (0, react.useCallback)((x, y) => {
1262
- if (!pianoRef.current) return -1;
1263
- const containerHeight = pianoRef.current.clientHeight;
1264
- const notes = [...blackNotes, ...whiteNotes];
1265
- for (let i = 0; i < notes.length; i++) {
1266
- const note = notes[i];
1267
- const pos = notePosition(note);
1268
- const w = (0, _tremolo_ui_functions.isWhiteKey)(note) ? whiteNoteWidth : whiteNoteWidth * blackPerWhiteWidth;
1269
- const h = (0, _tremolo_ui_functions.isWhiteKey)(note) ? containerHeight : containerHeight * .6;
1270
- if (pos <= x && x < pos + w && 0 <= y && y < h) return note;
1271
- }
1272
- return -1;
1273
- }, [
1274
- blackNotes,
1275
- notePosition,
1276
- whiteNoteWidth,
1277
- whiteNotes
1278
- ]);
1279
- const onDrag = (0, react.useCallback)((perX, perY) => {
1280
- if (!pianoRef.current) return;
1281
- const note = getHitKeyIndex(perX * staticWidth, perY * pianoRef.current.clientHeight);
1282
- const index = noteRangeArray.indexOf(note);
1283
- if (index == -1) return;
1284
- if (hitKeyIndex.current != index) {
1285
- keyRefs.current[hitKeyIndex.current]?.current?.stop();
1286
- keyRefs.current[index]?.current?.play();
1287
- hitKeyIndex.current = index;
1288
- }
1289
- }, [
1290
- getHitKeyIndex,
1291
- noteRangeArray,
1292
- staticWidth
1293
- ]);
1294
- const onDragEnd = (0, react.useCallback)(() => {
1295
- if (keyRefs.current[hitKeyIndex.current]?.current?.played()) keyRefs.current[hitKeyIndex.current]?.current?.stop();
1296
- hitKeyIndex.current = -1;
1297
- }, [keyRefs]);
1298
- (0, react.useEffect)(() => {
1299
- if (fill && pianoRef.current) {
1300
- const parent = pianoRef.current.parentElement;
1301
- if (!parent) throw new Error("doesn't have a parent element.");
1302
- const resizeObserver = new ResizeObserver(() => {
1303
- const w = pianoRef.current.clientWidth;
1304
- setWhiteNoteWidth(w / whiteNoteCount - padding);
1305
- });
1306
- resizeObserver.observe(parent);
1307
- return () => {
1308
- resizeObserver.unobserve(parent);
1309
- };
1310
- } else setWhiteNoteWidth(_whiteNoteWidth);
1311
- }, [
1312
- fill,
1313
- _whiteNoteWidth,
1314
- whiteNoteCount
1315
- ]);
1316
- const { refHandler: touchMoveRefCallback, pointerDownHandler } = usePianoDrag({
1317
- baseElementRef: pianoRef,
1318
- onDrag,
1319
- onDragEnd
1320
- });
1321
- useEventListener(globalThis.window, "keydown", (e) => {
1322
- if (e.repeat) return;
1323
- if (!keyboardShortcuts) return;
1324
- const index = keyboardShortcuts.keys.indexOf(e.key);
1325
- if (index != -1) {
1326
- if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play();
1327
- }
1328
- });
1329
- useEventListener(globalThis.window, "keyup", (e) => {
1330
- if (e.repeat) return;
1331
- if (!keyboardShortcuts) return;
1332
- const index = keyboardShortcuts.keys.indexOf(e.key);
1333
- if (index != -1) keyRefs.current[index]?.current?.stop();
1334
- });
1335
- (0, react.useImperativeHandle)(forwardedRef, () => {
1336
- return {
1337
- playNote(note, velocity) {
1338
- const index = noteRangeArray.indexOf(note);
1339
- if (index != -1) {
1340
- if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play(velocity);
1341
- } else onPlayNote?.(note, velocity);
1342
- },
1343
- stopNote(note) {
1344
- const index = noteRangeArray.indexOf(note);
1345
- if (index != -1) keyRefs.current[index]?.current?.stop();
1346
- else onStopNote?.(note);
1347
- }
1107
+ },
1108
+ ...props,
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
1348
1116
  };
1349
- }, [
1350
- noteRangeArray,
1351
- onPlayNote,
1352
- onStopNote
1353
- ]);
1354
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoProvider, {
1355
- notePosition,
1356
- noteRange,
1357
- glissando,
1358
- midiMax,
1359
- fill,
1360
- onPlayNote,
1361
- onStopNote,
1362
- label,
1363
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1364
- ref: (div) => {
1365
- pianoRef.current = div;
1366
- touchMoveRefCallback(div);
1367
- },
1368
- 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,
1369
1126
  style: {
1370
- width: fill ? "100%" : staticWidth,
1371
- height,
1372
- ...style
1373
- },
1374
- onPointerDown: (event) => {
1375
- pointerDownHandler(event);
1376
- 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}%`
1377
1131
  },
1378
- ...props,
1379
- children: childrenWithProps || noteRangeArray.map((note, index) => (0, _tremolo_ui_functions.isWhiteKey)(note) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WhiteKey, {
1380
- ref: keyRefs.current[index],
1381
- noteNumber: note,
1382
- __width: whiteNoteWidth
1383
- }, note) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlackKey, {
1384
- ref: keyRefs.current[index],
1385
- noteNumber: note,
1386
- __width: whiteNoteWidth * blackPerWhiteWidth
1387
- }, note))
1388
- })
1389
- });
1390
- }),
1391
- WhiteKey,
1392
- BlackKey,
1393
- KeyLabel
1394
- };
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
+ }) };
1395
1143
  //#endregion
1396
1144
  //#region src/components/PointsEditor/Background.tsx
1397
1145
  function Background({ className, children, ...props }) {
@@ -1403,96 +1151,24 @@ function Background({ className, children, ...props }) {
1403
1151
  }
1404
1152
  //#endregion
1405
1153
  //#region src/components/PointsEditor/context.tsx
1406
- const createPointsEditorStore = (initProps) => {
1407
- const DEFAULT_PROPS = {
1408
- containerElementRef: (0, react.createRef)(),
1409
- disabled: false,
1410
- readonly: false,
1411
- externalStyles: {
1412
- userSelectNone: true,
1413
- cursor: "grabbing"
1414
- }
1415
- };
1416
- return (0, zustand.createStore)()((set) => ({
1417
- ...DEFAULT_PROPS,
1418
- ...initProps,
1419
- setContainerElementRef: (containerElementRef) => {
1420
- set(() => ({ containerElementRef }));
1421
- }
1422
- }));
1423
- };
1424
1154
  const PointsEditorContext = (0, react.createContext)(null);
1425
- function PointsEditorProvider({ children, ...props }) {
1426
- const storeRef = (0, react.useRef)(null);
1427
- if (!storeRef.current) storeRef.current = createPointsEditorStore(props);
1428
- (0, react.useEffect)(() => {
1429
- if (storeRef.current) storeRef.current.setState(props);
1430
- else storeRef.current = createPointsEditorStore(props);
1431
- }, [props]);
1432
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorContext.Provider, {
1433
- value: storeRef.current,
1434
- children
1435
- });
1436
- }
1155
+ const PointsEditorProvider = PointsEditorContext.Provider;
1437
1156
  function usePointsEditorContext(selector) {
1438
- const store = (0, react.useContext)(PointsEditorContext);
1439
- if (!store) throw new Error("Missing PointsEditorContext.Provider in the tree");
1440
- 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;
1441
1160
  }
1442
1161
  //#endregion
1443
1162
  //#region src/components/PointsEditor/Container.tsx
1444
- function Container({ className, children, ...props }) {
1445
- const containerElementRef = (0, react.useRef)(null);
1446
- usePointsEditorContext((s) => s.setContainerElementRef)(containerElementRef);
1163
+ function Container({ children, className, ref, ...props }) {
1447
1164
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1448
- ref: containerElementRef,
1165
+ ref: useComposedRefs(ref, usePointsEditorContext((s) => s.containerRef)),
1449
1166
  className: (0, clsx.default)("tremolo-points-editor-container", className),
1450
1167
  ...props,
1451
1168
  children
1452
1169
  });
1453
1170
  }
1454
1171
  //#endregion
1455
- //#region src/hooks/useDragWithElement.ts
1456
- function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
1457
- const [dragging, setDragging] = (0, react.useState)(false);
1458
- const normalizedX = (0, react.useRef)(0);
1459
- const normalizedY = (0, react.useRef)(0);
1460
- const handleDrag = (0, react.useCallback)((event) => {
1461
- if (!baseElementRef.current || !dragging) return;
1462
- const isTouch = event instanceof TouchEvent;
1463
- if (isTouch && event.cancelable) event.preventDefault();
1464
- const { left, top, right, bottom } = baseElementRef.current.getBoundingClientRect();
1465
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX;
1466
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY;
1467
- const nx = (0, _tremolo_ui_functions.normalizeValue)(mouseX, left, right);
1468
- const ny = (0, _tremolo_ui_functions.normalizeValue)(mouseY, top, bottom);
1469
- normalizedX.current = nx;
1470
- normalizedY.current = ny;
1471
- onDrag(nx, ny);
1472
- }, [
1473
- onDrag,
1474
- baseElementRef,
1475
- dragging
1476
- ]);
1477
- const refHandler = useRefCallbackEvent("touchmove", handleDrag, { passive: false }, [onDrag]);
1478
- const pointerDownHandler = (0, react.useCallback)((event) => {
1479
- setDragging(true);
1480
- handleDrag(event);
1481
- onDragStart?.(normalizedX.current, normalizedY.current);
1482
- }, [handleDrag, onDragStart]);
1483
- useEventListener(globalThis.window, "pointermove", handleDrag);
1484
- useEventListener(globalThis.window, "pointerup", () => {
1485
- if (!dragging) return;
1486
- setDragging(false);
1487
- onDragEnd?.(normalizedX.current, normalizedY.current);
1488
- });
1489
- return {
1490
- refHandler,
1491
- pointerDownHandler,
1492
- dragging
1493
- };
1494
- }
1495
- //#endregion
1496
1172
  //#region src/components/PointsEditor/Point.tsx
1497
1173
  function clampPoint(point, min, max) {
1498
1174
  const { x, y } = point;
@@ -1501,48 +1177,88 @@ function clampPoint(point, min, max) {
1501
1177
  y: (0, _tremolo_ui_functions.clamp)(y, min?.y ?? 0, max?.y ?? 1)
1502
1178
  };
1503
1179
  }
1504
- function Point({ value, min, max, size, width = 16, height = 16, color, disabled, readonly, onChange, onDragStart, onDragEnd, className, style, onPointerDown, ...props }) {
1505
- const containerElementRef = usePointsEditorContext((s) => s.containerElementRef);
1506
- const externalStyles = usePointsEditorContext((s) => s.externalStyles);
1507
- const __disabled = usePointsEditorContext((s) => s.disabled);
1508
- const __readonly = usePointsEditorContext((s) => s.readonly);
1509
- const { refHandler: touchMoveRefCallback, pointerDownHandler, dragging } = useDragWithElement({
1510
- baseElementRef: containerElementRef,
1511
- onDrag: (x, y) => {
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,
1198
+ cursor: readonly ? void 0 : externalStyles.cursor,
1199
+ onChange: ([x, y]) => {
1512
1200
  if (readonly) return;
1513
1201
  onChange?.(clampPoint({
1514
1202
  x,
1515
1203
  y
1516
1204
  }, min, max));
1517
1205
  },
1518
- onDragStart: (x, y) => {
1206
+ onDragStart: ([x, y]) => {
1519
1207
  if (readonly) return;
1520
1208
  if (externalStyles.userSelectNone) addUserSelectNone();
1521
- if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
1522
1209
  onDragStart?.(clampPoint({
1523
1210
  x,
1524
1211
  y
1525
1212
  }, min, max));
1526
1213
  },
1527
- onDragEnd: (x, y) => {
1214
+ onDragEnd: ([x, y]) => {
1528
1215
  if (readonly) return;
1529
1216
  if (externalStyles.userSelectNone) removeUserSelectNone();
1530
- if (externalStyles.cursor) resetCursorStyle();
1531
1217
  onDragEnd?.(clampPoint({
1532
1218
  x,
1533
1219
  y
1534
1220
  }, min, max));
1535
1221
  }
1536
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
+ };
1537
1255
  const colors = { "--color": color };
1538
1256
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1539
- ref: (div) => {
1540
- touchMoveRefCallback(div);
1541
- },
1257
+ ref: refCallback,
1542
1258
  className: (0, clsx.default)("tremolo-points-editor-point", className),
1543
1259
  tabIndex: 0,
1544
- "aria-disabled": disabled != void 0 ? disabled : __disabled,
1545
- "aria-readonly": readonly != void 0 ? readonly : __readonly,
1260
+ "aria-disabled": disabled,
1261
+ "aria-readonly": readonly,
1546
1262
  "data-dragging": dragging,
1547
1263
  style: {
1548
1264
  ...colors,
@@ -1552,34 +1268,52 @@ function Point({ value, min, max, size, width = 16, height = 16, color, disabled
1552
1268
  top: `${value.y * 100}%`,
1553
1269
  ...style
1554
1270
  },
1555
- onPointerDown: (event) => {
1556
- pointerDownHandler(event);
1557
- onPointerDown?.(event);
1271
+ onPointerDown,
1272
+ onKeyDown: (event) => {
1273
+ handleKeyDown(event);
1274
+ onKeyDown?.(event);
1558
1275
  },
1559
1276
  ...props
1560
1277
  });
1561
1278
  }
1562
1279
  //#endregion
1563
1280
  //#region src/components/PointsEditor/index.tsx
1564
- const defaultExternalStyles$2 = {
1565
- userSelectNone: true,
1566
- cursor: "grabbing"
1567
- };
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];
1568
1286
  /**
1569
1287
  * Multiple Point Controller
1570
1288
  */
1571
1289
  const PointsEditor = {
1572
- 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 ?? {};
1573
1293
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorProvider, {
1574
- disabled,
1575
- readonly,
1576
- externalStyles: {
1577
- ...defaultExternalStyles$2,
1578
- ..._externalStyles
1579
- },
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
+ ]),
1580
1312
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1581
1313
  ref: forwardedRef,
1582
1314
  className: (0, clsx.default)("tremolo-points-editor", className),
1315
+ "aria-disabled": disabled,
1316
+ "aria-readonly": readonly,
1583
1317
  style: {
1584
1318
  width,
1585
1319
  height,
@@ -1596,59 +1330,33 @@ const PointsEditor = {
1596
1330
  };
1597
1331
  //#endregion
1598
1332
  //#region src/components/Slider/context.tsx
1599
- const createSliderStore = (initProps) => {
1600
- const DEFAULT_PROPS = {
1601
- min: 0,
1602
- max: 0,
1603
- step: 0,
1604
- skew: 0,
1605
- vertical: false,
1606
- reverse: false,
1607
- disabled: false,
1608
- readonly: false
1609
- };
1610
- return (0, zustand.createStore)()(() => ({
1611
- ...DEFAULT_PROPS,
1612
- ...initProps
1613
- }));
1614
- };
1615
1333
  const SliderContext = (0, react.createContext)(null);
1616
- function SliderProvider({ children, ...props }) {
1617
- const storeRef = (0, react.useRef)(null);
1618
- if (!storeRef.current) storeRef.current = createSliderStore(props);
1619
- (0, react.useEffect)(() => {
1620
- if (storeRef.current) storeRef.current.setState(props);
1621
- else storeRef.current = createSliderStore(props);
1622
- }, [props]);
1623
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderContext.Provider, {
1624
- value: storeRef.current,
1625
- children
1626
- });
1627
- }
1334
+ const SliderProvider = SliderContext.Provider;
1628
1335
  function useSliderContext(selector) {
1629
- const store = (0, react.useContext)(SliderContext);
1630
- if (!store) throw new Error("Missing SliderContext.Provider in the tree");
1631
- 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;
1632
1339
  }
1633
1340
  //#endregion
1634
- //#region src/components/Slider/ScaleOption.tsx
1635
- 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 }) {
1636
1343
  const min = useSliderContext((s) => s.min);
1637
1344
  const max = useSliderContext((s) => s.max);
1638
- const skew = useSliderContext((s) => s.skew);
1345
+ const scale = useSliderContext((s) => s.scale);
1639
1346
  const vertical = useSliderContext((s) => s.vertical);
1640
1347
  const reverse = useSliderContext((s) => s.reverse);
1641
- const calcPercent = (0, react.useCallback)((rawValue) => {
1642
- 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);
1643
1351
  }, [
1644
1352
  vertical,
1645
1353
  reverse,
1646
1354
  max,
1647
1355
  min,
1648
- skew
1356
+ scale
1649
1357
  ]);
1650
1358
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1651
- className: (0, clsx.default)("tremolo-slider-scale-option", className),
1359
+ className: (0, clsx.default)("tremolo-slider-marks-option", className),
1652
1360
  style: {
1653
1361
  left: !vertical ? `${calcPercent(value)}%` : void 0,
1654
1362
  top: vertical ? `${calcPercent(value)}%` : void 0,
@@ -1657,7 +1365,7 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
1657
1365
  "data-vertical": vertical,
1658
1366
  ...props,
1659
1367
  children: [type != "number" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1660
- className: (0, clsx.default)("tremolo-slider-scale-option-mark", classes?.mark),
1368
+ className: (0, clsx.default)("tremolo-slider-marks-option-mark", classes?.mark),
1661
1369
  style: {
1662
1370
  width: !vertical ? thickness : length,
1663
1371
  height: vertical ? thickness : length,
@@ -1667,7 +1375,7 @@ function ScaleOption({ value, type = "mark-number", label, thickness = 1, length
1667
1375
  },
1668
1376
  "data-vertical": vertical
1669
1377
  }), type != "mark" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1670
- className: (0, clsx.default)("tremolo-slider-scale-option-label", classes?.label),
1378
+ className: (0, clsx.default)("tremolo-slider-marks-option-label", classes?.label),
1671
1379
  style: {
1672
1380
  width: labelWidth,
1673
1381
  ...styles?.label
@@ -1692,8 +1400,8 @@ function generateOptionsList(options, min, max, step) {
1692
1400
  return optionsList;
1693
1401
  }
1694
1402
  //#endregion
1695
- //#region src/components/Slider/Scale.tsx
1696
- 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 }) {
1697
1405
  const min = useSliderContext((s) => s.min);
1698
1406
  const max = useSliderContext((s) => s.max);
1699
1407
  const step = useSliderContext((s) => s.step);
@@ -1702,7 +1410,7 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
1702
1410
  const optionsList = options ? generateOptionsList(options, min, max, step) : [];
1703
1411
  if ((0, _tremolo_ui_functions.xor)(vertical, reverse)) optionsList.reverse();
1704
1412
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1705
- className: (0, clsx.default)("tremolo-slider-scale", className),
1413
+ className: (0, clsx.default)("tremolo-slider-marks", className),
1706
1414
  style: {
1707
1415
  marginLeft: vertical ? gap : void 0,
1708
1416
  marginTop: !vertical ? gap : void 0,
@@ -1710,72 +1418,70 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
1710
1418
  },
1711
1419
  ...props,
1712
1420
  children: options ? optionsList.map(({ value, type }, index) => {
1713
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ScaleOption, {
1421
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(MarksOption, {
1714
1422
  value,
1715
1423
  type
1716
1424
  }, index);
1717
1425
  }) : children
1718
1426
  });
1719
1427
  }
1720
- const Thumb$1 = (0, react.forwardRef)(({ size, width = 22, height = 22, color, children, className, style, __percent }, ref) => {
1721
- const wrapperRef = (0, react.useRef)(null);
1722
- const vertical = useSliderContext((s) => s.vertical);
1723
- const disabled = useSliderContext((s) => s.disabled);
1724
- const readonly = useSliderContext((s) => s.readonly);
1725
- (0, react.useImperativeHandle)(ref, () => {
1726
- return {
1727
- focus() {
1728
- wrapperRef.current?.focus();
1729
- },
1730
- blur() {
1731
- wrapperRef.current?.blur();
1732
- }
1733
- };
1734
- }, []);
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, []);
1735
1443
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1736
1444
  className: "tremolo-slider-thumb-wrapper",
1737
1445
  style: {
1738
- top: vertical ? `${__percent}%` : "50%",
1739
- left: !vertical ? `${__percent}%` : "50%"
1446
+ top: vertical ? `${percent}%` : "50%",
1447
+ left: !vertical ? `${percent}%` : "50%"
1740
1448
  },
1741
1449
  children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1742
- ref: wrapperRef,
1450
+ ref: elementRef,
1743
1451
  className: (0, clsx.default)("tremolo-slider-thumb", className),
1744
1452
  tabIndex: 0,
1745
1453
  "aria-disabled": disabled,
1746
1454
  "aria-readonly": readonly,
1747
1455
  style: {
1748
1456
  "--color": color,
1749
- width: size ?? width,
1750
- height: size ?? height,
1751
1457
  ...style
1752
1458
  }
1753
1459
  })
1754
1460
  });
1755
- });
1756
- function Track({ length = 140, thickness = 10, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0, ...props }) {
1757
- const vertical = useSliderContext((s) => s.vertical);
1758
- const reverse = useSliderContext((s) => s.reverse);
1759
- 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);
1760
1465
  const direction = vertical ? "bottom" : "right";
1761
1466
  const colors = {
1762
1467
  "--active": active,
1763
1468
  "--inactive": inactive
1764
1469
  };
1765
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1470
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1471
+ ref: composedRef,
1766
1472
  className: (0, clsx.default)("tremolo-slider-track", className),
1767
1473
  "aria-disabled": disabled,
1768
1474
  "data-vertical": vertical,
1769
1475
  style: !defaultStyle ? style : {
1770
1476
  ...colors,
1771
- 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}%)`,
1772
1478
  borderRadius: (0, _tremolo_ui_functions.styleHelper)(thickness, "/", 2),
1773
1479
  width: !vertical ? length : thickness,
1774
1480
  height: vertical ? length : thickness,
1775
1481
  ...style
1776
1482
  },
1777
1483
  ...props,
1778
- children: [children, __thumb]
1484
+ children
1779
1485
  });
1780
1486
  }
1781
1487
  //#endregion
@@ -1785,55 +1491,37 @@ const defaultExternalStyles$1 = {
1785
1491
  cursor: "pointer"
1786
1492
  };
1787
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
+ /**
1788
1499
  * Customizable slider
1789
1500
  */
1790
1501
  const Slider = {
1791
- 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) => {
1792
- 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);
1793
1504
  const thumbRef = (0, react.useRef)(null);
1794
1505
  const externalStyles = {
1795
1506
  ...defaultExternalStyles$1,
1796
1507
  ..._externalStyles
1797
1508
  };
1798
- 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);
1799
1510
  const rev = (0, _tremolo_ui_functions.toFixed)(100 - p);
1800
1511
  const displayReversed = (0, react.useMemo)(() => (0, _tremolo_ui_functions.xor)(vertical, reverse), [vertical, reverse]);
1801
1512
  const percent = displayReversed ? rev : p;
1802
- let trackProps = {};
1803
- let thumbProps = {};
1804
- let scaleComponent = void 0;
1805
- if (children != void 0) react.default.Children.forEach(children, (child) => {
1806
- if (react.default.isValidElement(child)) if (child.type == Thumb$1) thumbProps = child.props;
1807
- else if (child.type == Track) trackProps = child.props;
1808
- else if (child.type == Scale) scaleComponent = child;
1809
- else throw new Error("only <SliderThumb> or <SliderTrack>");
1810
- else throw new Error("children is an invalid element.");
1811
- });
1812
- const onDrag = (0, react.useCallback)((nx, ny) => {
1813
- if (!onChange || readonly) return;
1814
- const n = vertical ? ny : nx;
1815
- 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));
1816
- }, [
1817
- displayReversed,
1818
- max,
1513
+ const axis = (0, react.useMemo)(() => ({
1819
1514
  min,
1820
- onChange,
1821
- readonly,
1822
- skew,
1823
- step,
1824
- vertical
1825
- ]);
1826
- const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
1827
- let newValue;
1828
- if (eventType == "normalized") newValue = (0, _tremolo_ui_functions.rawValue)((0, _tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
1829
- else newValue = value + x;
1830
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(newValue, step), min, max);
1831
- }, [
1832
1515
  max,
1516
+ step,
1517
+ scale,
1518
+ reverse: displayReversed
1519
+ }), [
1833
1520
  min,
1834
- skew,
1521
+ max,
1835
1522
  step,
1836
- value
1523
+ scale,
1524
+ displayReversed
1837
1525
  ]);
1838
1526
  const handleKeyDown = (0, react.useCallback)((event) => {
1839
1527
  if (!keyboard || !onChange || readonly) return;
@@ -1845,51 +1533,76 @@ const Slider = {
1845
1533
  "ArrowDown"
1846
1534
  ].includes(key)) {
1847
1535
  event.preventDefault();
1848
- let x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
1849
- if (reverse) x *= -1;
1850
- 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));
1851
1539
  }
1852
1540
  }, [
1853
1541
  keyboard,
1854
1542
  onChange,
1855
1543
  readonly,
1856
1544
  reverse,
1857
- updateValueByEvent
1545
+ value,
1546
+ axis
1858
1547
  ]);
1859
- const { refHandler: touchMoveRefCallback, pointerDownHandler } = useDragWithElement({
1860
- baseElementRef: trackElementRef,
1861
- onDrag,
1862
- onDragStart: (nx, ny) => {
1548
+ const valueOf = (v) => v[vertical ? 1 : 0];
1549
+ const { refCallback: dragRefCallback } = useDragValue({
1550
+ axis,
1551
+ baseElementRef: trackRef,
1552
+ cursor: readonly ? void 0 : externalStyles.cursor,
1553
+ updateOnPointerDown: true,
1554
+ onChange: (v) => {
1555
+ if (readonly) return;
1556
+ onChange?.(valueOf(v));
1557
+ },
1558
+ onDragStart: (v) => {
1863
1559
  if (readonly) return;
1864
1560
  if (externalStyles.userSelectNone) addUserSelectNone();
1865
- if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
1866
1561
  thumbRef.current?.focus();
1867
- 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));
1868
1563
  },
1869
- onDragEnd: (nx, ny) => {
1564
+ onDragEnd: (v) => {
1870
1565
  if (readonly) return;
1871
1566
  if (externalStyles.userSelectNone) removeUserSelectNone();
1872
- if (externalStyles.cursor) resetCursorStyle();
1873
- 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));
1874
1568
  }
1875
1569
  });
1876
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
1570
+ const rootRefCallback = useComposedRefs(dragRefCallback, useWheel((event) => {
1877
1571
  if (!wheel || !onChange || readonly) return;
1878
1572
  event.preventDefault();
1879
- let x;
1880
- 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;
1881
1575
  else {
1882
1576
  if (event.deltaY == 0) return;
1883
- x = event.deltaY > 0 ? -wheel[1] : wheel[1];
1577
+ direction = event.deltaY > 0 ? -1 : 1;
1884
1578
  }
1885
- if (vertical && reverse) x *= -1;
1886
- onChange(updateValueByEvent(wheel[0], x));
1887
- }, { passive: false }, [
1888
- wheel,
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,
1889
1588
  vertical,
1589
+ reverse,
1590
+ disabled,
1890
1591
  readonly,
1891
- onChange,
1892
- updateValueByEvent
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
1893
1606
  ]);
1894
1607
  (0, react.useImperativeHandle)(forwardedRef, () => {
1895
1608
  return {
@@ -1902,20 +1615,10 @@ const Slider = {
1902
1615
  };
1903
1616
  }, []);
1904
1617
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderProvider, {
1905
- min,
1906
- max,
1907
- step,
1908
- skew,
1909
- vertical,
1910
- reverse,
1911
- disabled,
1912
- readonly,
1618
+ value: context,
1913
1619
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1914
1620
  className: (0, clsx.default)("tremolo-slider", className),
1915
- ref: (div) => {
1916
- wheelRefCallback(div);
1917
- touchMoveRefCallback(div);
1918
- },
1621
+ ref: rootRefCallback,
1919
1622
  tabIndex: -1,
1920
1623
  role: "slider",
1921
1624
  "aria-valuenow": value,
@@ -1924,14 +1627,9 @@ const Slider = {
1924
1627
  "aria-orientation": vertical ? "vertical" : "horizontal",
1925
1628
  "aria-disabled": disabled,
1926
1629
  "aria-readonly": readonly,
1927
- style: {
1928
- margin: `calc(${(0, _tremolo_ui_functions.styleHelper)(thumbProps?.size ?? 22)} / 2)`,
1929
- ...style
1930
- },
1931
- onPointerDown: (event) => {
1932
- pointerDownHandler(event);
1933
- onPointerDown?.(event);
1934
- },
1630
+ "data-vertical": vertical,
1631
+ style,
1632
+ onPointerDown,
1935
1633
  onKeyDown: (event) => {
1936
1634
  handleKeyDown(event);
1937
1635
  onKeyDown?.(event);
@@ -1945,93 +1643,30 @@ const Slider = {
1945
1643
  onBlur?.(event);
1946
1644
  },
1947
1645
  ...props,
1948
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1949
- style: {
1950
- display: "flex",
1951
- flexDirection: vertical ? "row" : "column",
1952
- width: !vertical ? "100%" : void 0,
1953
- height: vertical ? "100%" : void 0
1954
- },
1955
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1956
- className: "tremolo-slider-track-wrapper",
1957
- ref: trackElementRef,
1958
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Track, {
1959
- __percent: percent,
1960
- __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$1, {
1961
- ref: thumbRef,
1962
- __percent: percent,
1963
- ...thumbProps
1964
- }),
1965
- ...trackProps
1966
- })
1967
- }), scaleComponent]
1968
- })
1646
+ children
1969
1647
  })
1970
1648
  });
1971
1649
  }),
1972
1650
  Thumb: Thumb$1,
1973
1651
  Track,
1974
- Scale,
1975
- ScaleOption
1652
+ Marks,
1653
+ MarksOption
1976
1654
  };
1977
1655
  //#endregion
1978
- //#region src/components/_util/composeRefs.tsx
1979
- /**
1980
- * Set a given ref to a given value
1981
- * This utility takes care of different types of refs: callback refs and RefObject(s)
1982
- */
1983
- function setRef(ref, value) {
1984
- if (typeof ref === "function") return ref(value);
1985
- else if (ref !== null && ref !== void 0) ref.current = value;
1986
- }
1987
- /**
1988
- * A utility to compose multiple refs together
1989
- * Accepts callback refs and RefObject(s)
1990
- */
1991
- function composeRefs(...refs) {
1992
- return (node) => {
1993
- let hasCleanup = false;
1994
- const cleanups = refs.map((ref) => {
1995
- const cleanup = setRef(ref, node);
1996
- if (!hasCleanup && typeof cleanup == "function") hasCleanup = true;
1997
- return cleanup;
1998
- });
1999
- if (hasCleanup) return () => {
2000
- for (let i = 0; i < cleanups.length; i++) {
2001
- const cleanup = cleanups[i];
2002
- if (typeof cleanup == "function") cleanup();
2003
- else setRef(refs[i], null);
2004
- }
2005
- };
2006
- };
2007
- }
2008
- //#endregion
2009
- //#region src/components/WheelObserver/index.tsx
2010
- /**
2011
- * @example
2012
- * <WheelObserver
2013
- * onWheel={(event) => {
2014
- * event.preventDefault()
2015
- * console.log(event.deltaY)
2016
- * }
2017
- * >
2018
- * <div>Wheel here</div>
2019
- * </WheelObserver>
2020
- */
2021
- function WheelObserver(props) {
2022
- const { as: Component = "div", children, onWheel, ref, ...attributes } = props;
2023
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
2024
- ref: composeRefs(ref, useRefCallbackEvent("wheel", (event) => {
2025
- if (onWheel) onWheel(event);
2026
- }, { passive: false }, [onWheel])),
2027
- ...attributes,
2028
- children
2029
- });
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;
2030
1663
  }
2031
1664
  //#endregion
2032
1665
  //#region src/components/XYPad/Area.tsx
2033
- function Area({ width = 120, height = 120, color, children, className, style, __thumb, ...props }) {
2034
- 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),
2035
1670
  className: (0, clsx.default)("tremolo-xy-pad-area", className),
2036
1671
  style: {
2037
1672
  "--color": color,
@@ -2040,121 +1675,105 @@ function Area({ width = 120, height = 120, color, children, className, style, __
2040
1675
  ...style
2041
1676
  },
2042
1677
  ...props,
2043
- children: [children, __thumb]
1678
+ children
2044
1679
  });
2045
1680
  }
2046
1681
  //#endregion
2047
1682
  //#region src/components/XYPad/Thumb.tsx
2048
- const Thumb = (0, react.forwardRef)(({ size, width = 22, height = 22, color, children, wrapperClassName, wrapperStyle, className, style, __disabled, __readonly, __css, ...props }, ref) => {
2049
- const wrapperRef = (0, react.useRef)(null);
2050
- (0, react.useImperativeHandle)(ref, () => {
2051
- return {
2052
- focus() {
2053
- wrapperRef.current?.focus();
2054
- },
2055
- blur() {
2056
- wrapperRef.current?.blur();
2057
- }
2058
- };
2059
- }, []);
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, []);
2060
1696
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2061
1697
  className: (0, clsx.default)("tremolo-xy-pad-thumb-wrapper", wrapperClassName),
2062
1698
  style: {
2063
- ...__css,
1699
+ left: `${percent[0]}%`,
1700
+ top: `${percent[1]}%`,
2064
1701
  ...wrapperStyle
2065
1702
  },
2066
1703
  ...props,
2067
1704
  children: children ? children : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2068
- ref: wrapperRef,
1705
+ ref: elementRef,
2069
1706
  className: (0, clsx.default)("tremolo-xy-pad-thumb", className),
2070
1707
  tabIndex: 0,
2071
- "aria-disabled": __disabled,
2072
- "aria-readonly": __readonly,
1708
+ "aria-disabled": disabled,
1709
+ "aria-readonly": readonly,
2073
1710
  style: {
2074
1711
  "--color": color,
2075
- width: size ?? width,
2076
- height: size ?? height,
2077
1712
  ...style
2078
1713
  }
2079
1714
  })
2080
1715
  });
2081
- });
1716
+ }
2082
1717
  //#endregion
2083
1718
  //#region src/components/XYPad/index.tsx
2084
- const defaultValueOptions = {
2085
- skew: 1,
2086
- step: 1,
2087
- reverse: false,
2088
- wheel: ["raw", 1],
2089
- keyboard: ["raw", 1]
2090
- };
2091
1719
  const defaultExternalStyles = {
2092
1720
  userSelectNone: true,
2093
1721
  cursor: "pointer"
2094
1722
  };
2095
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
+ /**
2096
1729
  * Simple XYPad
2097
1730
  */
2098
1731
  const XYPad = {
2099
- 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) => {
2100
- const x = (0, react.useMemo)(() => {
2101
- return {
2102
- ...defaultValueOptions,
2103
- ..._x
2104
- };
2105
- }, [_x]);
2106
- const y = (0, react.useMemo)(() => {
2107
- return {
2108
- ...defaultValueOptions,
2109
- ..._y
2110
- };
2111
- }, [_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) => {
2112
1733
  const rootRef = (0, react.useRef)(null);
2113
- const areaElementRef = (0, react.useRef)(null);
1734
+ const areaRef = (0, react.useRef)(null);
2114
1735
  const thumbRef = (0, react.useRef)(null);
2115
1736
  const externalStyles = {
2116
1737
  ...defaultExternalStyles,
2117
1738
  ..._externalStyles
2118
1739
  };
2119
- const nx = (0, _tremolo_ui_functions.normalizeValue)(x.value, x.min, x.max, x.skew);
2120
- const ny = (0, _tremolo_ui_functions.normalizeValue)(y.value, y.min, y.max, y.skew);
2121
- const percentX = (0, _tremolo_ui_functions.toFixed)((x.reverse ? 1 - nx : nx) * 100);
2122
- const percentY = (0, _tremolo_ui_functions.toFixed)((y.reverse ? 1 - ny : ny) * 100);
2123
- let areaProps = {};
2124
- let thumbProps = {};
2125
- if (children != void 0) react.default.Children.forEach(children, (child) => {
2126
- if (react.default.isValidElement(child)) if (child.type == Thumb) thumbProps = child.props;
2127
- else if (child.type == Area) areaProps = child.props;
2128
- else throw new Error("only <XYPadThumb> or <XYPadArea>");
2129
- else throw new Error("children is an invalid element.");
2130
- });
2131
- const onDrag = (0, react.useCallback)((nx, ny) => {
2132
- if (!onChange || readonly) return;
2133
- const vx = (0, _tremolo_ui_functions.rawValue)(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew);
2134
- const vy = (0, _tremolo_ui_functions.rawValue)(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew);
2135
- 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));
2136
1748
  }, [
2137
- onChange,
2138
- readonly,
2139
- x.max,
2140
- x.min,
2141
- x.reverse,
2142
- x.skew,
2143
- x.step,
2144
- y.max,
2145
- y.min,
2146
- y.reverse,
2147
- y.skew,
2148
- 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
2149
1774
  ]);
2150
- const updateValueByEvent = (0, react.useCallback)((eventType, target, x) => {
2151
- let v;
2152
- 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);
2153
- else v = target.value + x;
2154
- return (0, _tremolo_ui_functions.clamp)((0, _tremolo_ui_functions.stepValue)(v, target.step), target.min, target.max);
2155
- }, []);
2156
1775
  const handleKeyDown = (0, react.useCallback)((event) => {
2157
- if (!onChange || readonly) return;
1776
+ if (!onChange || readonly || !keyboard) return;
2158
1777
  const key = event.key;
2159
1778
  if ([
2160
1779
  "ArrowRight",
@@ -2162,62 +1781,72 @@ const XYPad = {
2162
1781
  "ArrowUp",
2163
1782
  "ArrowDown"
2164
1783
  ].includes(key)) {
2165
- const isHorizontal = key == "ArrowRight" || key == "ArrowLeft";
2166
- const target = isHorizontal ? x : y;
2167
- if (!target.keyboard) return;
1784
+ const i = key == "ArrowRight" || key == "ArrowLeft" ? 0 : 1;
2168
1785
  event.preventDefault();
2169
- let delta = target.keyboard[1];
2170
- if (key == "ArrowLeft" || key == "ArrowUp") delta *= -1;
2171
- if (target.reverse) delta *= -1;
2172
- const newValue = updateValueByEvent(target.keyboard[0], target, delta);
2173
- if (isHorizontal) onChange(newValue, y.value);
2174
- 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));
2175
1790
  }
2176
1791
  }, [
2177
1792
  onChange,
2178
1793
  readonly,
2179
- x,
2180
- y,
2181
- updateValueByEvent
1794
+ keyboard,
1795
+ reverse,
1796
+ nudge
2182
1797
  ]);
2183
- const { refHandler: touchMoveRefCallback, pointerDownHandler } = useDragWithElement({
2184
- baseElementRef: areaElementRef,
2185
- onDrag,
2186
- onDragStart: (nx, ny) => {
1798
+ const { refCallback: dragRefCallback } = useDragValue({
1799
+ axis,
1800
+ baseElementRef: areaRef,
1801
+ updateOnPointerDown: true,
1802
+ cursor: readonly ? void 0 : externalStyles.cursor,
1803
+ onChange: (v) => {
1804
+ if (readonly) return;
1805
+ onChange?.(v);
1806
+ },
1807
+ onDragStart: (v) => {
2187
1808
  if (readonly) return;
2188
1809
  if (externalStyles.userSelectNone) addUserSelectNone();
2189
- if (externalStyles.cursor) setCursorStyle(externalStyles.cursor);
2190
1810
  thumbRef.current?.focus();
2191
- 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);
2192
- 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);
2193
- onDragStart?.(valueX, valueY);
1811
+ onDragStart?.(v);
2194
1812
  },
2195
- onDragEnd: (nx, ny) => {
1813
+ onDragEnd: (v) => {
2196
1814
  if (readonly) return;
2197
1815
  if (externalStyles.userSelectNone) removeUserSelectNone();
2198
- if (externalStyles.cursor) resetCursorStyle();
2199
- 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);
2200
- 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);
2201
- onDragEnd?.(valueX, valueY);
1816
+ onDragEnd?.(v);
2202
1817
  }
2203
1818
  });
2204
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
2205
- if (!onChange || readonly) return;
2206
- const target = event.shiftKey ? x : y;
2207
- 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;
2208
1822
  event.preventDefault();
2209
- let delta = target.wheel[1];
2210
- if (event.deltaY < 0) delta *= -1;
2211
- if (target.reverse) delta *= -1;
2212
- const newValue = updateValueByEvent(target.wheel[0], target, delta);
2213
- if (event.shiftKey) onChange(newValue, y.value);
2214
- else onChange(x.value, newValue);
2215
- }, { passive: false }, [
2216
- x,
2217
- y,
2218
- onChange,
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,
2219
1848
  readonly,
2220
- updateValueByEvent
1849
+ percent
2221
1850
  ]);
2222
1851
  (0, react.useImperativeHandle)(forwardedRef, () => {
2223
1852
  return {
@@ -2230,51 +1859,30 @@ const XYPad = {
2230
1859
  original: rootRef
2231
1860
  };
2232
1861
  }, []);
2233
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2234
- className: (0, clsx.default)("tremolo-xy-pad", className),
2235
- ref: composeRefs(rootRef, wheelRefCallback, touchMoveRefCallback),
2236
- tabIndex: -1,
2237
- "aria-disabled": disabled,
2238
- "aria-readonly": readonly,
2239
- style: {
2240
- margin: `calc(${(0, _tremolo_ui_functions.styleHelper)(thumbProps?.size ?? 22)} / 2)`,
2241
- cursor: readonly ? "not-allowed" : "pointer",
2242
- WebkitTapHighlightColor: "transparent",
2243
- ...style
2244
- },
2245
- onPointerDown: (event) => {
2246
- pointerDownHandler(event);
2247
- onPointerDown?.(event);
2248
- },
2249
- onKeyDown: (event) => {
2250
- handleKeyDown(event);
2251
- onKeyDown?.(event);
2252
- },
2253
- onFocus: (event) => {
2254
- thumbRef.current?.focus();
2255
- onFocus?.(event);
2256
- },
2257
- onBlur: (event) => {
2258
- thumbRef.current?.blur();
2259
- onBlur?.(event);
2260
- },
2261
- ...props,
1862
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadProvider, {
1863
+ value: context,
2262
1864
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2263
- className: "tremolo-xy-pad-area-wrapper",
2264
- ref: areaElementRef,
2265
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Area, {
2266
- __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb, {
2267
- ref: thumbRef,
2268
- __disabled: disabled,
2269
- __readonly: readonly,
2270
- __css: {
2271
- top: `${percentY}%`,
2272
- left: `${percentX}%`
2273
- },
2274
- ...thumbProps
2275
- }),
2276
- ...areaProps
2277
- })
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
2278
1886
  })
2279
1887
  });
2280
1888
  }),
@@ -2347,7 +1955,6 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
2347
1955
  }
2348
1956
  //#endregion
2349
1957
  exports.AnimationCanvas = AnimationCanvas;
2350
- exports.DragObserver = DragObserver;
2351
1958
  exports.Knob = Knob;
2352
1959
  Object.defineProperty(exports, "NOT_SUPPORTED", {
2353
1960
  enumerable: true,
@@ -2366,19 +1973,20 @@ exports.Piano = Piano;
2366
1973
  exports.PointsEditor = PointsEditor;
2367
1974
  exports.SHORTCUTS = SHORTCUTS;
2368
1975
  exports.Slider = Slider;
2369
- exports.WheelObserver = WheelObserver;
2370
1976
  exports.XYPad = XYPad;
2371
1977
  exports.clampPoint = clampPoint;
2372
- exports.getNoteRangeArray = getNoteRangeArray;
2373
1978
  exports.useAnimationFrame = useAnimationFrame;
2374
1979
  exports.useDrag = useDrag;
2375
- exports.useDragWithElement = useDragWithElement;
1980
+ exports.useDragValue = useDragValue;
2376
1981
  exports.useEventListener = useEventListener;
2377
1982
  exports.useInterval = useInterval;
2378
1983
  exports.useLongPress = useLongPress;
2379
1984
  exports.useMIDIAccess = useMIDIAccess;
2380
1985
  exports.useMIDIInput = useMIDIInput;
2381
1986
  exports.useMIDIMessage = useMIDIMessage;
1987
+ exports.useNumberInputContext = useNumberInputContext;
1988
+ exports.usePointsEditorContext = usePointsEditorContext;
2382
1989
  exports.useSliderContext = useSliderContext;
1990
+ exports.useWheel = useWheel;
2383
1991
 
2384
1992
  //# sourceMappingURL=index.cjs.map