@tremolo-ui/react 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +2272 -1650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1461 -475
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1461 -475
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2258 -1648
- package/dist/index.js.map +1 -1
- package/package.json +32 -31
- package/src/components/AnimationCanvas/index.tsx +162 -194
- package/src/components/Knob/ActiveLine.tsx +9 -8
- package/src/components/Knob/InactiveLine.tsx +11 -14
- package/src/components/Knob/SVGRoot.tsx +10 -17
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +73 -89
- package/src/components/Knob/index.tsx +227 -105
- package/src/components/NumberInput/DecrementStepper.tsx +21 -50
- package/src/components/NumberInput/IncrementStepper.tsx +22 -50
- package/src/components/NumberInput/InputField.tsx +206 -0
- package/src/components/NumberInput/Stepper.tsx +150 -21
- package/src/components/NumberInput/StepperButton.tsx +95 -0
- package/src/components/NumberInput/context.tsx +104 -149
- package/src/components/NumberInput/index.tsx +461 -110
- package/src/components/Piano/index.tsx +413 -263
- package/src/components/Piano/keyboard-shortcuts.ts +61 -0
- package/src/components/PointsEditor/Background.tsx +19 -4
- package/src/components/PointsEditor/Container.tsx +92 -22
- package/src/components/PointsEditor/Point.tsx +293 -53
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +97 -75
- package/src/components/PointsEditor/index.tsx +497 -62
- package/src/components/Slider/Marks.tsx +84 -0
- package/src/components/Slider/MarksOption.tsx +115 -0
- package/src/components/Slider/Thumb.tsx +99 -54
- package/src/components/Slider/Track.tsx +73 -62
- package/src/components/Slider/context.tsx +40 -53
- package/src/components/Slider/index.tsx +237 -190
- package/src/components/Slider/type.ts +27 -9
- package/src/components/XYPad/Area.tsx +52 -33
- package/src/components/XYPad/Thumb.tsx +121 -60
- package/src/components/XYPad/context.tsx +49 -0
- package/src/components/XYPad/index.tsx +338 -246
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +170 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +39 -9
- package/src/hooks/useDragValue.ts +194 -0
- package/src/hooks/useEventListener.ts +11 -7
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/hooks/useWheel.ts +29 -9
- package/src/index.ts +50 -27
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -536
- package/src/Globals.d.ts +0 -3
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/Knob/index.css +0 -77
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- package/src/components/NumberInput/index.css +0 -185
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/index.css +0 -90
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/Piano/keyboardShortcuts.ts +0 -31
- package/src/components/PointsEditor/index.css +0 -30
- package/src/components/Slider/Scale.tsx +0 -52
- package/src/components/Slider/ScaleOption.tsx +0 -103
- package/src/components/Slider/index.css +0 -112
- package/src/components/XYPad/index.css +0 -48
- package/src/components/_util/index.ts +0 -28
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -100
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- package/src/styles/global.css +0 -19
package/dist/index.js
CHANGED
|
@@ -1,146 +1,191 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { useComposedRefs } from "./compose-refs.js";
|
|
2
|
+
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
+
import { NOT_SUPPORTED, PERMISSION_DENIED, PITCH_BEND_CENTER, UNAVAILABLE, createAnimationCanvas, createDrag, createDragValue, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createSelectionBox, createWheel, elementMapping, relativeMapping, toXY } from "@tremolo-ui/dom";
|
|
3
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
-
import { clamp, decimalPart,
|
|
5
|
-
import { NOT_SUPPORTED, PERMISSION_DENIED, createDrag, createMIDIAccess, createMIDIInput, createMIDIMessage, createWheel } from "@tremolo-ui/dom";
|
|
6
|
-
import { createStore, useStore } from "zustand";
|
|
7
|
-
//#region src/components/AnimationCanvas/canvas.ts
|
|
8
|
-
const drawingState = [
|
|
9
|
-
"strokeStyle",
|
|
10
|
-
"fillStyle",
|
|
11
|
-
"globalAlpha",
|
|
12
|
-
"lineWidth",
|
|
13
|
-
"lineCap",
|
|
14
|
-
"lineJoin",
|
|
15
|
-
"miterLimit",
|
|
16
|
-
"lineDashOffset",
|
|
17
|
-
"shadowOffsetX",
|
|
18
|
-
"shadowOffsetY",
|
|
19
|
-
"shadowBlur",
|
|
20
|
-
"shadowColor",
|
|
21
|
-
"globalCompositeOperation",
|
|
22
|
-
"font",
|
|
23
|
-
"textAlign",
|
|
24
|
-
"textBaseline",
|
|
25
|
-
"direction",
|
|
26
|
-
"imageSmoothingEnabled"
|
|
27
|
-
];
|
|
28
|
-
function setDprConfig(canvas, context, width, height, dpr) {
|
|
29
|
-
canvas.width = width * dpr;
|
|
30
|
-
canvas.height = height * dpr;
|
|
31
|
-
context.setTransform(1, 0, 0, 1, 0, 0);
|
|
32
|
-
context.scale(dpr, dpr);
|
|
33
|
-
canvas.style.width = `${width}px`;
|
|
34
|
-
canvas.style.height = `${height}px`;
|
|
35
|
-
}
|
|
36
|
-
//#endregion
|
|
5
|
+
import { applyDelta, blackKeyWidth, clamp, decimalPart, getNoteRangeArray, isWhiteKey, linearScale, mapModifier, noteKey, notePosition, pianoWidth, radian, selectInputEvent, selectModifier, toFixed, toPrecision, xor } from "@tremolo-ui/functions";
|
|
37
6
|
//#region src/components/AnimationCanvas/index.tsx
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
height: height.current,
|
|
55
|
-
count: count + 1,
|
|
56
|
-
deltaTime,
|
|
57
|
-
elapsedTime,
|
|
58
|
-
fps: 1e3 / deltaTime
|
|
59
|
-
});
|
|
60
|
-
}, [draw, animate]);
|
|
7
|
+
/**
|
|
8
|
+
* A simple animatable canvas with requestAnimationFrame()
|
|
9
|
+
*/
|
|
10
|
+
const AnimationCanvas = /* @__PURE__ */ forwardRef(function AnimationCanvas({ draw, init, animate = true, options, reduceFlickering = true, width = 100, height = 100, resizable = false, className, onContextMenu = (event) => event.preventDefault(), ...props }, forwardedRef) {
|
|
11
|
+
const [node, setNode] = useState(null);
|
|
12
|
+
const composedRef = useComposedRefs(forwardedRef, setNode);
|
|
13
|
+
const latest = useRef({
|
|
14
|
+
draw,
|
|
15
|
+
init,
|
|
16
|
+
animate,
|
|
17
|
+
width,
|
|
18
|
+
height,
|
|
19
|
+
reduceFlickering,
|
|
20
|
+
options
|
|
21
|
+
});
|
|
22
|
+
const instanceRef = useRef(null);
|
|
61
23
|
useEffect(() => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
24
|
+
latest.current = {
|
|
25
|
+
draw,
|
|
26
|
+
init,
|
|
27
|
+
animate,
|
|
28
|
+
width,
|
|
29
|
+
height,
|
|
30
|
+
reduceFlickering,
|
|
31
|
+
options
|
|
32
|
+
};
|
|
33
|
+
instanceRef.current?.update({
|
|
34
|
+
animate,
|
|
35
|
+
size: {
|
|
72
36
|
width,
|
|
73
37
|
height
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
38
|
+
},
|
|
39
|
+
reduceFlickering
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (!node) return;
|
|
44
|
+
const current = latest.current;
|
|
45
|
+
const instance = createAnimationCanvas(node, {
|
|
46
|
+
draw: (context, frame) => latest.current.draw(context, frame),
|
|
47
|
+
init: (context, size) => latest.current.init?.(context, size),
|
|
48
|
+
animate: current.animate,
|
|
49
|
+
size: {
|
|
50
|
+
width: current.width,
|
|
51
|
+
height: current.height
|
|
52
|
+
},
|
|
53
|
+
reduceFlickering: current.reduceFlickering,
|
|
54
|
+
resizable,
|
|
55
|
+
contextAttributes: current.options
|
|
56
|
+
});
|
|
57
|
+
instanceRef.current = instance;
|
|
58
|
+
return () => {
|
|
59
|
+
instanceRef.current = null;
|
|
60
|
+
instance.destroy();
|
|
79
61
|
};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const memoContext = memoCanvas?.getContext("2d", options);
|
|
85
|
-
const ro = new ResizeObserver((entries) => {
|
|
86
|
-
for (const entry of entries) {
|
|
87
|
-
const w = entry.contentRect.width;
|
|
88
|
-
const h = entry.contentRect.height;
|
|
89
|
-
const contextMemo = {};
|
|
90
|
-
if (reduceFlickering && memoCanvas && memoContext) {
|
|
91
|
-
for (const prop of drawingState) contextMemo[prop] = context[prop];
|
|
92
|
-
memoCanvas.width = w * dpr;
|
|
93
|
-
memoCanvas.height = h * dpr;
|
|
94
|
-
memoContext.scale(1 / dpr, 1 / dpr);
|
|
95
|
-
if (canvas.width > 0 && canvas.height > 0) memoContext.drawImage(canvas, 0, 0);
|
|
96
|
-
}
|
|
97
|
-
setDprConfig(canvas, context, w, h, dpr);
|
|
98
|
-
widthRef.current = w;
|
|
99
|
-
heightRef.current = h;
|
|
100
|
-
if (reduceFlickering && memoContext && memoCanvas && memoCanvas.width > 0 && memoCanvas.height > 0) {
|
|
101
|
-
for (const prop of drawingState) context[prop] = contextMemo[prop];
|
|
102
|
-
context.drawImage(memoContext.canvas, 0, 0);
|
|
103
|
-
}
|
|
104
|
-
if (!animate) loop(context, widthRef, heightRef, -1);
|
|
105
|
-
}
|
|
106
|
-
});
|
|
107
|
-
ro.observe(parent);
|
|
108
|
-
const width = parent.clientWidth;
|
|
109
|
-
const height = parent.clientHeight;
|
|
110
|
-
firstRendering(width, height);
|
|
111
|
-
return () => {
|
|
112
|
-
if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
|
|
113
|
-
ro.disconnect();
|
|
114
|
-
};
|
|
115
|
-
} else {
|
|
116
|
-
const { width, height } = canvas.getBoundingClientRect();
|
|
117
|
-
firstRendering(width, height);
|
|
118
|
-
return () => {
|
|
119
|
-
if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current);
|
|
120
|
-
};
|
|
121
|
-
}
|
|
122
|
-
}, [
|
|
123
|
-
loop,
|
|
124
|
-
init,
|
|
125
|
-
options,
|
|
126
|
-
reduceFlickering,
|
|
127
|
-
relativeSize,
|
|
128
|
-
animate
|
|
129
|
-
]);
|
|
130
|
-
return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("canvas", {
|
|
131
|
-
className: clsx("tremolo-animation-canvas", className),
|
|
132
|
-
width: relativeSize ? 0 : _width,
|
|
133
|
-
height: relativeSize ? 0 : _height,
|
|
134
|
-
ref: canvasRef,
|
|
62
|
+
}, [node, resizable]);
|
|
63
|
+
return /* @__PURE__ */ jsx("canvas", {
|
|
64
|
+
className,
|
|
65
|
+
ref: composedRef,
|
|
135
66
|
onContextMenu,
|
|
136
67
|
...props
|
|
137
|
-
})
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
//#endregion
|
|
71
|
+
//#region src/hooks/_internal/useCheckSteps.ts
|
|
72
|
+
/** Positions probed across the travel. The ends are left out so that the
|
|
73
|
+
* clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
|
|
74
|
+
const PROBES = [
|
|
75
|
+
.1,
|
|
76
|
+
.2,
|
|
77
|
+
.3,
|
|
78
|
+
.4,
|
|
79
|
+
.5,
|
|
80
|
+
.6,
|
|
81
|
+
.7,
|
|
82
|
+
.8,
|
|
83
|
+
.9
|
|
84
|
+
];
|
|
85
|
+
const NONE = {
|
|
86
|
+
shiftKey: false,
|
|
87
|
+
altKey: false,
|
|
88
|
+
ctrlKey: false,
|
|
89
|
+
metaKey: false
|
|
90
|
+
};
|
|
91
|
+
const MODIFIER_STATE = {
|
|
92
|
+
shift: {
|
|
93
|
+
...NONE,
|
|
94
|
+
shiftKey: true
|
|
95
|
+
},
|
|
96
|
+
alt: {
|
|
97
|
+
...NONE,
|
|
98
|
+
altKey: true
|
|
99
|
+
},
|
|
100
|
+
ctrl: {
|
|
101
|
+
...NONE,
|
|
102
|
+
ctrlKey: true
|
|
103
|
+
},
|
|
104
|
+
meta: {
|
|
105
|
+
...NONE,
|
|
106
|
+
metaKey: true
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
/** Each entry of a modifier-aware input option, as a name and held keys. */
|
|
110
|
+
function entries(options) {
|
|
111
|
+
if (Array.isArray(options)) return [{
|
|
112
|
+
name: "",
|
|
113
|
+
modifiers: NONE
|
|
114
|
+
}];
|
|
115
|
+
return Object.keys(options).map((key) => ({
|
|
116
|
+
name: key === "default" ? "" : key,
|
|
117
|
+
modifiers: MODIFIER_STATE[key] ?? NONE
|
|
118
|
+
}));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Press every entry of `options` at nine points along the travel and report
|
|
122
|
+
* whether anything came of it.
|
|
123
|
+
*
|
|
124
|
+
* Run against `applyDelta` itself rather than against a reading of `step`:
|
|
125
|
+
* the whole point is that the amount, the step and the scale interact, and
|
|
126
|
+
* the pipeline is the only thing that knows how.
|
|
127
|
+
*/
|
|
128
|
+
function probe(options, range, format) {
|
|
129
|
+
const { min, max, scale = linearScale } = range;
|
|
130
|
+
const outcome = {
|
|
131
|
+
moved: false,
|
|
132
|
+
visible: false
|
|
133
|
+
};
|
|
134
|
+
for (const { modifiers } of entries(options)) for (const position of PROBES) {
|
|
135
|
+
const value = scale.denormalize(position, min, max);
|
|
136
|
+
const up = applyDelta(value, 1, options, range, modifiers);
|
|
137
|
+
const down = applyDelta(value, -1, options, range, modifiers);
|
|
138
|
+
if (up !== value || down !== value) outcome.moved = true;
|
|
139
|
+
if (!format) continue;
|
|
140
|
+
const shown = format(value);
|
|
141
|
+
if (format(up) !== shown || format(down) !== shown) outcome.visible = true;
|
|
142
|
+
}
|
|
143
|
+
return outcome;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
147
|
+
* a change the user can see.
|
|
148
|
+
*
|
|
149
|
+
* Two settings that are each fine on their own can cancel out, and nothing
|
|
150
|
+
* fails when they do — the control simply sits there:
|
|
151
|
+
*
|
|
152
|
+
* - **`step` coarser than the amount.** `keyboard={['raw', 0.1]}` with
|
|
153
|
+
* `step={1}` rounds every press straight back to where it started
|
|
154
|
+
* - **the display coarser than the amount.** A `format` showing two decimals
|
|
155
|
+
* of a kHz value cannot show a press worth 1 Hz
|
|
156
|
+
*
|
|
157
|
+
* The second is only reported when the press is invisible at *every* point
|
|
158
|
+
* along the travel. A display that rounds is a deliberate choice and is
|
|
159
|
+
* normally right — it is being too coarse everywhere that makes it a mistake.
|
|
160
|
+
*
|
|
161
|
+
* @internal
|
|
162
|
+
*/
|
|
163
|
+
function useCheckSteps({ component, axis, range, keyboard, wheel, format }) {
|
|
164
|
+
const { min, max, step, scale } = range ?? {};
|
|
165
|
+
const where = axis ? `${component} (${axis})` : component;
|
|
166
|
+
useEffect(() => {
|
|
167
|
+
try {
|
|
168
|
+
if (process.env.NODE_ENV === "production") return;
|
|
169
|
+
if (!range || min === void 0 || max === void 0 || !(min < max)) return;
|
|
170
|
+
for (const [name, options] of [["keyboard", keyboard], ["wheel", wheel]]) {
|
|
171
|
+
if (!options) continue;
|
|
172
|
+
const { moved, visible } = probe(options, range, format);
|
|
173
|
+
if (!moved) console.warn(`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` + (step !== void 0 ? ` Each press is smaller than \`step\` (${step}), so it rounds` : " Each press rounds") + " straight back to where it started.");
|
|
174
|
+
else if (format && !visible) console.warn(`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\` shows the same text before and after, everywhere in the range. The display is too coarse for it to be seen.`);
|
|
175
|
+
}
|
|
176
|
+
} catch {}
|
|
177
|
+
}, [
|
|
178
|
+
where,
|
|
179
|
+
min,
|
|
180
|
+
max,
|
|
181
|
+
step,
|
|
182
|
+
scale,
|
|
183
|
+
keyboard,
|
|
184
|
+
wheel
|
|
185
|
+
]);
|
|
141
186
|
}
|
|
142
187
|
//#endregion
|
|
143
|
-
//#region src/hooks/useCallbackRef.ts
|
|
188
|
+
//#region src/hooks/_internal/useCallbackRef.ts
|
|
144
189
|
/**
|
|
145
190
|
* Internal
|
|
146
191
|
* @private
|
|
@@ -155,36 +200,77 @@ function useCallbackRef(callback, deps = []) {
|
|
|
155
200
|
return useCallback((...args) => callbackRef.current?.(...args), deps);
|
|
156
201
|
}
|
|
157
202
|
//#endregion
|
|
158
|
-
//#region src/hooks/
|
|
203
|
+
//#region src/hooks/useDragValue.ts
|
|
159
204
|
/**
|
|
160
|
-
*
|
|
205
|
+
* Drive a value with a pointer drag.
|
|
161
206
|
*
|
|
162
|
-
* @returns a ref callback
|
|
207
|
+
* @returns a ref callback for the element that starts the drag, and whether a drag is in progress
|
|
163
208
|
*/
|
|
164
|
-
function
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
const
|
|
209
|
+
function useDragValue(options) {
|
|
210
|
+
const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor, pointerLock } = options;
|
|
211
|
+
if (!baseElementRef && !options.getValue) throw new Error("useDragValue: give either baseElementRef or getValue, so that the drag has something to move");
|
|
212
|
+
const [dragging, setDragging] = useState(false);
|
|
213
|
+
const valueGetter = useCallbackRef(options.getValue);
|
|
214
|
+
const changeHandler = useCallbackRef(options.onChange);
|
|
215
|
+
const dragStartHandler = useCallbackRef(options.onDragStart);
|
|
216
|
+
const dragEndHandler = useCallbackRef(options.onDragEnd);
|
|
217
|
+
const latest = useRef(options);
|
|
218
|
+
const instanceRef = useRef(null);
|
|
168
219
|
const [node, setNode] = useState(null);
|
|
220
|
+
const [pixelRangeX, pixelRangeY] = Array.isArray(pixelRange) ? pixelRange : [pixelRange, pixelRange];
|
|
169
221
|
useEffect(() => {
|
|
170
222
|
if (!node) return;
|
|
171
|
-
const instance =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
223
|
+
const instance = createDragValue(node, {
|
|
224
|
+
axis: latest.current.axis,
|
|
225
|
+
mapping: baseElementRef ? elementMapping(() => baseElementRef.current, { sensitivity: (state) => latest.current.sensitivity?.(state) ?? 1 }) : relativeMapping({
|
|
226
|
+
pixelRange: [pixelRangeX ?? 100, pixelRangeY ?? 100],
|
|
227
|
+
sensitivity: (state) => latest.current.sensitivity?.(state) ?? 1
|
|
228
|
+
}),
|
|
229
|
+
getValue: () => valueGetter() ?? [0, 0],
|
|
230
|
+
updateOnPointerDown: latest.current.updateOnPointerDown,
|
|
231
|
+
threshold: latest.current.threshold,
|
|
232
|
+
cursor: latest.current.cursor,
|
|
233
|
+
pointerLock: latest.current.pointerLock,
|
|
234
|
+
shouldStart: (event) => latest.current.shouldStart?.(event) ?? true,
|
|
235
|
+
onChange: (value, state) => changeHandler(value, state),
|
|
236
|
+
onDragStart: (value, state) => {
|
|
237
|
+
setDragging(true);
|
|
238
|
+
dragStartHandler(value, state);
|
|
239
|
+
},
|
|
240
|
+
onDragEnd: (value, state) => {
|
|
241
|
+
setDragging(false);
|
|
242
|
+
dragEndHandler(value, state);
|
|
243
|
+
}
|
|
177
244
|
});
|
|
178
|
-
|
|
245
|
+
instanceRef.current = instance;
|
|
246
|
+
return () => {
|
|
247
|
+
instanceRef.current = null;
|
|
248
|
+
instance.destroy();
|
|
249
|
+
};
|
|
179
250
|
}, [
|
|
180
251
|
node,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
252
|
+
baseElementRef,
|
|
253
|
+
pixelRangeX,
|
|
254
|
+
pixelRangeY,
|
|
255
|
+
valueGetter,
|
|
256
|
+
changeHandler,
|
|
184
257
|
dragStartHandler,
|
|
185
258
|
dragEndHandler
|
|
186
259
|
]);
|
|
187
|
-
|
|
260
|
+
useEffect(() => {
|
|
261
|
+
latest.current = options;
|
|
262
|
+
instanceRef.current?.update({
|
|
263
|
+
axis,
|
|
264
|
+
updateOnPointerDown,
|
|
265
|
+
threshold,
|
|
266
|
+
cursor,
|
|
267
|
+
pointerLock
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
return {
|
|
271
|
+
refCallback: setNode,
|
|
272
|
+
dragging
|
|
273
|
+
};
|
|
188
274
|
}
|
|
189
275
|
//#endregion
|
|
190
276
|
//#region src/hooks/useWheel.ts
|
|
@@ -193,88 +279,169 @@ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
|
193
279
|
*
|
|
194
280
|
* The listener is not passive, so the handler may call `preventDefault()`.
|
|
195
281
|
*
|
|
196
|
-
* @returns a ref callback to attach to the element
|
|
282
|
+
* @returns a ref callback to attach to the element, unused when `target` is given
|
|
197
283
|
*/
|
|
198
|
-
function useWheel(onWheel) {
|
|
284
|
+
function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
199
285
|
const wheelHandler = useCallbackRef(onWheel);
|
|
200
286
|
const [node, setNode] = useState(null);
|
|
201
287
|
useEffect(() => {
|
|
202
|
-
|
|
203
|
-
|
|
288
|
+
const element = target ? target.current : node;
|
|
289
|
+
if (!element) return;
|
|
290
|
+
const instance = createWheel(element, (event) => wheelHandler(event), { requireFocus });
|
|
204
291
|
return () => instance.destroy();
|
|
205
|
-
}, [
|
|
292
|
+
}, [
|
|
293
|
+
node,
|
|
294
|
+
target,
|
|
295
|
+
wheelHandler,
|
|
296
|
+
requireFocus
|
|
297
|
+
]);
|
|
206
298
|
return setNode;
|
|
207
299
|
}
|
|
208
300
|
//#endregion
|
|
209
|
-
//#region src/
|
|
210
|
-
|
|
211
|
-
|
|
301
|
+
//#region src/input-event.ts
|
|
302
|
+
/**
|
|
303
|
+
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
304
|
+
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
305
|
+
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
306
|
+
* `useCheckSteps` warns about.
|
|
307
|
+
*
|
|
308
|
+
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
309
|
+
* amount move at all — see `applyDelta` in `@tremolo-ui/functions`.
|
|
310
|
+
*/
|
|
311
|
+
const DEFAULT_KEYBOARD_OPTIONS = {
|
|
312
|
+
default: ["raw", 1],
|
|
313
|
+
shift: ["raw", .1]
|
|
314
|
+
};
|
|
315
|
+
/**
|
|
316
|
+
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
317
|
+
*
|
|
318
|
+
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
319
|
+
* and fills `deltaX`, so no modifier is bound here.
|
|
320
|
+
*/
|
|
321
|
+
const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
|
|
322
|
+
/**
|
|
323
|
+
* The drag sensitivity used by value controls by default. Shift makes the
|
|
324
|
+
* same movement cover a tenth of the range, matching the arrow keys.
|
|
325
|
+
*/
|
|
326
|
+
const DEFAULT_DRAG_SENSITIVITY = {
|
|
327
|
+
default: 1,
|
|
328
|
+
shift: .1
|
|
329
|
+
};
|
|
330
|
+
//#endregion
|
|
331
|
+
//#region src/components/_util/css-length.ts
|
|
332
|
+
/**
|
|
333
|
+
* A number as a CSS length, for a custom property.
|
|
334
|
+
*
|
|
335
|
+
* React appends `px` to a bare number for the properties it knows take a
|
|
336
|
+
* length, and a custom property is never one of them: `--size: 50` would come
|
|
337
|
+
* out as the invalid `50`. Anything already a string is passed through, so
|
|
338
|
+
* `'3rem'` and `'100%'` still work.
|
|
339
|
+
*/
|
|
340
|
+
function cssLength(value) {
|
|
341
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/components/_util/Placement.tsx
|
|
345
|
+
/**
|
|
346
|
+
* The subcomponent that established the current placement, if any.
|
|
347
|
+
*
|
|
348
|
+
* @internal
|
|
349
|
+
*/
|
|
350
|
+
const PlacementContext = /* @__PURE__ */ createContext(null);
|
|
351
|
+
/**
|
|
352
|
+
* Marks its children as being inside `name`, for {@link useCheckPlacement}.
|
|
353
|
+
*
|
|
354
|
+
* @internal
|
|
355
|
+
*/
|
|
356
|
+
function Placement({ name, children }) {
|
|
357
|
+
return /* @__PURE__ */ jsx(PlacementContext.Provider, {
|
|
358
|
+
value: name,
|
|
359
|
+
children
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Warn, in development only, when `child` is not rendered inside `parent`.
|
|
364
|
+
*
|
|
365
|
+
* A compound component renders its children exactly as they are composed, so a
|
|
366
|
+
* subcomponent put at the wrong level still renders — it only loses whatever
|
|
367
|
+
* its layout depended on, and does so silently. `Slider.Thumb` outside
|
|
368
|
+
* `Slider.Track` keeps its `position: absolute` but resolves it against the
|
|
369
|
+
* page rather than the track, and neither the build nor the tests notice.
|
|
370
|
+
*
|
|
371
|
+
* @internal
|
|
372
|
+
*/
|
|
373
|
+
function useCheckPlacement(child, parent) {
|
|
374
|
+
const found = useContext(PlacementContext);
|
|
375
|
+
useEffect(() => {
|
|
376
|
+
try {
|
|
377
|
+
if (process.env.NODE_ENV !== "production" && found !== parent) console.warn(`[tremolo-ui] ${child} has to be rendered inside ${parent}` + (found === null ? "." : `, but it is inside ${found}.`) + " It renders either way, so nothing fails — its position just comes out wrong.");
|
|
378
|
+
} catch {}
|
|
379
|
+
}, [
|
|
380
|
+
found,
|
|
381
|
+
child,
|
|
382
|
+
parent
|
|
383
|
+
]);
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* 角度から円弧上の点を求める。
|
|
387
|
+
*
|
|
388
|
+
* 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
|
|
389
|
+
* それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
|
|
390
|
+
*/
|
|
391
|
+
function pointOnArc(angle, radius) {
|
|
392
|
+
return {
|
|
393
|
+
x: 50 + radius * Math.cos(radian(angle - 90)),
|
|
394
|
+
y: 50 + radius * Math.sin(radian(angle - 90))
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
/** 線の太さが viewBox からはみ出さないようにした半径 */
|
|
398
|
+
function arcRadius(strokeWidth) {
|
|
399
|
+
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
400
|
+
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
212
401
|
}
|
|
213
|
-
|
|
214
|
-
|
|
402
|
+
/** Build an SVG path for an arc, splitting full turns into drawable segments. */
|
|
403
|
+
function arcPath(startAngle, endAngle, radius) {
|
|
404
|
+
const start = pointOnArc(startAngle, radius);
|
|
405
|
+
const sweep = endAngle - startAngle;
|
|
406
|
+
const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180));
|
|
407
|
+
const segmentSweep = sweep / segmentCount;
|
|
408
|
+
let path = `M ${start.x} ${start.y}`;
|
|
409
|
+
for (let i = 1; i <= segmentCount; i += 1) {
|
|
410
|
+
const end = pointOnArc(startAngle + segmentSweep * i, radius);
|
|
411
|
+
path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`;
|
|
412
|
+
}
|
|
413
|
+
return path;
|
|
215
414
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
415
|
+
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
416
|
+
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
417
|
+
const p = scale.normalize(value, min, max);
|
|
418
|
+
const s = scale.normalize(startValue, min, max);
|
|
219
419
|
const r1 = -angleRange / 2;
|
|
220
|
-
const r2 = r1 + Math.min(p, s) * angleRange;
|
|
221
|
-
const r3 = r1 + Math.max(p, s) * angleRange;
|
|
222
|
-
const r4 = angleRange / 2;
|
|
223
420
|
return {
|
|
224
421
|
p,
|
|
225
422
|
r1,
|
|
226
|
-
r2,
|
|
227
|
-
r3,
|
|
228
|
-
r4
|
|
229
|
-
x1: 50 + 50 * Math.cos(radian(r1 - 90)),
|
|
230
|
-
y1: 50 + 50 * Math.sin(radian(r1 - 90)),
|
|
231
|
-
x2: 50 + 50 * Math.cos(radian(r2 - 90)),
|
|
232
|
-
y2: 50 + 50 * Math.sin(radian(r2 - 90)),
|
|
233
|
-
x3: 50 + 50 * Math.cos(radian(r3 - 90)),
|
|
234
|
-
y3: 50 + 50 * Math.sin(radian(r3 - 90)),
|
|
235
|
-
x4: 50 + 50 * Math.cos(radian(r4 - 90)),
|
|
236
|
-
y4: 50 + 50 * Math.sin(radian(r4 - 90))
|
|
423
|
+
r2: r1 + Math.min(p, s) * angleRange,
|
|
424
|
+
r3: r1 + Math.max(p, s) * angleRange,
|
|
425
|
+
r4: angleRange / 2
|
|
237
426
|
};
|
|
238
427
|
}
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
...initProps,
|
|
242
|
-
...calcDrawingState(initProps)
|
|
243
|
-
}));
|
|
244
|
-
};
|
|
245
|
-
const KnobContext = createContext(null);
|
|
246
|
-
function KnobProvider({ children, ...props }) {
|
|
247
|
-
const storeRef = useRef(null);
|
|
248
|
-
if (!storeRef.current) storeRef.current = createKnobStore(props);
|
|
249
|
-
useEffect(() => {
|
|
250
|
-
if (storeRef.current) storeRef.current.setState({
|
|
251
|
-
...props,
|
|
252
|
-
...calcDrawingState(props)
|
|
253
|
-
});
|
|
254
|
-
else storeRef.current = createKnobStore(props);
|
|
255
|
-
}, [props]);
|
|
256
|
-
return /* @__PURE__ */ jsx(KnobContext.Provider, {
|
|
257
|
-
value: storeRef.current,
|
|
258
|
-
children
|
|
259
|
-
});
|
|
260
|
-
}
|
|
428
|
+
const KnobContext = /* @__PURE__ */ createContext(null);
|
|
429
|
+
const KnobProvider = KnobContext.Provider;
|
|
261
430
|
function useKnobContext(selector) {
|
|
262
|
-
const
|
|
263
|
-
if (!
|
|
264
|
-
return
|
|
431
|
+
const context = useContext(KnobContext);
|
|
432
|
+
if (!context) throw new Error("Missing KnobContext.Provider in the tree");
|
|
433
|
+
return selector ? selector(context) : context;
|
|
265
434
|
}
|
|
266
435
|
//#endregion
|
|
267
436
|
//#region src/components/Knob/ActiveLine.tsx
|
|
268
437
|
function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
438
|
+
useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
|
|
269
439
|
const r2 = useKnobContext((s) => s.r2);
|
|
270
440
|
const r3 = useKnobContext((s) => s.r3);
|
|
271
|
-
const
|
|
272
|
-
const y2 = useKnobContext((s) => s.y2);
|
|
273
|
-
const x3 = useKnobContext((s) => s.x3);
|
|
274
|
-
const y3 = useKnobContext((s) => s.y3);
|
|
441
|
+
const radius = arcRadius(strokeWidth);
|
|
275
442
|
return /* @__PURE__ */ jsx("path", {
|
|
276
|
-
className
|
|
277
|
-
d:
|
|
443
|
+
className,
|
|
444
|
+
d: arcPath(r2, r3, radius),
|
|
278
445
|
fill: "none",
|
|
279
446
|
stroke,
|
|
280
447
|
strokeWidth,
|
|
@@ -284,6 +451,7 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
|
|
|
284
451
|
//#endregion
|
|
285
452
|
//#region src/components/Knob/InactiveLine.tsx
|
|
286
453
|
function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
454
|
+
useCheckPlacement("Knob.InactiveLine", "Knob.SVGRoot");
|
|
287
455
|
const min = useKnobContext((s) => s.min);
|
|
288
456
|
const max = useKnobContext((s) => s.max);
|
|
289
457
|
const startValue = useKnobContext((s) => s.startValue);
|
|
@@ -291,24 +459,17 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
291
459
|
const r2 = useKnobContext((s) => s.r2);
|
|
292
460
|
const r3 = useKnobContext((s) => s.r3);
|
|
293
461
|
const r4 = useKnobContext((s) => s.r4);
|
|
294
|
-
const
|
|
295
|
-
const y1 = useKnobContext((s) => s.y1);
|
|
296
|
-
const x2 = useKnobContext((s) => s.x2);
|
|
297
|
-
const y2 = useKnobContext((s) => s.y2);
|
|
298
|
-
const x3 = useKnobContext((s) => s.x3);
|
|
299
|
-
const y3 = useKnobContext((s) => s.y3);
|
|
300
|
-
const x4 = useKnobContext((s) => s.x4);
|
|
301
|
-
const y4 = useKnobContext((s) => s.y4);
|
|
462
|
+
const radius = arcRadius(strokeWidth);
|
|
302
463
|
return /* @__PURE__ */ jsxs(Fragment, { children: [startValue > min && /* @__PURE__ */ jsx("path", {
|
|
303
|
-
className
|
|
304
|
-
d:
|
|
464
|
+
className,
|
|
465
|
+
d: arcPath(r1, r2, radius),
|
|
305
466
|
fill: "none",
|
|
306
467
|
stroke,
|
|
307
468
|
strokeWidth,
|
|
308
469
|
...props
|
|
309
470
|
}), startValue < max && /* @__PURE__ */ jsx("path", {
|
|
310
|
-
className
|
|
311
|
-
d:
|
|
471
|
+
className,
|
|
472
|
+
d: arcPath(r3, r4, radius),
|
|
312
473
|
fill: "none",
|
|
313
474
|
stroke,
|
|
314
475
|
strokeWidth,
|
|
@@ -316,13 +477,30 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
316
477
|
})] });
|
|
317
478
|
}
|
|
318
479
|
//#endregion
|
|
480
|
+
//#region src/components/Knob/SVGRoot.tsx
|
|
481
|
+
function SVGRoot({ children, style, ...props }) {
|
|
482
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
483
|
+
viewBox: `0 0 100 100`,
|
|
484
|
+
style: {
|
|
485
|
+
display: "block",
|
|
486
|
+
...style
|
|
487
|
+
},
|
|
488
|
+
...props,
|
|
489
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
490
|
+
name: "Knob.SVGRoot",
|
|
491
|
+
children
|
|
492
|
+
})
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
//#endregion
|
|
319
496
|
//#region src/components/Knob/Thumb.tsx
|
|
320
497
|
function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
|
|
498
|
+
useCheckPlacement("Knob.Thumb", "Knob.SVGRoot");
|
|
321
499
|
const angleRange = useKnobContext((s) => s.angleRange);
|
|
322
500
|
const p = useKnobContext((s) => s.p);
|
|
323
501
|
const r1 = useKnobContext((s) => s.r1);
|
|
324
502
|
return /* @__PURE__ */ jsxs("svg", {
|
|
325
|
-
className
|
|
503
|
+
className,
|
|
326
504
|
...props,
|
|
327
505
|
children: [/* @__PURE__ */ jsx("circle", {
|
|
328
506
|
cx: "50%",
|
|
@@ -330,7 +508,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
330
508
|
r: `${thumbSize / 2}%`,
|
|
331
509
|
fill: thumb
|
|
332
510
|
}), /* @__PURE__ */ jsx("line", {
|
|
333
|
-
className:
|
|
511
|
+
className: classes?.thumbLine,
|
|
334
512
|
x1: "50%",
|
|
335
513
|
y1: `${(100 - clamp(thumbSize, 0, 100)) / 2}%`,
|
|
336
514
|
x2: "50%",
|
|
@@ -345,66 +523,43 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
345
523
|
});
|
|
346
524
|
}
|
|
347
525
|
//#endregion
|
|
348
|
-
//#region src/components/Knob/SVGRoot.tsx
|
|
349
|
-
function SVGRoot({ children, style, ...props }) {
|
|
350
|
-
return /* @__PURE__ */ jsx("svg", {
|
|
351
|
-
viewBox: `0 0 100 100`,
|
|
352
|
-
style: {
|
|
353
|
-
display: "block",
|
|
354
|
-
overflow: "visible",
|
|
355
|
-
...style
|
|
356
|
-
},
|
|
357
|
-
...props,
|
|
358
|
-
children: children || /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
359
|
-
/* @__PURE__ */ jsx(InactiveLine, {}),
|
|
360
|
-
/* @__PURE__ */ jsx(ActiveLine, {}),
|
|
361
|
-
/* @__PURE__ */ jsx(Thumb$2, {})
|
|
362
|
-
] })
|
|
363
|
-
});
|
|
364
|
-
}
|
|
365
|
-
//#endregion
|
|
366
526
|
//#region src/components/Knob/index.tsx
|
|
367
|
-
const defaultExternalStyles$
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
527
|
+
const defaultExternalStyles$2 = { cursor: "grabbing" };
|
|
528
|
+
/**
|
|
529
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
530
|
+
* the control does not change its value.
|
|
531
|
+
*/
|
|
532
|
+
const WHEEL_OPTIONS$2 = { requireFocus: true };
|
|
371
533
|
/**
|
|
372
534
|
* Interactive rotary knob component implemented in SVG.
|
|
373
535
|
*/
|
|
374
536
|
const Knob = {
|
|
375
|
-
Root: forwardRef(({ value, min, max, step = 1,
|
|
376
|
-
const [dragging, setDragging] = useState(false);
|
|
377
|
-
const valueRef = useRef(0);
|
|
537
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
|
|
378
538
|
const elmRef = useRef(null);
|
|
379
539
|
const externalStyles = {
|
|
380
|
-
...defaultExternalStyles$
|
|
540
|
+
...defaultExternalStyles$2,
|
|
381
541
|
..._externalStyles
|
|
382
542
|
};
|
|
383
|
-
const
|
|
384
|
-
|
|
385
|
-
onChange(clamp(stepValue(rawValue(valueRef.current - y / 100, min, max, skew), step), min, max));
|
|
386
|
-
}, [
|
|
387
|
-
max,
|
|
543
|
+
const inactive = disabled || readonly;
|
|
544
|
+
const range = useMemo(() => ({
|
|
388
545
|
min,
|
|
389
|
-
onChange,
|
|
390
|
-
readonly,
|
|
391
|
-
skew,
|
|
392
|
-
step
|
|
393
|
-
]);
|
|
394
|
-
const updateValueByEvent = useCallback((eventType, x) => {
|
|
395
|
-
let newValue;
|
|
396
|
-
if (eventType == "normalized") newValue = rawValue(normalizeValue(value, min, max, skew) + x, min, max, skew);
|
|
397
|
-
else newValue = value + x;
|
|
398
|
-
return clamp(stepValue(newValue, step), min, max);
|
|
399
|
-
}, [
|
|
400
546
|
max,
|
|
547
|
+
step,
|
|
548
|
+
scale
|
|
549
|
+
}), [
|
|
401
550
|
min,
|
|
402
|
-
|
|
551
|
+
max,
|
|
403
552
|
step,
|
|
404
|
-
|
|
553
|
+
scale
|
|
405
554
|
]);
|
|
555
|
+
useCheckSteps({
|
|
556
|
+
component: "Knob",
|
|
557
|
+
range,
|
|
558
|
+
keyboard,
|
|
559
|
+
wheel
|
|
560
|
+
});
|
|
406
561
|
const handleKeyDown = useCallback((event) => {
|
|
407
|
-
if (!keyboard || !onChange ||
|
|
562
|
+
if (!keyboard || !onChange || inactive) return;
|
|
408
563
|
const key = event.key;
|
|
409
564
|
if ([
|
|
410
565
|
"ArrowRight",
|
|
@@ -413,76 +568,93 @@ const Knob = {
|
|
|
413
568
|
"ArrowDown"
|
|
414
569
|
].includes(key)) {
|
|
415
570
|
event.preventDefault();
|
|
416
|
-
|
|
417
|
-
onChange(updateValueByEvent(keyboard[0], x));
|
|
571
|
+
onChange(applyDelta(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
|
|
418
572
|
}
|
|
419
573
|
}, [
|
|
420
574
|
keyboard,
|
|
421
575
|
onChange,
|
|
422
|
-
|
|
423
|
-
|
|
576
|
+
inactive,
|
|
577
|
+
value,
|
|
578
|
+
range
|
|
424
579
|
]);
|
|
425
|
-
const dragRefCallback =
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
580
|
+
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
581
|
+
axis: useMemo(() => [range, {
|
|
582
|
+
...range,
|
|
583
|
+
reverse: true
|
|
584
|
+
}], [range]),
|
|
585
|
+
getValue: () => [value, value],
|
|
586
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
587
|
+
threshold: 1,
|
|
588
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
589
|
+
pointerLock: inactive ? false : pointerLock,
|
|
590
|
+
shouldStart: () => !inactive,
|
|
591
|
+
onChange: (v) => {
|
|
592
|
+
if (inactive) return;
|
|
593
|
+
onChange?.(v[1]);
|
|
436
594
|
}
|
|
437
595
|
});
|
|
438
596
|
const wheelRefCallback = useWheel((event) => {
|
|
439
|
-
if (!wheel ||
|
|
597
|
+
if (!wheel || inactive) return;
|
|
440
598
|
event.preventDefault();
|
|
441
|
-
if (!onChange || event.deltaY
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
599
|
+
if (!onChange || event.deltaY === 0) return;
|
|
600
|
+
onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range, event));
|
|
601
|
+
}, WHEEL_OPTIONS$2);
|
|
602
|
+
const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
|
|
603
|
+
const context = useMemo(() => {
|
|
604
|
+
const config = {
|
|
605
|
+
value,
|
|
606
|
+
min,
|
|
607
|
+
max,
|
|
608
|
+
step,
|
|
609
|
+
scale,
|
|
610
|
+
startValue,
|
|
611
|
+
angleRange
|
|
612
|
+
};
|
|
613
|
+
return {
|
|
614
|
+
...config,
|
|
615
|
+
...calcAngles(config)
|
|
616
|
+
};
|
|
617
|
+
}, [
|
|
618
|
+
value,
|
|
619
|
+
min,
|
|
620
|
+
max,
|
|
621
|
+
step,
|
|
622
|
+
scale,
|
|
623
|
+
startValue,
|
|
624
|
+
angleRange
|
|
625
|
+
]);
|
|
445
626
|
useImperativeHandle(forwardedRef, () => {
|
|
446
627
|
return {
|
|
447
628
|
focus() {
|
|
448
|
-
elmRef.current?.focus();
|
|
629
|
+
if (!disabled) elmRef.current?.focus();
|
|
449
630
|
},
|
|
450
631
|
blur() {
|
|
451
632
|
elmRef.current?.blur();
|
|
452
633
|
}
|
|
453
634
|
};
|
|
454
|
-
}, []);
|
|
635
|
+
}, [disabled]);
|
|
455
636
|
return /* @__PURE__ */ jsx(KnobProvider, {
|
|
456
|
-
value,
|
|
457
|
-
min,
|
|
458
|
-
max,
|
|
459
|
-
step,
|
|
460
|
-
skew,
|
|
461
|
-
startValue,
|
|
462
|
-
angleRange,
|
|
637
|
+
value: context,
|
|
463
638
|
children: /* @__PURE__ */ jsx("div", {
|
|
464
|
-
ref:
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
dragRefCallback(elm);
|
|
468
|
-
},
|
|
469
|
-
className: clsx("tremolo-knob", className),
|
|
470
|
-
tabIndex: 0,
|
|
639
|
+
ref: rootRefCallback,
|
|
640
|
+
className,
|
|
641
|
+
tabIndex: disabled ? -1 : 0,
|
|
471
642
|
role: "slider",
|
|
472
643
|
"aria-valuenow": value,
|
|
473
644
|
"aria-valuemin": min,
|
|
474
645
|
"aria-valuemax": max,
|
|
475
646
|
"aria-disabled": disabled,
|
|
476
647
|
"aria-readonly": readonly,
|
|
477
|
-
"data-
|
|
648
|
+
"data-disabled": disabled ? "" : void 0,
|
|
649
|
+
"data-readonly": readonly ? "" : void 0,
|
|
650
|
+
"data-dragging": dragging ? "" : void 0,
|
|
478
651
|
style: {
|
|
479
|
-
|
|
480
|
-
height: size,
|
|
652
|
+
"--knob-size": cssLength(size),
|
|
481
653
|
...style
|
|
482
654
|
},
|
|
483
655
|
onPointerDown,
|
|
484
656
|
onDoubleClick: (event) => {
|
|
485
|
-
if (enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
657
|
+
if (!inactive && enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
486
658
|
onDoubleClick?.(event);
|
|
487
659
|
},
|
|
488
660
|
onKeyDown: (event) => {
|
|
@@ -490,7 +662,7 @@ const Knob = {
|
|
|
490
662
|
onKeyDown?.(event);
|
|
491
663
|
},
|
|
492
664
|
...props,
|
|
493
|
-
children
|
|
665
|
+
children
|
|
494
666
|
})
|
|
495
667
|
});
|
|
496
668
|
}),
|
|
@@ -500,142 +672,43 @@ const Knob = {
|
|
|
500
672
|
Thumb: Thumb$2
|
|
501
673
|
};
|
|
502
674
|
//#endregion
|
|
503
|
-
//#region src/components/NumberInput/type.ts
|
|
504
|
-
function selectUnit(units, value) {
|
|
505
|
-
let i = 0;
|
|
506
|
-
for (; i < units.length; i++) if (Math.abs(units[i][1]) > Math.abs(value)) break;
|
|
507
|
-
return units[Math.max(0, i - 1)];
|
|
508
|
-
}
|
|
509
|
-
function parseValue(inputString, units, digit) {
|
|
510
|
-
const str = inputString.trim();
|
|
511
|
-
if (!units || typeof units == "string") {
|
|
512
|
-
const m = str.match(/-?\d+(\.\d+)?/);
|
|
513
|
-
let v = Number(m?.[0] ?? "0");
|
|
514
|
-
v = isNaN(v) ? 0 : v;
|
|
515
|
-
return {
|
|
516
|
-
rawValue: v,
|
|
517
|
-
formatValue: (digit != void 0 ? v.toFixed(digit) : v) + (units ?? ""),
|
|
518
|
-
unit: units ?? ""
|
|
519
|
-
};
|
|
520
|
-
}
|
|
521
|
-
const unitList = units.map(([u, _s]) => u);
|
|
522
|
-
const scaleList = units.map(([_u, s]) => s);
|
|
523
|
-
let rawValue = 0;
|
|
524
|
-
let unit = unitList[0];
|
|
525
|
-
let formatValue = (digit != void 0 ? rawValue.toFixed(digit) : rawValue) + unit;
|
|
526
|
-
const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/);
|
|
527
|
-
if (m) {
|
|
528
|
-
rawValue = Number(m[1]) || 0;
|
|
529
|
-
const uIndex = unitList.indexOf(m[3]);
|
|
530
|
-
rawValue *= uIndex != -1 ? scaleList[uIndex] : 1;
|
|
531
|
-
const [u, scale] = selectUnit(units, rawValue);
|
|
532
|
-
const v = rawValue / scale;
|
|
533
|
-
formatValue = (digit != void 0 ? v.toFixed(digit) : v) + u;
|
|
534
|
-
unit = u;
|
|
535
|
-
}
|
|
536
|
-
return {
|
|
537
|
-
rawValue,
|
|
538
|
-
formatValue,
|
|
539
|
-
unit
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
//#endregion
|
|
543
675
|
//#region src/components/NumberInput/context.tsx
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
}
|
|
547
|
-
const createNumberInputStore = (initProps) => {
|
|
548
|
-
const DEFAULT_PROPS = {
|
|
549
|
-
value: "",
|
|
550
|
-
valueAsNumber: 0,
|
|
551
|
-
min: 0,
|
|
552
|
-
max: 0,
|
|
553
|
-
step: 1,
|
|
554
|
-
readonly: false,
|
|
555
|
-
keepWithinRange: true,
|
|
556
|
-
onChange: (_v, _t) => {}
|
|
557
|
-
};
|
|
558
|
-
return createStore()((set) => ({
|
|
559
|
-
...DEFAULT_PROPS,
|
|
560
|
-
...initProps,
|
|
561
|
-
value: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).formatValue,
|
|
562
|
-
valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
|
|
563
|
-
increment: () => set((state) => {
|
|
564
|
-
if (state.readonly) return {};
|
|
565
|
-
let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
|
|
566
|
-
if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
|
|
567
|
-
const v = parseValue(String(next), state.units, state.digit).formatValue;
|
|
568
|
-
state?.onChange?.(next, v);
|
|
569
|
-
return {
|
|
570
|
-
value: v,
|
|
571
|
-
valueAsNumber: next
|
|
572
|
-
};
|
|
573
|
-
}),
|
|
574
|
-
decrement: () => set((state) => {
|
|
575
|
-
if (state.readonly) return {};
|
|
576
|
-
let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
|
|
577
|
-
if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
|
|
578
|
-
const v = parseValue(String(next), state.units, state.digit).formatValue;
|
|
579
|
-
state?.onChange?.(next, v);
|
|
580
|
-
return {
|
|
581
|
-
value: v,
|
|
582
|
-
valueAsNumber: next
|
|
583
|
-
};
|
|
584
|
-
}),
|
|
585
|
-
change: (value) => set((state) => {
|
|
586
|
-
if (state.readonly) return {};
|
|
587
|
-
const v = value ?? "";
|
|
588
|
-
return {
|
|
589
|
-
value: v,
|
|
590
|
-
valueAsNumber: parseValue(v, state.units, state.digit).rawValue
|
|
591
|
-
};
|
|
592
|
-
})
|
|
593
|
-
}));
|
|
594
|
-
};
|
|
595
|
-
const NumberInputContext = createContext(null);
|
|
596
|
-
function NumberInputProvider({ children, ...props }) {
|
|
597
|
-
const storeRef = useRef(null);
|
|
598
|
-
if (!storeRef.current) storeRef.current = createNumberInputStore(props);
|
|
599
|
-
useEffect(() => {
|
|
600
|
-
if (storeRef.current) {
|
|
601
|
-
const parsed = parseValue(String(props?.value || ""), props?.units, props?.digit);
|
|
602
|
-
storeRef.current.setState({
|
|
603
|
-
...props,
|
|
604
|
-
value: parsed.formatValue,
|
|
605
|
-
valueAsNumber: parsed.rawValue
|
|
606
|
-
});
|
|
607
|
-
} else storeRef.current = createNumberInputStore(props);
|
|
608
|
-
}, [props]);
|
|
609
|
-
return /* @__PURE__ */ jsx(NumberInputContext.Provider, {
|
|
610
|
-
value: storeRef.current,
|
|
611
|
-
children
|
|
612
|
-
});
|
|
613
|
-
}
|
|
676
|
+
const NumberInputContext = /* @__PURE__ */ createContext(null);
|
|
677
|
+
const NumberInputProvider = NumberInputContext.Provider;
|
|
614
678
|
function useNumberInputContext(selector) {
|
|
615
|
-
const
|
|
616
|
-
if (!
|
|
617
|
-
return
|
|
679
|
+
const context = useContext(NumberInputContext);
|
|
680
|
+
if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
|
|
681
|
+
return selector ? selector(context) : context;
|
|
682
|
+
}
|
|
683
|
+
const StepperContext = /* @__PURE__ */ createContext(null);
|
|
684
|
+
const StepperProvider = StepperContext.Provider;
|
|
685
|
+
function useStepperContext() {
|
|
686
|
+
return useContext(StepperContext);
|
|
618
687
|
}
|
|
619
688
|
//#endregion
|
|
620
689
|
//#region src/hooks/useEventListener.ts
|
|
621
690
|
function useEventListener(target, event, handler, options) {
|
|
622
691
|
const listener = useCallbackRef(handler);
|
|
692
|
+
const cleanupRef = useRef(() => {});
|
|
623
693
|
useEffect(() => {
|
|
624
694
|
const node = typeof target === "function" ? target() : target ?? document;
|
|
625
695
|
if (!node) return;
|
|
626
696
|
node.addEventListener(event, listener, options);
|
|
627
|
-
|
|
697
|
+
const cleanup = () => {
|
|
628
698
|
node.removeEventListener(event, listener, options);
|
|
629
699
|
};
|
|
700
|
+
cleanupRef.current = cleanup;
|
|
701
|
+
return () => {
|
|
702
|
+
cleanup();
|
|
703
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
704
|
+
};
|
|
630
705
|
}, [
|
|
631
706
|
event,
|
|
632
707
|
target,
|
|
633
708
|
options,
|
|
634
709
|
listener
|
|
635
710
|
]);
|
|
636
|
-
return () =>
|
|
637
|
-
(typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
|
|
638
|
-
};
|
|
711
|
+
return useCallback(() => cleanupRef.current(), []);
|
|
639
712
|
}
|
|
640
713
|
//#endregion
|
|
641
714
|
//#region src/hooks/useInterval.ts
|
|
@@ -659,783 +732,851 @@ function useInterval(callback, delay) {
|
|
|
659
732
|
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
660
733
|
const [pressed, setPressed] = useState(false);
|
|
661
734
|
const [delay, setDelay] = useState(initialDelay);
|
|
735
|
+
const activePointerId = useRef(null);
|
|
736
|
+
const isPressed = useRef(false);
|
|
662
737
|
useInterval(() => {
|
|
663
738
|
callback();
|
|
664
739
|
setDelay(interval);
|
|
665
740
|
}, pressed ? delay : null);
|
|
666
|
-
|
|
741
|
+
const stop = useCallback((pointerId) => {
|
|
742
|
+
if (!isPressed.current) return;
|
|
743
|
+
if (pointerId !== void 0 && activePointerId.current !== null && activePointerId.current !== pointerId) return;
|
|
744
|
+
isPressed.current = false;
|
|
745
|
+
activePointerId.current = null;
|
|
667
746
|
setPressed(false);
|
|
668
747
|
setDelay(initialDelay);
|
|
748
|
+
}, [initialDelay]);
|
|
749
|
+
useEventListener(globalThis.window, "pointerup", (event) => {
|
|
750
|
+
stop(event.pointerId);
|
|
751
|
+
});
|
|
752
|
+
useEventListener(globalThis.window, "pointercancel", (event) => {
|
|
753
|
+
stop(event.pointerId);
|
|
754
|
+
});
|
|
755
|
+
useEventListener(globalThis.window, "blur", () => {
|
|
756
|
+
stop();
|
|
669
757
|
});
|
|
670
|
-
return useCallback(() => {
|
|
758
|
+
return useCallback((event) => {
|
|
759
|
+
if (isPressed.current || event && event.button !== 0) return;
|
|
760
|
+
isPressed.current = true;
|
|
761
|
+
activePointerId.current = event?.pointerId ?? null;
|
|
671
762
|
callback();
|
|
672
763
|
setPressed(true);
|
|
673
764
|
}, [callback]);
|
|
674
765
|
}
|
|
675
766
|
//#endregion
|
|
676
|
-
//#region src/components/NumberInput/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
const
|
|
767
|
+
//#region src/components/NumberInput/StepperButton.tsx
|
|
768
|
+
/**
|
|
769
|
+
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
770
|
+
* in which way they move the value and which arrow they draw.
|
|
771
|
+
*/
|
|
772
|
+
function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
|
|
773
|
+
const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
774
|
+
const stepper = useStepperContext();
|
|
775
|
+
const press = useLongPress(() => {
|
|
776
|
+
if (stepper?.draggingRef.current) return;
|
|
777
|
+
nudge(direction, ["raw", step]);
|
|
778
|
+
});
|
|
683
779
|
return /* @__PURE__ */ jsx("div", {
|
|
684
|
-
className
|
|
780
|
+
className,
|
|
685
781
|
role: "button",
|
|
686
782
|
tabIndex: -1,
|
|
687
|
-
"aria-
|
|
688
|
-
"aria-
|
|
689
|
-
|
|
783
|
+
"aria-label": direction > 0 ? "Increment" : "Decrement",
|
|
784
|
+
"aria-disabled": disabled || (direction > 0 ? atMax : atMin),
|
|
785
|
+
"data-disabled": disabled || (direction > 0 ? atMax : atMin) ? "" : void 0,
|
|
786
|
+
"data-readonly": readonly ? "" : void 0,
|
|
787
|
+
style,
|
|
788
|
+
onPointerDown: (event) => {
|
|
789
|
+
if (!disabled && !readonly) press(event);
|
|
790
|
+
onPointerDown?.(event);
|
|
791
|
+
},
|
|
690
792
|
...props,
|
|
691
|
-
children: children
|
|
692
|
-
width: size,
|
|
693
|
-
height: size,
|
|
694
|
-
viewBox: "0 0 24 24",
|
|
695
|
-
fill: "none",
|
|
696
|
-
stroke: "currentColor",
|
|
697
|
-
strokeWidth: "2",
|
|
698
|
-
strokeLinecap: "round",
|
|
699
|
-
strokeLinejoin: "round",
|
|
700
|
-
children: /* @__PURE__ */ jsx("polyline", { points: "6 9 12 15 18 9" })
|
|
701
|
-
})
|
|
793
|
+
children: children ?? icon
|
|
702
794
|
});
|
|
703
795
|
}
|
|
704
|
-
|
|
796
|
+
function StepperArrow({ up }) {
|
|
797
|
+
return /* @__PURE__ */ jsx("svg", {
|
|
798
|
+
style: {
|
|
799
|
+
width: "var(--stepper-icon-size)",
|
|
800
|
+
height: "var(--stepper-icon-size)"
|
|
801
|
+
},
|
|
802
|
+
viewBox: "0 0 24 24",
|
|
803
|
+
fill: "none",
|
|
804
|
+
stroke: "currentColor",
|
|
805
|
+
strokeWidth: "2",
|
|
806
|
+
strokeLinecap: "round",
|
|
807
|
+
strokeLinejoin: "round",
|
|
808
|
+
children: /* @__PURE__ */ jsx("polyline", { points: up ? "18 15 12 9 6 15" : "6 9 12 15 18 9" })
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
//#endregion
|
|
812
|
+
//#region src/components/NumberInput/DecrementStepper.tsx
|
|
813
|
+
/** Lowers the value by one `step`, repeating while held. */
|
|
814
|
+
function DecrementStepper(props) {
|
|
815
|
+
return /* @__PURE__ */ jsx(StepperButton, {
|
|
816
|
+
direction: -1,
|
|
817
|
+
variant: "decrement",
|
|
818
|
+
icon: /* @__PURE__ */ jsx(StepperArrow, { up: false }),
|
|
819
|
+
...props
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
705
823
|
//#region src/components/NumberInput/IncrementStepper.tsx
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
className: clsx("tremolo-number-input-increment-stepper", className),
|
|
714
|
-
role: "button",
|
|
715
|
-
tabIndex: -1,
|
|
716
|
-
"aria-disabled": keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER),
|
|
717
|
-
"aria-readonly": readonly,
|
|
718
|
-
onPointerDown: press,
|
|
719
|
-
...props,
|
|
720
|
-
children: children || /* @__PURE__ */ jsx("svg", {
|
|
721
|
-
width: size,
|
|
722
|
-
height: size,
|
|
723
|
-
viewBox: "0 0 24 24",
|
|
724
|
-
fill: "none",
|
|
725
|
-
stroke: "currentColor",
|
|
726
|
-
strokeWidth: "2",
|
|
727
|
-
strokeLinecap: "round",
|
|
728
|
-
strokeLinejoin: "round",
|
|
729
|
-
children: /* @__PURE__ */ jsx("polyline", { points: "18 15 12 9 6 15" })
|
|
730
|
-
})
|
|
824
|
+
/** Raises the value by one `step`, repeating while held. */
|
|
825
|
+
function IncrementStepper(props) {
|
|
826
|
+
return /* @__PURE__ */ jsx(StepperButton, {
|
|
827
|
+
direction: 1,
|
|
828
|
+
variant: "increment",
|
|
829
|
+
icon: /* @__PURE__ */ jsx(StepperArrow, { up: true }),
|
|
830
|
+
...props
|
|
731
831
|
});
|
|
732
832
|
}
|
|
733
833
|
//#endregion
|
|
734
|
-
//#region src/components/NumberInput/
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
step,
|
|
770
|
-
valueAsNumber
|
|
771
|
-
]);
|
|
772
|
-
const handleKeyDown = useCallback((event) => {
|
|
773
|
-
if (!keyboard || !onChange || readonly) return;
|
|
774
|
-
const key = event.key;
|
|
775
|
-
if (!["ArrowUp", "ArrowDown"].includes(key)) return;
|
|
776
|
-
event.preventDefault();
|
|
777
|
-
const x = key == "ArrowUp" ? keyboard[1] : -keyboard[1];
|
|
778
|
-
const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)));
|
|
779
|
-
onChange?.(parsed.rawValue, parsed.formatValue);
|
|
834
|
+
//#region src/components/NumberInput/InputField.tsx
|
|
835
|
+
/** The leading number of the displayed text, whatever the format put around it. */
|
|
836
|
+
const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
|
|
837
|
+
const numberEnd = (text) => text.match(NUMBER_PREFIX)?.[0].length ?? text.length;
|
|
838
|
+
/**
|
|
839
|
+
* The index the caret is measured against: the decimal point, or where one
|
|
840
|
+
* would go if the number has none.
|
|
841
|
+
*
|
|
842
|
+
* Measuring from an end instead would slide the caret across a digit whenever
|
|
843
|
+
* the number changed length — `9.9` to `10.0` gains a character in front, `10`
|
|
844
|
+
* to `9` loses one — which is exactly what stepping does.
|
|
845
|
+
*/
|
|
846
|
+
const decimalAnchor = (text) => {
|
|
847
|
+
const dot = text.indexOf(".");
|
|
848
|
+
return dot === -1 ? numberEnd(text) : dot;
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
852
|
+
*
|
|
853
|
+
* While the user types, their own text stands rather than `format(value)`, so
|
|
854
|
+
* that a half-finished entry is not rewritten under the caret.
|
|
855
|
+
*
|
|
856
|
+
* How it behaves on focus, Enter and the arrow keys is set on `Root`
|
|
857
|
+
* (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
|
|
858
|
+
*/
|
|
859
|
+
const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
860
|
+
const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
861
|
+
const [focused, setFocused] = useState(false);
|
|
862
|
+
const shown = unformatOnFocus && focused && !editing ? String(value) : text;
|
|
863
|
+
useEffect(() => {
|
|
864
|
+
if (!focused || selectOnFocus === "none") return;
|
|
865
|
+
const input = inputRef.current;
|
|
866
|
+
if (!input) return;
|
|
867
|
+
if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
|
|
868
|
+
else input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
|
|
780
869
|
}, [
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
updateValueByEvent
|
|
870
|
+
focused,
|
|
871
|
+
selectOnFocus,
|
|
872
|
+
inputRef
|
|
785
873
|
]);
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
874
|
+
/**
|
|
875
|
+
* Where to put the caret once the stepped value has been rendered, and the
|
|
876
|
+
* text it was measured against.
|
|
877
|
+
*/
|
|
878
|
+
const caretAfterStep = useRef(null);
|
|
879
|
+
useLayoutEffect(() => {
|
|
880
|
+
const pending = caretAfterStep.current;
|
|
881
|
+
if (!pending) return;
|
|
882
|
+
caretAfterStep.current = null;
|
|
883
|
+
const input = inputRef.current;
|
|
884
|
+
if (!input || input.value === pending.from) return;
|
|
885
|
+
const place = decimalAnchor(input.value) + pending.offset;
|
|
886
|
+
const caret = Math.max(0, Math.min(place, numberEnd(input.value)));
|
|
887
|
+
input.setSelectionRange(caret, caret);
|
|
793
888
|
});
|
|
794
|
-
|
|
795
|
-
return {
|
|
796
|
-
focus() {
|
|
797
|
-
elmRef.current?.focus();
|
|
798
|
-
},
|
|
799
|
-
blur() {
|
|
800
|
-
elmRef.current?.blur();
|
|
801
|
-
}
|
|
802
|
-
};
|
|
803
|
-
}, []);
|
|
889
|
+
const composedRef = useComposedRefs(forwardedRef, inputRef);
|
|
804
890
|
return /* @__PURE__ */ jsx("input", {
|
|
805
|
-
ref:
|
|
806
|
-
|
|
807
|
-
wheelRefCallback(input);
|
|
808
|
-
},
|
|
809
|
-
className: clsx("tremolo-number-input", className),
|
|
810
|
-
value,
|
|
811
|
-
min,
|
|
812
|
-
max,
|
|
813
|
-
step,
|
|
891
|
+
ref: composedRef,
|
|
892
|
+
className,
|
|
814
893
|
type: "text",
|
|
894
|
+
inputMode: "decimal",
|
|
895
|
+
role: "spinbutton",
|
|
896
|
+
value: shown,
|
|
897
|
+
disabled,
|
|
898
|
+
readOnly: readonly,
|
|
815
899
|
"aria-disabled": disabled,
|
|
816
900
|
"aria-readonly": readonly,
|
|
817
|
-
|
|
818
|
-
"data-
|
|
901
|
+
"data-disabled": disabled ? "" : void 0,
|
|
902
|
+
"data-readonly": readonly ? "" : void 0,
|
|
903
|
+
"aria-valuenow": value,
|
|
904
|
+
"aria-valuemin": min,
|
|
905
|
+
"aria-valuemax": max,
|
|
906
|
+
"aria-valuetext": text,
|
|
907
|
+
step,
|
|
908
|
+
"data-out-of-range": outOfRange ? "" : void 0,
|
|
909
|
+
style,
|
|
819
910
|
onChange: (event) => {
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
const cursorPos = event.target.selectionStart;
|
|
823
|
-
change(v);
|
|
824
|
-
const valueAsNumber = parseValue(v, units, digit).rawValue;
|
|
825
|
-
onChange?.(valueAsNumber, v);
|
|
826
|
-
if (cursorPos) {
|
|
827
|
-
event.target.selectionStart = cursorPos;
|
|
828
|
-
event.target.selectionEnd = cursorPos;
|
|
829
|
-
}
|
|
911
|
+
caretAfterStep.current = null;
|
|
912
|
+
setDraft(event.currentTarget.value);
|
|
830
913
|
},
|
|
831
914
|
onFocus: (event) => {
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
const parsed = parseValue(v, units, digit);
|
|
835
|
-
if (selectWithFocus == "all") event.currentTarget.setSelectionRange(0, v.length);
|
|
836
|
-
else if (selectWithFocus == "number") event.currentTarget.setSelectionRange(0, parsed.formatValue.length - parsed.unit.length);
|
|
837
|
-
onFocus?.(parsed.rawValue, parsed.formatValue, event);
|
|
915
|
+
setFocused(true);
|
|
916
|
+
onFocus?.(event);
|
|
838
917
|
},
|
|
839
918
|
onBlur: (event) => {
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
const parsed = parseValue(String(v), units, digit);
|
|
844
|
-
change(parsed.formatValue);
|
|
845
|
-
if (clampValueOnBlur) onChange?.(parsed.rawValue, parsed.formatValue);
|
|
846
|
-
onBlur?.(parsed.rawValue, parsed.formatValue, event);
|
|
919
|
+
setFocused(false);
|
|
920
|
+
commitDraft();
|
|
921
|
+
onBlur?.(event);
|
|
847
922
|
},
|
|
848
923
|
onKeyDown: (event) => {
|
|
849
|
-
|
|
850
|
-
|
|
924
|
+
const key = event.key;
|
|
925
|
+
if (key === "Enter") {
|
|
926
|
+
commitDraft();
|
|
927
|
+
if (blurOnEnter) event.currentTarget.blur();
|
|
928
|
+
} else if (keyboard && !disabled && !readonly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
|
|
929
|
+
event.preventDefault();
|
|
930
|
+
const input = event.currentTarget;
|
|
931
|
+
if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
|
|
932
|
+
offset: input.selectionStart - decimalAnchor(input.value),
|
|
933
|
+
from: input.value
|
|
934
|
+
};
|
|
935
|
+
nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
|
|
936
|
+
}
|
|
851
937
|
onKeyDown?.(event);
|
|
852
938
|
},
|
|
853
939
|
...props
|
|
854
940
|
});
|
|
855
941
|
});
|
|
856
942
|
//#endregion
|
|
857
|
-
//#region src/
|
|
858
|
-
function Stepper({ dynamic = true, children, className, ...props }) {
|
|
859
|
-
return /* @__PURE__ */ jsx("div", {
|
|
860
|
-
className: clsx("tremolo-number-input-stepper", className),
|
|
861
|
-
"data-dynamic": dynamic,
|
|
862
|
-
...props,
|
|
863
|
-
children
|
|
864
|
-
});
|
|
865
|
-
}
|
|
943
|
+
//#region src/hooks/useDrag.ts
|
|
866
944
|
/**
|
|
867
|
-
*
|
|
945
|
+
* Track a pointer drag on an element.
|
|
946
|
+
*
|
|
947
|
+
* @returns a ref callback to attach to the element being dragged
|
|
868
948
|
*/
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
className: clsx("tremolo-number-input-wrapper", wrapperClassName),
|
|
884
|
-
tabIndex: 0,
|
|
885
|
-
style: { ...colors },
|
|
886
|
-
"data-stepper": !!children,
|
|
887
|
-
"data-variant": variant,
|
|
888
|
-
children: [/* @__PURE__ */ jsx(InternalInput, {
|
|
889
|
-
ref: forwardedRef,
|
|
890
|
-
readonly,
|
|
891
|
-
disabled,
|
|
892
|
-
selectWithFocus,
|
|
893
|
-
blurOnEnter,
|
|
894
|
-
clampValueOnBlur,
|
|
895
|
-
wheel,
|
|
896
|
-
keyboard,
|
|
897
|
-
className,
|
|
898
|
-
onChange,
|
|
899
|
-
onFocus,
|
|
900
|
-
onBlur,
|
|
901
|
-
...props
|
|
902
|
-
}), children]
|
|
903
|
-
})
|
|
949
|
+
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
950
|
+
const dragHandler = useCallbackRef(onDrag);
|
|
951
|
+
const dragStartHandler = useCallbackRef(onDragStart);
|
|
952
|
+
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
953
|
+
const [node, setNode] = useState(null);
|
|
954
|
+
useEffect(() => {
|
|
955
|
+
if (!node) return;
|
|
956
|
+
const instance = createDrag(node, {
|
|
957
|
+
threshold,
|
|
958
|
+
cursor,
|
|
959
|
+
pointerLock,
|
|
960
|
+
onDragStart: (state) => dragStartHandler(state),
|
|
961
|
+
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
962
|
+
onDragEnd: (state) => dragEndHandler(state)
|
|
904
963
|
});
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
if (!node) {
|
|
917
|
-
cleanup?.();
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
node.addEventListener(event, handler, options);
|
|
921
|
-
cleanup = () => {
|
|
922
|
-
node.removeEventListener(event, handler, options);
|
|
923
|
-
};
|
|
924
|
-
};
|
|
925
|
-
}, deps);
|
|
964
|
+
return () => instance.destroy();
|
|
965
|
+
}, [
|
|
966
|
+
node,
|
|
967
|
+
threshold,
|
|
968
|
+
cursor,
|
|
969
|
+
pointerLock,
|
|
970
|
+
dragHandler,
|
|
971
|
+
dragStartHandler,
|
|
972
|
+
dragEndHandler
|
|
973
|
+
]);
|
|
974
|
+
return setNode;
|
|
926
975
|
}
|
|
927
976
|
//#endregion
|
|
928
|
-
//#region src/
|
|
977
|
+
//#region src/components/NumberInput/Stepper.tsx
|
|
929
978
|
/**
|
|
930
|
-
*
|
|
931
|
-
*
|
|
932
|
-
*
|
|
979
|
+
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
980
|
+
* up and down moves the value one `step` every `drag` pixels.
|
|
981
|
+
*
|
|
982
|
+
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
983
|
+
* off text selection on whatever element it is attached to.
|
|
933
984
|
*/
|
|
934
|
-
function
|
|
935
|
-
const
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
noteRange: {
|
|
973
|
-
first: 0,
|
|
974
|
-
last: 127
|
|
985
|
+
const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style, children, ...props }, forwardedRef) {
|
|
986
|
+
const { value, step, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
|
|
987
|
+
const inactive = disabled || readonly;
|
|
988
|
+
/**
|
|
989
|
+
* The sensitivity as an amount per `drag` pixels, so that the drag goes
|
|
990
|
+
* through the same `applyDelta` the wheel and the arrow keys do. Carrying
|
|
991
|
+
* the modifier map over rather than resolving it here is what keeps `step`
|
|
992
|
+
* out of the pipeline for a modifier entry — naming one is a request to move
|
|
993
|
+
* off the grid.
|
|
994
|
+
*/
|
|
995
|
+
const dragOptions = useMemo(() => mapModifier(dragSensitivity, (factor) => ["raw", step * factor]), [dragSensitivity, step]);
|
|
996
|
+
/**
|
|
997
|
+
* Set once the drag has actually moved the value, so that the press-and-hold
|
|
998
|
+
* repeat of a stepper stands down and leaves the value to the drag. A ref,
|
|
999
|
+
* because it is read from inside the repeat rather than rendered.
|
|
1000
|
+
*/
|
|
1001
|
+
const draggingRef = useRef(false);
|
|
1002
|
+
/**
|
|
1003
|
+
* Where the drag started, taken on the first move rather than on pointer
|
|
1004
|
+
* down: the steppers act on pointer down, so by then the value may already
|
|
1005
|
+
* have been nudged once, and the drag should carry on from there.
|
|
1006
|
+
*/
|
|
1007
|
+
const originRef = useRef(null);
|
|
1008
|
+
/**
|
|
1009
|
+
* Which modifier the drag is currently counting at, and where the previous
|
|
1010
|
+
* event was — a key produces no pointer event of its own, so a change is
|
|
1011
|
+
* only seen on the next move and has to be dated back to the one before it.
|
|
1012
|
+
*/
|
|
1013
|
+
const factorRef = useRef(1);
|
|
1014
|
+
const previousYRef = useRef(0);
|
|
1015
|
+
const dragRefCallback = useDrag({
|
|
1016
|
+
threshold: 1,
|
|
1017
|
+
cursor: inactive ? void 0 : "ns-resize",
|
|
1018
|
+
pointerLock: inactive ? false : pointerLock,
|
|
1019
|
+
onDragStart: (state) => {
|
|
1020
|
+
originRef.current = null;
|
|
1021
|
+
draggingRef.current = false;
|
|
1022
|
+
factorRef.current = selectModifier(dragSensitivity, state.event).value;
|
|
975
1023
|
},
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1024
|
+
onDrag: (_x, y, _dx, _dy, state) => {
|
|
1025
|
+
if (inactive || drag === null) return;
|
|
1026
|
+
if (!originRef.current) {
|
|
1027
|
+
originRef.current = {
|
|
1028
|
+
y,
|
|
1029
|
+
value
|
|
1030
|
+
};
|
|
1031
|
+
previousYRef.current = y;
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
const factor = selectModifier(dragSensitivity, state.event).value;
|
|
1035
|
+
if (factor !== factorRef.current) {
|
|
1036
|
+
originRef.current = {
|
|
1037
|
+
y: previousYRef.current,
|
|
1038
|
+
value
|
|
1039
|
+
};
|
|
1040
|
+
factorRef.current = factor;
|
|
1041
|
+
}
|
|
1042
|
+
previousYRef.current = y;
|
|
1043
|
+
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
1044
|
+
if (steps === 0) return;
|
|
1045
|
+
draggingRef.current = true;
|
|
1046
|
+
changeValue(applyDelta(originRef.current.value, steps, dragOptions, rawRange, state.event));
|
|
1047
|
+
},
|
|
1048
|
+
onDragEnd: () => {
|
|
1049
|
+
draggingRef.current = false;
|
|
1050
|
+
}
|
|
1000
1051
|
});
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
return useStore(store, selector);
|
|
1006
|
-
}
|
|
1007
|
-
//#endregion
|
|
1008
|
-
//#region src/components/Piano/KeyLabel.tsx
|
|
1009
|
-
function KeyLabel({ label, className, wrapperClassName, wrapperStyle, __note, __label, ...props }) {
|
|
1010
|
-
const index = getNoteRangeArray(usePianoContext((s) => s.noteRange)).indexOf(__note);
|
|
1011
|
-
const content = label ? label(__note, index) : __label && __label(__note, index);
|
|
1012
|
-
return (content != void 0 || content != null) && /* @__PURE__ */ jsx("div", {
|
|
1013
|
-
className: clsx("tremolo-piano-key-label-wrapper", wrapperClassName),
|
|
1014
|
-
style: wrapperStyle,
|
|
1052
|
+
const composedRef = useComposedRefs(forwardedRef, drag !== null && !inactive ? dragRefCallback : void 0);
|
|
1053
|
+
const context = useMemo(() => ({ draggingRef }), []);
|
|
1054
|
+
return /* @__PURE__ */ jsx(StepperProvider, {
|
|
1055
|
+
value: context,
|
|
1015
1056
|
children: /* @__PURE__ */ jsx("div", {
|
|
1016
|
-
|
|
1057
|
+
ref: composedRef,
|
|
1058
|
+
className,
|
|
1059
|
+
style,
|
|
1017
1060
|
...props,
|
|
1018
|
-
children
|
|
1019
|
-
})
|
|
1020
|
-
});
|
|
1021
|
-
}
|
|
1022
|
-
const KeyImpl = forwardRef(({ keyType, noteNumber, width, height, bg, color, activeBg, activeColor, className, style, children, __width, ...props }, ref) => {
|
|
1023
|
-
const [played, setPlayed] = useState(false);
|
|
1024
|
-
const midiMax = usePianoContext((s) => s.midiMax);
|
|
1025
|
-
const fill = usePianoContext((s) => s.fill);
|
|
1026
|
-
const onPlayNote = usePianoContext((s) => s.onPlayNote);
|
|
1027
|
-
const onStopNote = usePianoContext((s) => s.onStopNote);
|
|
1028
|
-
const label = usePianoContext((s) => s.label);
|
|
1029
|
-
const position = usePianoContext((s) => s.notePosition)(noteNumber);
|
|
1030
|
-
const disabled = noteNumber > midiMax;
|
|
1031
|
-
const colors = {
|
|
1032
|
-
"--bg": bg,
|
|
1033
|
-
"--color": color,
|
|
1034
|
-
"--active-bg": activeBg,
|
|
1035
|
-
"--active-color": activeColor
|
|
1036
|
-
};
|
|
1037
|
-
useImperativeHandle(ref, () => {
|
|
1038
|
-
return {
|
|
1039
|
-
play(velocity) {
|
|
1040
|
-
if (disabled) return;
|
|
1041
|
-
setPlayed(true);
|
|
1042
|
-
if (onPlayNote) onPlayNote(noteNumber, velocity);
|
|
1043
|
-
},
|
|
1044
|
-
stop() {
|
|
1045
|
-
setPlayed(false);
|
|
1046
|
-
if (onStopNote) onStopNote(noteNumber);
|
|
1047
|
-
},
|
|
1048
|
-
played() {
|
|
1049
|
-
return played;
|
|
1050
|
-
}
|
|
1051
|
-
};
|
|
1052
|
-
}, [
|
|
1053
|
-
disabled,
|
|
1054
|
-
noteNumber,
|
|
1055
|
-
onPlayNote,
|
|
1056
|
-
onStopNote,
|
|
1057
|
-
played
|
|
1058
|
-
]);
|
|
1059
|
-
let keyLabelProps = {};
|
|
1060
|
-
if (children != void 0) React.Children.forEach(children, (child) => {
|
|
1061
|
-
if (React.isValidElement(child)) if (child.type == KeyLabel) keyLabelProps = child.props;
|
|
1062
|
-
else throw new Error("only <KeyLabel>");
|
|
1063
|
-
else throw new Error("children is an invalid element.");
|
|
1064
|
-
});
|
|
1065
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1066
|
-
className: clsx(`tremolo-piano-${keyType}-key`, className),
|
|
1067
|
-
"aria-disabled": disabled,
|
|
1068
|
-
"data-active": played,
|
|
1069
|
-
style: {
|
|
1070
|
-
...colors,
|
|
1071
|
-
left: position,
|
|
1072
|
-
width: fill ? __width : width,
|
|
1073
|
-
height,
|
|
1074
|
-
...style
|
|
1075
|
-
},
|
|
1076
|
-
...props,
|
|
1077
|
-
children: /* @__PURE__ */ jsx(KeyLabel, {
|
|
1078
|
-
label,
|
|
1079
|
-
__note: noteNumber,
|
|
1080
|
-
...keyLabelProps
|
|
1061
|
+
children
|
|
1081
1062
|
})
|
|
1082
1063
|
});
|
|
1083
1064
|
});
|
|
1084
|
-
const WhiteKey = forwardRef(({ width = 40, height = "100%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
|
|
1085
|
-
return /* @__PURE__ */ jsx(KeyImpl, {
|
|
1086
|
-
ref,
|
|
1087
|
-
keyType: "white",
|
|
1088
|
-
width,
|
|
1089
|
-
height,
|
|
1090
|
-
onContextMenu,
|
|
1091
|
-
...props
|
|
1092
|
-
});
|
|
1093
|
-
});
|
|
1094
|
-
const BlackKey = forwardRef(({ width = 26, height = "60%", onContextMenu = (e) => e.preventDefault(), ...props }, ref) => {
|
|
1095
|
-
return /* @__PURE__ */ jsx(KeyImpl, {
|
|
1096
|
-
ref,
|
|
1097
|
-
keyType: "black",
|
|
1098
|
-
width,
|
|
1099
|
-
height,
|
|
1100
|
-
onContextMenu,
|
|
1101
|
-
...props
|
|
1102
|
-
});
|
|
1103
|
-
});
|
|
1104
1065
|
//#endregion
|
|
1105
|
-
//#region src/components/
|
|
1106
|
-
const SHORTCUTS = { HOME_ROW: { keys: [
|
|
1107
|
-
"a",
|
|
1108
|
-
"w",
|
|
1109
|
-
"s",
|
|
1110
|
-
"e",
|
|
1111
|
-
"d",
|
|
1112
|
-
"f",
|
|
1113
|
-
"t",
|
|
1114
|
-
"g",
|
|
1115
|
-
"y",
|
|
1116
|
-
"h",
|
|
1117
|
-
"u",
|
|
1118
|
-
"j",
|
|
1119
|
-
"k",
|
|
1120
|
-
"o",
|
|
1121
|
-
"l",
|
|
1122
|
-
"p",
|
|
1123
|
-
";"
|
|
1124
|
-
] } };
|
|
1125
|
-
//#endregion
|
|
1126
|
-
//#region src/components/Piano/index.tsx
|
|
1066
|
+
//#region src/components/NumberInput/index.tsx
|
|
1127
1067
|
/**
|
|
1128
|
-
*
|
|
1068
|
+
* A number at the start of the text, and nothing read after it. A half-typed
|
|
1069
|
+
* entry still yields the number in front of it, but text with no number is
|
|
1070
|
+
* `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
|
|
1129
1071
|
*/
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const
|
|
1072
|
+
const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;
|
|
1073
|
+
const defaultFormat = (value) => String(value);
|
|
1074
|
+
const defaultParse = (text) => {
|
|
1075
|
+
const match = text.match(LEADING_NUMBER);
|
|
1076
|
+
return match ? Number(match[1]) : NaN;
|
|
1077
|
+
};
|
|
1134
1078
|
/**
|
|
1135
|
-
*
|
|
1079
|
+
* Input with some useful functions for entering numerical values.
|
|
1136
1080
|
*/
|
|
1137
|
-
const
|
|
1138
|
-
Root: forwardRef(({
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
const
|
|
1145
|
-
const
|
|
1146
|
-
const
|
|
1147
|
-
const
|
|
1148
|
-
const
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
const h = isWhiteKey(note) ? containerHeight : containerHeight * .6;
|
|
1198
|
-
if (pos <= x && x < pos + w && 0 <= y && y < h) return note;
|
|
1199
|
-
}
|
|
1200
|
-
return -1;
|
|
1081
|
+
const NumberInput = {
|
|
1082
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, selectOnFocus = "none", unformatOnFocus = false, keepCaretOnStep = false, blurOnEnter = true, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
|
|
1083
|
+
const inputRef = useRef(null);
|
|
1084
|
+
/**
|
|
1085
|
+
* The text being typed. The only state here: it is not a copy of `value`,
|
|
1086
|
+
* but the half-finished entry that has no value to be derived from yet.
|
|
1087
|
+
*/
|
|
1088
|
+
const [draft, setDraft] = useState(null);
|
|
1089
|
+
const inactive = disabled || readonly;
|
|
1090
|
+
const format = formatProp ?? defaultFormat;
|
|
1091
|
+
const parse = parseProp ?? defaultParse;
|
|
1092
|
+
const range = useMemo(() => ({
|
|
1093
|
+
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
1094
|
+
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
1095
|
+
step,
|
|
1096
|
+
scale
|
|
1097
|
+
}), [
|
|
1098
|
+
clampValue,
|
|
1099
|
+
min,
|
|
1100
|
+
max,
|
|
1101
|
+
step,
|
|
1102
|
+
scale
|
|
1103
|
+
]);
|
|
1104
|
+
const rawRange = useMemo(() => ({
|
|
1105
|
+
min: (clampValue ? min : void 0) ?? -Number.MAX_VALUE,
|
|
1106
|
+
max: (clampValue ? max : void 0) ?? Number.MAX_VALUE,
|
|
1107
|
+
step,
|
|
1108
|
+
scale
|
|
1109
|
+
}), [
|
|
1110
|
+
clampValue,
|
|
1111
|
+
min,
|
|
1112
|
+
max,
|
|
1113
|
+
step,
|
|
1114
|
+
scale
|
|
1115
|
+
]);
|
|
1116
|
+
useCheckSteps({
|
|
1117
|
+
component: "NumberInput",
|
|
1118
|
+
range: useMemo(() => min !== void 0 && max !== void 0 && min < max ? {
|
|
1119
|
+
min,
|
|
1120
|
+
max,
|
|
1121
|
+
step,
|
|
1122
|
+
scale
|
|
1123
|
+
} : null, [
|
|
1124
|
+
min,
|
|
1125
|
+
max,
|
|
1126
|
+
step,
|
|
1127
|
+
scale
|
|
1128
|
+
]),
|
|
1129
|
+
keyboard,
|
|
1130
|
+
wheel,
|
|
1131
|
+
format
|
|
1132
|
+
});
|
|
1133
|
+
const text = draft ?? format(value);
|
|
1134
|
+
const editing = draft !== null;
|
|
1135
|
+
const outOfRange = !editing && (min !== void 0 && value < min || max !== void 0 && value > max);
|
|
1136
|
+
const handleDraft = useCallback((next) => {
|
|
1137
|
+
if (inactive) return;
|
|
1138
|
+
setDraft(next);
|
|
1139
|
+
const parsed = parse(next);
|
|
1140
|
+
if (Number.isFinite(parsed)) onChange?.(parsed);
|
|
1201
1141
|
}, [
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
whiteNotes
|
|
1142
|
+
inactive,
|
|
1143
|
+
parse,
|
|
1144
|
+
onChange
|
|
1206
1145
|
]);
|
|
1207
|
-
const
|
|
1208
|
-
if (
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1146
|
+
const changeValue = useCallback((next) => {
|
|
1147
|
+
if (inactive) return;
|
|
1148
|
+
setDraft(null);
|
|
1149
|
+
if (next !== value) onChange?.(next);
|
|
1150
|
+
}, [
|
|
1151
|
+
inactive,
|
|
1152
|
+
value,
|
|
1153
|
+
onChange
|
|
1154
|
+
]);
|
|
1155
|
+
const commitDraft = useCallback(() => {
|
|
1156
|
+
if (draft === null || inactive) return;
|
|
1157
|
+
const parsed = parse(draft);
|
|
1158
|
+
if (!Number.isFinite(parsed)) {
|
|
1159
|
+
setDraft(null);
|
|
1160
|
+
return;
|
|
1216
1161
|
}
|
|
1162
|
+
let committed = parsed;
|
|
1163
|
+
if (clampValue && min !== void 0) committed = Math.max(committed, min);
|
|
1164
|
+
if (clampValue && max !== void 0) committed = Math.min(committed, max);
|
|
1165
|
+
changeValue(committed);
|
|
1217
1166
|
}, [
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1167
|
+
draft,
|
|
1168
|
+
inactive,
|
|
1169
|
+
parse,
|
|
1170
|
+
clampValue,
|
|
1171
|
+
min,
|
|
1172
|
+
max,
|
|
1173
|
+
changeValue
|
|
1221
1174
|
]);
|
|
1222
|
-
const
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
}, [keyRefs]);
|
|
1226
|
-
useEffect(() => {
|
|
1227
|
-
if (fill && pianoRef.current) {
|
|
1228
|
-
const parent = pianoRef.current.parentElement;
|
|
1229
|
-
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1230
|
-
const resizeObserver = new ResizeObserver(() => {
|
|
1231
|
-
const w = pianoRef.current.clientWidth;
|
|
1232
|
-
setWhiteNoteWidth(w / whiteNoteCount - padding);
|
|
1233
|
-
});
|
|
1234
|
-
resizeObserver.observe(parent);
|
|
1235
|
-
return () => {
|
|
1236
|
-
resizeObserver.unobserve(parent);
|
|
1237
|
-
};
|
|
1238
|
-
} else setWhiteNoteWidth(_whiteNoteWidth);
|
|
1175
|
+
const nudge = useCallback((direction, option, modifiers) => {
|
|
1176
|
+
const [mode] = selectInputEvent(option, modifiers).option;
|
|
1177
|
+
changeValue(applyDelta(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
|
|
1239
1178
|
}, [
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1179
|
+
changeValue,
|
|
1180
|
+
value,
|
|
1181
|
+
rawRange,
|
|
1182
|
+
range
|
|
1183
|
+
]);
|
|
1184
|
+
const wheelRefCallback = useWheel((event) => {
|
|
1185
|
+
if (!wheel || inactive || event.deltaY === 0) return;
|
|
1186
|
+
event.preventDefault();
|
|
1187
|
+
nudge(-Math.sign(event.deltaY), wheel, event);
|
|
1188
|
+
}, { requireFocus: true });
|
|
1189
|
+
const context = useMemo(() => ({
|
|
1190
|
+
value,
|
|
1191
|
+
min,
|
|
1192
|
+
max,
|
|
1193
|
+
step,
|
|
1194
|
+
scale,
|
|
1195
|
+
disabled,
|
|
1196
|
+
readonly,
|
|
1197
|
+
clampValue,
|
|
1198
|
+
range,
|
|
1199
|
+
rawRange,
|
|
1200
|
+
keyboard,
|
|
1201
|
+
text,
|
|
1202
|
+
editing,
|
|
1203
|
+
outOfRange,
|
|
1204
|
+
dragSensitivity,
|
|
1205
|
+
pointerLock,
|
|
1206
|
+
selectOnFocus,
|
|
1207
|
+
unformatOnFocus,
|
|
1208
|
+
keepCaretOnStep,
|
|
1209
|
+
blurOnEnter,
|
|
1210
|
+
atMin: clampValue && min !== void 0 && value <= min,
|
|
1211
|
+
atMax: clampValue && max !== void 0 && value >= max,
|
|
1212
|
+
drag,
|
|
1213
|
+
format,
|
|
1214
|
+
parse,
|
|
1215
|
+
setDraft: handleDraft,
|
|
1216
|
+
commitDraft,
|
|
1217
|
+
changeValue,
|
|
1218
|
+
nudge,
|
|
1219
|
+
inputRef
|
|
1220
|
+
}), [
|
|
1221
|
+
value,
|
|
1222
|
+
min,
|
|
1223
|
+
max,
|
|
1224
|
+
step,
|
|
1225
|
+
scale,
|
|
1226
|
+
disabled,
|
|
1227
|
+
readonly,
|
|
1228
|
+
clampValue,
|
|
1229
|
+
range,
|
|
1230
|
+
rawRange,
|
|
1231
|
+
keyboard,
|
|
1232
|
+
text,
|
|
1233
|
+
editing,
|
|
1234
|
+
outOfRange,
|
|
1235
|
+
drag,
|
|
1236
|
+
dragSensitivity,
|
|
1237
|
+
pointerLock,
|
|
1238
|
+
selectOnFocus,
|
|
1239
|
+
unformatOnFocus,
|
|
1240
|
+
keepCaretOnStep,
|
|
1241
|
+
blurOnEnter,
|
|
1242
|
+
format,
|
|
1243
|
+
parse,
|
|
1244
|
+
handleDraft,
|
|
1245
|
+
commitDraft,
|
|
1246
|
+
changeValue,
|
|
1247
|
+
nudge
|
|
1243
1248
|
]);
|
|
1244
|
-
const { refHandler: touchMoveRefCallback, pointerDownHandler } = usePianoDrag({
|
|
1245
|
-
baseElementRef: pianoRef,
|
|
1246
|
-
onDrag,
|
|
1247
|
-
onDragEnd
|
|
1248
|
-
});
|
|
1249
|
-
useEventListener(globalThis.window, "keydown", (e) => {
|
|
1250
|
-
if (e.repeat) return;
|
|
1251
|
-
if (!keyboardShortcuts) return;
|
|
1252
|
-
const index = keyboardShortcuts.keys.indexOf(e.key);
|
|
1253
|
-
if (index != -1) {
|
|
1254
|
-
if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play();
|
|
1255
|
-
}
|
|
1256
|
-
});
|
|
1257
|
-
useEventListener(globalThis.window, "keyup", (e) => {
|
|
1258
|
-
if (e.repeat) return;
|
|
1259
|
-
if (!keyboardShortcuts) return;
|
|
1260
|
-
const index = keyboardShortcuts.keys.indexOf(e.key);
|
|
1261
|
-
if (index != -1) keyRefs.current[index]?.current?.stop();
|
|
1262
|
-
});
|
|
1263
1249
|
useImperativeHandle(forwardedRef, () => {
|
|
1264
1250
|
return {
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
if (index != -1) {
|
|
1268
|
-
if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play(velocity);
|
|
1269
|
-
} else onPlayNote?.(note, velocity);
|
|
1251
|
+
focus() {
|
|
1252
|
+
if (!disabled) inputRef.current?.focus();
|
|
1270
1253
|
},
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
if (index != -1) keyRefs.current[index]?.current?.stop();
|
|
1274
|
-
else onStopNote?.(note);
|
|
1254
|
+
blur() {
|
|
1255
|
+
inputRef.current?.blur();
|
|
1275
1256
|
}
|
|
1276
1257
|
};
|
|
1277
|
-
}, [
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
onStopNote
|
|
1281
|
-
]);
|
|
1282
|
-
return /* @__PURE__ */ jsx(PianoProvider, {
|
|
1283
|
-
notePosition,
|
|
1284
|
-
noteRange,
|
|
1285
|
-
glissando,
|
|
1286
|
-
midiMax,
|
|
1287
|
-
fill,
|
|
1288
|
-
onPlayNote,
|
|
1289
|
-
onStopNote,
|
|
1290
|
-
label,
|
|
1258
|
+
}, [disabled]);
|
|
1259
|
+
return /* @__PURE__ */ jsx(NumberInputProvider, {
|
|
1260
|
+
value: context,
|
|
1291
1261
|
children: /* @__PURE__ */ jsx("div", {
|
|
1292
|
-
ref:
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
style: {
|
|
1298
|
-
width: fill ? "100%" : staticWidth,
|
|
1299
|
-
height,
|
|
1300
|
-
...style
|
|
1301
|
-
},
|
|
1302
|
-
onPointerDown: (event) => {
|
|
1303
|
-
pointerDownHandler(event);
|
|
1304
|
-
onPointerDown?.(event);
|
|
1305
|
-
},
|
|
1262
|
+
ref: wheelRefCallback,
|
|
1263
|
+
className,
|
|
1264
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1265
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1266
|
+
style,
|
|
1306
1267
|
...props,
|
|
1307
|
-
children
|
|
1308
|
-
ref: keyRefs.current[index],
|
|
1309
|
-
noteNumber: note,
|
|
1310
|
-
__width: whiteNoteWidth
|
|
1311
|
-
}, note) : /* @__PURE__ */ jsx(BlackKey, {
|
|
1312
|
-
ref: keyRefs.current[index],
|
|
1313
|
-
noteNumber: note,
|
|
1314
|
-
__width: whiteNoteWidth * blackPerWhiteWidth
|
|
1315
|
-
}, note))
|
|
1268
|
+
children
|
|
1316
1269
|
})
|
|
1317
1270
|
});
|
|
1318
1271
|
}),
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1272
|
+
InputField,
|
|
1273
|
+
Stepper,
|
|
1274
|
+
IncrementStepper,
|
|
1275
|
+
DecrementStepper
|
|
1322
1276
|
};
|
|
1323
1277
|
//#endregion
|
|
1324
|
-
//#region src/components/
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1278
|
+
//#region src/components/Piano/keyboard-shortcuts.ts
|
|
1279
|
+
/**
|
|
1280
|
+
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
1281
|
+
*/
|
|
1282
|
+
const SHORTCUTS = {
|
|
1283
|
+
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
1284
|
+
HOME_ROW: { keys: [
|
|
1285
|
+
"a",
|
|
1286
|
+
"w",
|
|
1287
|
+
"s",
|
|
1288
|
+
"e",
|
|
1289
|
+
"d",
|
|
1290
|
+
"f",
|
|
1291
|
+
"t",
|
|
1292
|
+
"g",
|
|
1293
|
+
"y",
|
|
1294
|
+
"h",
|
|
1295
|
+
"u",
|
|
1296
|
+
"j",
|
|
1297
|
+
"k",
|
|
1298
|
+
"o",
|
|
1299
|
+
"l",
|
|
1300
|
+
"p",
|
|
1301
|
+
";"
|
|
1302
|
+
] },
|
|
1303
|
+
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
1304
|
+
HOME_ROW_NATURAL: { keys: [
|
|
1305
|
+
"a",
|
|
1306
|
+
"",
|
|
1307
|
+
"s",
|
|
1308
|
+
"",
|
|
1309
|
+
"d",
|
|
1310
|
+
"f",
|
|
1311
|
+
"",
|
|
1312
|
+
"g",
|
|
1313
|
+
"",
|
|
1314
|
+
"h",
|
|
1315
|
+
"",
|
|
1316
|
+
"j",
|
|
1317
|
+
"k",
|
|
1318
|
+
"",
|
|
1319
|
+
"l",
|
|
1320
|
+
"",
|
|
1321
|
+
";"
|
|
1322
|
+
] }
|
|
1351
1323
|
};
|
|
1352
|
-
const PointsEditorContext = createContext(null);
|
|
1353
|
-
function PointsEditorProvider({ children, ...props }) {
|
|
1354
|
-
const storeRef = useRef(null);
|
|
1355
|
-
if (!storeRef.current) storeRef.current = createPointsEditorStore(props);
|
|
1356
|
-
useEffect(() => {
|
|
1357
|
-
if (storeRef.current) storeRef.current.setState(props);
|
|
1358
|
-
else storeRef.current = createPointsEditorStore(props);
|
|
1359
|
-
}, [props]);
|
|
1360
|
-
return /* @__PURE__ */ jsx(PointsEditorContext.Provider, {
|
|
1361
|
-
value: storeRef.current,
|
|
1362
|
-
children
|
|
1363
|
-
});
|
|
1364
|
-
}
|
|
1365
|
-
function usePointsEditorContext(selector) {
|
|
1366
|
-
const store = useContext(PointsEditorContext);
|
|
1367
|
-
if (!store) throw new Error("Missing PointsEditorContext.Provider in the tree");
|
|
1368
|
-
return useStore(store, selector);
|
|
1369
|
-
}
|
|
1370
1324
|
//#endregion
|
|
1371
|
-
//#region src/components/
|
|
1372
|
-
function
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1325
|
+
//#region src/components/Piano/index.tsx
|
|
1326
|
+
function isEditableTarget(target) {
|
|
1327
|
+
if (!(target instanceof HTMLElement)) return false;
|
|
1328
|
+
if (target.matches("input, textarea, select")) return true;
|
|
1329
|
+
for (let element = target; element;) {
|
|
1330
|
+
const contentEditable = element.getAttribute("contenteditable");
|
|
1331
|
+
if (contentEditable !== null) return contentEditable.toLowerCase() !== "false";
|
|
1332
|
+
element = element.parentElement;
|
|
1333
|
+
}
|
|
1334
|
+
return false;
|
|
1335
|
+
}
|
|
1336
|
+
function shortcutKey(event) {
|
|
1337
|
+
return event.code || event.key;
|
|
1381
1338
|
}
|
|
1382
|
-
//#endregion
|
|
1383
|
-
//#region src/hooks/useDragWithElement.ts
|
|
1384
1339
|
/**
|
|
1385
|
-
*
|
|
1386
|
-
* the bounding rect of `baseElementRef`.
|
|
1387
|
-
*
|
|
1388
|
-
* @returns a ref callback for the element that starts the drag, and whether a drag is in progress
|
|
1340
|
+
* Customizable piano component.
|
|
1389
1341
|
*/
|
|
1390
|
-
function
|
|
1391
|
-
const [dragging, setDragging] = useState(false);
|
|
1392
|
-
const normalizedX = useRef(0);
|
|
1393
|
-
const normalizedY = useRef(0);
|
|
1394
|
-
const dragHandler = useCallbackRef(onDrag);
|
|
1395
|
-
const dragStartHandler = useCallbackRef(onDragStart);
|
|
1396
|
-
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
1397
|
-
const update = useCallback((clientX, clientY) => {
|
|
1398
|
-
const base = baseElementRef.current;
|
|
1399
|
-
if (!base) return false;
|
|
1400
|
-
const { left, top, right, bottom } = base.getBoundingClientRect();
|
|
1401
|
-
normalizedX.current = normalizeValue(clientX, left, right);
|
|
1402
|
-
normalizedY.current = normalizeValue(clientY, top, bottom);
|
|
1403
|
-
return true;
|
|
1404
|
-
}, [baseElementRef]);
|
|
1342
|
+
const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, height, style, className, label, keyProps, classes, onPlayNote, onStopNote, ...props }, forwardedRef) {
|
|
1405
1343
|
const [node, setNode] = useState(null);
|
|
1344
|
+
const [activeNotes, setActiveNotes] = useState([]);
|
|
1345
|
+
/** Set while `resizable` is on, measured from the parent. */
|
|
1346
|
+
const [resizedKeyWidth, setResizedKeyWidth] = useState(whiteKeyWidth);
|
|
1347
|
+
const notes = useMemo(() => getNoteRangeArray(noteRange), [noteRange]);
|
|
1348
|
+
const whiteKeyCount = useMemo(() => notes.filter(isWhiteKey).length, [notes]);
|
|
1349
|
+
const layout = useMemo(() => ({
|
|
1350
|
+
noteRange,
|
|
1351
|
+
whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
|
|
1352
|
+
keyGap,
|
|
1353
|
+
blackKeyWidthRatio,
|
|
1354
|
+
blackKeyHeightRatio
|
|
1355
|
+
}), [
|
|
1356
|
+
noteRange,
|
|
1357
|
+
resizable,
|
|
1358
|
+
resizedKeyWidth,
|
|
1359
|
+
whiteKeyWidth,
|
|
1360
|
+
keyGap,
|
|
1361
|
+
blackKeyWidthRatio,
|
|
1362
|
+
blackKeyHeightRatio
|
|
1363
|
+
]);
|
|
1364
|
+
const latest = useRef({
|
|
1365
|
+
layout,
|
|
1366
|
+
glissando,
|
|
1367
|
+
midiMax,
|
|
1368
|
+
onPlayNote,
|
|
1369
|
+
onStopNote
|
|
1370
|
+
});
|
|
1371
|
+
const instanceRef = useRef(null);
|
|
1372
|
+
const shortcutNotes = useRef(/* @__PURE__ */ new Map());
|
|
1373
|
+
const releaseShortcutNotes = useCallback(() => {
|
|
1374
|
+
for (const { note, source } of shortcutNotes.current.values()) instanceRef.current?.noteOff(note, { source });
|
|
1375
|
+
shortcutNotes.current.clear();
|
|
1376
|
+
}, []);
|
|
1406
1377
|
useEffect(() => {
|
|
1407
1378
|
if (!node) return;
|
|
1408
|
-
const instance =
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
onDrag: ({ clientX, clientY }) => {
|
|
1416
|
-
if (update(clientX, clientY)) dragHandler(normalizedX.current, normalizedY.current);
|
|
1417
|
-
},
|
|
1418
|
-
onDragEnd: () => {
|
|
1419
|
-
setDragging(false);
|
|
1420
|
-
dragEndHandler(normalizedX.current, normalizedY.current);
|
|
1421
|
-
}
|
|
1379
|
+
const instance = createPianoInput(node, {
|
|
1380
|
+
layout: latest.current.layout,
|
|
1381
|
+
glissando: latest.current.glissando,
|
|
1382
|
+
midiMax: latest.current.midiMax,
|
|
1383
|
+
onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
|
|
1384
|
+
onStopNote: (note) => latest.current.onStopNote?.(note),
|
|
1385
|
+
onActiveNotesChange: setActiveNotes
|
|
1422
1386
|
});
|
|
1423
|
-
|
|
1387
|
+
instanceRef.current = instance;
|
|
1388
|
+
return () => {
|
|
1389
|
+
instanceRef.current = null;
|
|
1390
|
+
instance.destroy();
|
|
1391
|
+
};
|
|
1392
|
+
}, [node]);
|
|
1393
|
+
useEffect(() => {
|
|
1394
|
+
latest.current = {
|
|
1395
|
+
layout,
|
|
1396
|
+
glissando,
|
|
1397
|
+
midiMax,
|
|
1398
|
+
onPlayNote,
|
|
1399
|
+
onStopNote
|
|
1400
|
+
};
|
|
1401
|
+
instanceRef.current?.update({
|
|
1402
|
+
layout,
|
|
1403
|
+
glissando,
|
|
1404
|
+
midiMax
|
|
1405
|
+
});
|
|
1406
|
+
});
|
|
1407
|
+
useEffect(() => {
|
|
1408
|
+
if (!resizable || !node) return;
|
|
1409
|
+
const parent = node.parentElement;
|
|
1410
|
+
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1411
|
+
const resizeObserver = new ResizeObserver(() => {
|
|
1412
|
+
setResizedKeyWidth(node.clientWidth / Math.max(whiteKeyCount, 1) - keyGap);
|
|
1413
|
+
});
|
|
1414
|
+
resizeObserver.observe(parent);
|
|
1415
|
+
return () => resizeObserver.disconnect();
|
|
1424
1416
|
}, [
|
|
1417
|
+
resizable,
|
|
1425
1418
|
node,
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
cursor,
|
|
1429
|
-
dragHandler,
|
|
1430
|
-
dragStartHandler,
|
|
1431
|
-
dragEndHandler
|
|
1419
|
+
whiteKeyCount,
|
|
1420
|
+
keyGap
|
|
1432
1421
|
]);
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1422
|
+
const shortcutKeys = keyboardShortcuts?.keys;
|
|
1423
|
+
const hasShortcuts = shortcutKeys !== void 0;
|
|
1424
|
+
const shortcutMapping = shortcutKeys?.join("\0");
|
|
1425
|
+
useEffect(() => releaseShortcutNotes, [
|
|
1426
|
+
releaseShortcutNotes,
|
|
1427
|
+
shortcutMapping,
|
|
1428
|
+
keyboardShortcutsScope,
|
|
1429
|
+
noteRange.first,
|
|
1430
|
+
noteRange.last
|
|
1431
|
+
]);
|
|
1432
|
+
/** The note a shortcut key plays, or null when it has none. */
|
|
1433
|
+
function shortcutNote(key) {
|
|
1434
|
+
if (!shortcutKeys || key === "") return null;
|
|
1435
|
+
const index = shortcutKeys.indexOf(key);
|
|
1436
|
+
const note = noteRange.first + index;
|
|
1437
|
+
return index === -1 || note > noteRange.last ? null : note;
|
|
1438
|
+
}
|
|
1439
|
+
const shortcutTarget = useMemo(() => () => {
|
|
1440
|
+
if (!hasShortcuts) return null;
|
|
1441
|
+
return keyboardShortcutsScope === "window" ? globalThis.window : node;
|
|
1442
|
+
}, [
|
|
1443
|
+
hasShortcuts,
|
|
1444
|
+
keyboardShortcutsScope,
|
|
1445
|
+
node
|
|
1446
|
+
]);
|
|
1447
|
+
const focusOutTarget = useMemo(() => () => hasShortcuts && keyboardShortcutsScope === "root" ? node : null, [
|
|
1448
|
+
hasShortcuts,
|
|
1449
|
+
keyboardShortcutsScope,
|
|
1450
|
+
node
|
|
1451
|
+
]);
|
|
1452
|
+
const windowBlurTarget = useMemo(() => () => hasShortcuts ? globalThis.window : null, [hasShortcuts]);
|
|
1453
|
+
useEventListener(shortcutTarget, "keydown", (e) => {
|
|
1454
|
+
const key = shortcutKey(e);
|
|
1455
|
+
if (e.repeat || shortcutNotes.current.has(key)) return;
|
|
1456
|
+
if (isEditableTarget(e.target)) return;
|
|
1457
|
+
const note = shortcutNote(e.key);
|
|
1458
|
+
if (note === null) return;
|
|
1459
|
+
const source = `keyboard:${key}`;
|
|
1460
|
+
shortcutNotes.current.set(key, {
|
|
1461
|
+
note,
|
|
1462
|
+
source
|
|
1463
|
+
});
|
|
1464
|
+
instanceRef.current?.noteOn(note, { source });
|
|
1465
|
+
});
|
|
1466
|
+
useEventListener(shortcutTarget, "keyup", (e) => {
|
|
1467
|
+
const key = shortcutKey(e);
|
|
1468
|
+
const shortcut = shortcutNotes.current.get(key);
|
|
1469
|
+
if (!shortcut) return;
|
|
1470
|
+
shortcutNotes.current.delete(key);
|
|
1471
|
+
instanceRef.current?.noteOff(shortcut.note, { source: shortcut.source });
|
|
1472
|
+
});
|
|
1473
|
+
useEventListener(focusOutTarget, "focusout", (e) => {
|
|
1474
|
+
if (e.relatedTarget instanceof Node && node?.contains(e.relatedTarget)) return;
|
|
1475
|
+
releaseShortcutNotes();
|
|
1476
|
+
});
|
|
1477
|
+
useEventListener(windowBlurTarget, "blur", releaseShortcutNotes);
|
|
1478
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
1479
|
+
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1480
|
+
source: "api",
|
|
1481
|
+
velocity
|
|
1482
|
+
}),
|
|
1483
|
+
stopNote: (note) => instanceRef.current?.noteOff(note, { source: "api" })
|
|
1484
|
+
}), []);
|
|
1485
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1486
|
+
ref: setNode,
|
|
1487
|
+
className,
|
|
1488
|
+
"data-resizable": resizable ? "" : void 0,
|
|
1489
|
+
role: "group",
|
|
1490
|
+
tabIndex: 0,
|
|
1491
|
+
style: {
|
|
1492
|
+
width: resizable ? "100%" : pianoWidth(layout),
|
|
1493
|
+
"--height": cssLength(height),
|
|
1494
|
+
position: "relative",
|
|
1495
|
+
...style
|
|
1496
|
+
},
|
|
1497
|
+
...props,
|
|
1498
|
+
children: notes.map((note, index) => {
|
|
1499
|
+
const keyType = isWhiteKey(note) ? "white" : "black";
|
|
1500
|
+
const state = {
|
|
1501
|
+
index,
|
|
1502
|
+
keyType,
|
|
1503
|
+
active: activeNotes.includes(note),
|
|
1504
|
+
disabled: note > midiMax
|
|
1505
|
+
};
|
|
1506
|
+
const { className: keyClassName, style: keyStyle, ...rest } = keyProps?.(note, state) ?? {};
|
|
1507
|
+
const content = label?.(note, state);
|
|
1508
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1509
|
+
className: keyClassName,
|
|
1510
|
+
"data-note": note,
|
|
1511
|
+
"data-note-key": noteKey(note),
|
|
1512
|
+
"data-active": state.active ? "" : void 0,
|
|
1513
|
+
"data-disabled": state.disabled ? "" : void 0,
|
|
1514
|
+
...rest,
|
|
1515
|
+
style: {
|
|
1516
|
+
position: "absolute",
|
|
1517
|
+
zIndex: keyType === "white" ? 1 : 2,
|
|
1518
|
+
...keyStyle,
|
|
1519
|
+
left: notePosition(note, layout),
|
|
1520
|
+
width: keyType === "white" ? layout.whiteKeyWidth : blackKeyWidth(layout),
|
|
1521
|
+
height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
|
|
1522
|
+
},
|
|
1523
|
+
children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
1524
|
+
className: classes?.keyLabelWrapper,
|
|
1525
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
1526
|
+
className: classes?.keyLabel,
|
|
1527
|
+
children: content
|
|
1528
|
+
})
|
|
1529
|
+
})
|
|
1530
|
+
}, note);
|
|
1531
|
+
})
|
|
1532
|
+
});
|
|
1533
|
+
}) };
|
|
1534
|
+
//#endregion
|
|
1535
|
+
//#region src/components/PointsEditor/Background.tsx
|
|
1536
|
+
function Background({ className, children, style, ...props }) {
|
|
1537
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1538
|
+
className,
|
|
1539
|
+
style: {
|
|
1540
|
+
position: "absolute",
|
|
1541
|
+
inset: 0,
|
|
1542
|
+
zIndex: 0,
|
|
1543
|
+
...style
|
|
1544
|
+
},
|
|
1545
|
+
...props,
|
|
1546
|
+
children
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
//#endregion
|
|
1550
|
+
//#region src/components/PointsEditor/context.tsx
|
|
1551
|
+
const PointsEditorContext = /* @__PURE__ */ createContext(null);
|
|
1552
|
+
const PointsEditorProvider = PointsEditorContext.Provider;
|
|
1553
|
+
function usePointsEditorContext(selector) {
|
|
1554
|
+
const context = useContext(PointsEditorContext);
|
|
1555
|
+
if (!context) throw new Error("Missing PointsEditorContext.Provider in the tree");
|
|
1556
|
+
return selector ? selector(context) : context;
|
|
1437
1557
|
}
|
|
1438
1558
|
//#endregion
|
|
1559
|
+
//#region src/components/_util/VisuallyHiddenRangeInput.tsx
|
|
1560
|
+
const hiddenStyle = {
|
|
1561
|
+
position: "absolute",
|
|
1562
|
+
width: 1,
|
|
1563
|
+
height: 1,
|
|
1564
|
+
padding: 0,
|
|
1565
|
+
margin: -1,
|
|
1566
|
+
overflow: "hidden",
|
|
1567
|
+
clip: "rect(0, 0, 0, 0)",
|
|
1568
|
+
clipPath: "inset(50%)",
|
|
1569
|
+
whiteSpace: "nowrap",
|
|
1570
|
+
border: 0,
|
|
1571
|
+
pointerEvents: "none"
|
|
1572
|
+
};
|
|
1573
|
+
const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef((props, ref) => /* @__PURE__ */ jsx("input", {
|
|
1574
|
+
...props,
|
|
1575
|
+
ref,
|
|
1576
|
+
type: "range",
|
|
1577
|
+
style: hiddenStyle
|
|
1578
|
+
}));
|
|
1579
|
+
//#endregion
|
|
1439
1580
|
//#region src/components/PointsEditor/Point.tsx
|
|
1440
1581
|
function clampPoint(point, min, max) {
|
|
1441
1582
|
const { x, y } = point;
|
|
@@ -1444,82 +1585,471 @@ function clampPoint(point, min, max) {
|
|
|
1444
1585
|
y: clamp(y, min?.y ?? 0, max?.y ?? 1)
|
|
1445
1586
|
};
|
|
1446
1587
|
}
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1588
|
+
/**
|
|
1589
|
+
* A point is placed by its position within the container, so its value is a
|
|
1590
|
+
* position: 0..1 on each axis, with y growing downwards.
|
|
1591
|
+
*/
|
|
1592
|
+
const AXIS = {
|
|
1593
|
+
min: 0,
|
|
1594
|
+
max: 1
|
|
1595
|
+
};
|
|
1596
|
+
/**
|
|
1597
|
+
* One setting for both axes, or one per axis. The point holds a range input
|
|
1598
|
+
* for each, so anything that names or describes it comes in a pair.
|
|
1599
|
+
*/
|
|
1600
|
+
function perAxis(value, axis) {
|
|
1601
|
+
return typeof value === "string" ? value : value?.[axis];
|
|
1602
|
+
}
|
|
1603
|
+
function Point({ value, children, id: idProp, min, max, size, width, height, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
|
|
1604
|
+
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, registerPoint, beginPointDrag, movePointDrag, nudgeSelection } = usePointsEditorContext();
|
|
1605
|
+
const generatedId = useId();
|
|
1606
|
+
const id = idProp ?? generatedId;
|
|
1607
|
+
const selected = selection.includes(id);
|
|
1608
|
+
const disabled = _disabled ?? rootDisabled;
|
|
1609
|
+
const readonly = _readonly ?? rootReadonly;
|
|
1610
|
+
const inactive = disabled || readonly;
|
|
1611
|
+
const wheel = _wheel === void 0 ? rootWheel : _wheel;
|
|
1612
|
+
const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
|
|
1613
|
+
useCheckPlacement("PointsEditor.Point", "PointsEditor.Container");
|
|
1614
|
+
const [element, setElement] = useState(null);
|
|
1615
|
+
const xInputRef = useRef(null);
|
|
1616
|
+
const yInputRef = useRef(null);
|
|
1617
|
+
const registration = useRef({
|
|
1618
|
+
value,
|
|
1619
|
+
min,
|
|
1620
|
+
max,
|
|
1621
|
+
readonly: inactive,
|
|
1622
|
+
onChange,
|
|
1623
|
+
element,
|
|
1624
|
+
wheel
|
|
1625
|
+
});
|
|
1626
|
+
useEffect(() => {
|
|
1627
|
+
registration.current = {
|
|
1628
|
+
value,
|
|
1629
|
+
min,
|
|
1630
|
+
max,
|
|
1631
|
+
readonly: inactive,
|
|
1632
|
+
onChange,
|
|
1633
|
+
element,
|
|
1634
|
+
wheel
|
|
1635
|
+
};
|
|
1636
|
+
});
|
|
1637
|
+
useEffect(() => registerPoint(id, registration), [id, registerPoint]);
|
|
1638
|
+
/** Where the pointer was when the drag started, to measure the move from. */
|
|
1639
|
+
const pointerOrigin = useRef(null);
|
|
1640
|
+
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1641
|
+
axis: AXIS,
|
|
1642
|
+
baseElementRef: containerRef,
|
|
1643
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1644
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
1645
|
+
shouldStart: () => !disabled,
|
|
1646
|
+
onChange: ([x, y]) => {
|
|
1647
|
+
const origin = pointerOrigin.current;
|
|
1648
|
+
if (!origin) return;
|
|
1649
|
+
movePointDrag({
|
|
1650
|
+
x: x - origin.x,
|
|
1651
|
+
y: y - origin.y
|
|
1652
|
+
});
|
|
1461
1653
|
},
|
|
1462
|
-
onDragStart: (x, y) => {
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
onDragStart?.(clampPoint({
|
|
1654
|
+
onDragStart: ([x, y], state) => {
|
|
1655
|
+
beginPointDrag(id, state.event);
|
|
1656
|
+
pointerOrigin.current = {
|
|
1466
1657
|
x,
|
|
1467
1658
|
y
|
|
1468
|
-
}
|
|
1659
|
+
};
|
|
1660
|
+
xInputRef.current?.focus();
|
|
1661
|
+
if (inactive) return;
|
|
1662
|
+
onDragStart?.(clampPoint(value, min, max));
|
|
1663
|
+
},
|
|
1664
|
+
onDragEnd: () => {
|
|
1665
|
+
pointerOrigin.current = null;
|
|
1666
|
+
if (inactive) return;
|
|
1667
|
+
onDragEnd?.(clampPoint(value, min, max));
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
const nudge = useCallback((axis, direction, option, modifiers) => {
|
|
1671
|
+
const next = applyDelta(value[axis], direction, option, AXIS, modifiers);
|
|
1672
|
+
nudgeSelection(id, {
|
|
1673
|
+
x: axis === "x" ? next - value.x : 0,
|
|
1674
|
+
y: axis === "y" ? next - value.y : 0
|
|
1675
|
+
});
|
|
1676
|
+
}, [
|
|
1677
|
+
value,
|
|
1678
|
+
id,
|
|
1679
|
+
nudgeSelection
|
|
1680
|
+
]);
|
|
1681
|
+
const refCallback = useComposedRefs(dragRefCallback, setElement);
|
|
1682
|
+
const handleKeyDown = (event) => {
|
|
1683
|
+
const key = event.key;
|
|
1684
|
+
if (![
|
|
1685
|
+
"ArrowRight",
|
|
1686
|
+
"ArrowLeft",
|
|
1687
|
+
"ArrowUp",
|
|
1688
|
+
"ArrowDown"
|
|
1689
|
+
].includes(key)) return;
|
|
1690
|
+
const axis = key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y";
|
|
1691
|
+
event.preventDefault();
|
|
1692
|
+
if (!onChange || inactive || !keyboard) return;
|
|
1693
|
+
nudge(axis, key === "ArrowLeft" || key === "ArrowUp" ? -1 : 1, keyboard, event);
|
|
1694
|
+
};
|
|
1695
|
+
const current = clampPoint(value, min, max);
|
|
1696
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1697
|
+
ref: refCallback,
|
|
1698
|
+
className,
|
|
1699
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1700
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1701
|
+
tabIndex: -1,
|
|
1702
|
+
"data-dragging": dragging ? "" : void 0,
|
|
1703
|
+
"data-selected": selected ? "" : void 0,
|
|
1704
|
+
style: {
|
|
1705
|
+
"--color": color,
|
|
1706
|
+
"--width": cssLength(size ?? width),
|
|
1707
|
+
"--height": cssLength(size ?? height),
|
|
1708
|
+
position: "absolute",
|
|
1709
|
+
translate: "var(--translate, -50% -50%)",
|
|
1710
|
+
...style,
|
|
1711
|
+
left: `${value.x * 100}%`,
|
|
1712
|
+
top: `${value.y * 100}%`
|
|
1713
|
+
},
|
|
1714
|
+
onPointerDown,
|
|
1715
|
+
onFocus: (event) => {
|
|
1716
|
+
if (!disabled && event.target === event.currentTarget) xInputRef.current?.focus();
|
|
1717
|
+
onFocus?.(event);
|
|
1469
1718
|
},
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1719
|
+
onKeyDown: (event) => {
|
|
1720
|
+
handleKeyDown(event);
|
|
1721
|
+
onKeyDown?.(event);
|
|
1722
|
+
},
|
|
1723
|
+
...props,
|
|
1724
|
+
children: [["x", "y"].map((axis) => /* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
1725
|
+
ref: axis === "x" ? xInputRef : yInputRef,
|
|
1726
|
+
"data-axis": axis,
|
|
1727
|
+
value: current[axis],
|
|
1728
|
+
min: min?.[axis] ?? 0,
|
|
1729
|
+
max: max?.[axis] ?? 1,
|
|
1730
|
+
step: "any",
|
|
1731
|
+
disabled,
|
|
1732
|
+
"aria-readonly": readonly,
|
|
1733
|
+
"aria-orientation": axis === "x" ? "horizontal" : "vertical",
|
|
1734
|
+
"aria-label": perAxis(ariaLabel, axis) ?? axis,
|
|
1735
|
+
"aria-valuetext": perAxis(ariaValuetext, axis),
|
|
1736
|
+
onChange: (event) => {
|
|
1737
|
+
if (readonly) {
|
|
1738
|
+
event.currentTarget.value = String(current[axis]);
|
|
1739
|
+
return;
|
|
1740
|
+
}
|
|
1741
|
+
const delta = event.currentTarget.valueAsNumber - value[axis];
|
|
1742
|
+
nudgeSelection(id, {
|
|
1743
|
+
x: axis === "x" ? delta : 0,
|
|
1744
|
+
y: axis === "y" ? delta : 0
|
|
1745
|
+
});
|
|
1746
|
+
}
|
|
1747
|
+
}, axis)), children]
|
|
1748
|
+
});
|
|
1749
|
+
}
|
|
1750
|
+
//#endregion
|
|
1751
|
+
//#region src/components/PointsEditor/Container.tsx
|
|
1752
|
+
const Container = /* @__PURE__ */ forwardRef(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1753
|
+
const { containerRef, disabled, selectable, isPointElement, nudgeFocusedPoint, beginSelectionBox, moveSelectionBox, endSelectionBox } = usePointsEditorContext();
|
|
1754
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1755
|
+
axis: AXIS,
|
|
1756
|
+
baseElementRef: containerRef,
|
|
1757
|
+
shouldStart: (event) => !disabled && !isPointElement(event.target),
|
|
1758
|
+
onDragStart: ([x, y], state) => {
|
|
1759
|
+
beginSelectionBox({
|
|
1474
1760
|
x,
|
|
1475
1761
|
y
|
|
1476
|
-
},
|
|
1477
|
-
}
|
|
1762
|
+
}, state.event);
|
|
1763
|
+
},
|
|
1764
|
+
onChange: ([x, y]) => moveSelectionBox({
|
|
1765
|
+
x,
|
|
1766
|
+
y
|
|
1767
|
+
}),
|
|
1768
|
+
onDragEnd: endSelectionBox
|
|
1478
1769
|
});
|
|
1479
|
-
|
|
1770
|
+
useWheel((event) => {
|
|
1771
|
+
const horizontal = event.deltaX !== 0;
|
|
1772
|
+
const delta = horizontal ? event.deltaX : event.deltaY;
|
|
1773
|
+
if (delta === 0) return;
|
|
1774
|
+
const axis = horizontal || event.shiftKey ? "x" : "y";
|
|
1775
|
+
if (nudgeFocusedPoint(axis, delta < 0 ? -1 : 1, event)) event.preventDefault();
|
|
1776
|
+
}, { target: containerRef });
|
|
1777
|
+
const composedRef = useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1480
1778
|
return /* @__PURE__ */ jsx("div", {
|
|
1481
|
-
ref:
|
|
1482
|
-
className
|
|
1483
|
-
tabIndex: 0,
|
|
1484
|
-
"aria-disabled": disabled != void 0 ? disabled : __disabled,
|
|
1485
|
-
"aria-readonly": readonly != void 0 ? readonly : __readonly,
|
|
1486
|
-
"data-dragging": dragging,
|
|
1779
|
+
ref: composedRef,
|
|
1780
|
+
className,
|
|
1487
1781
|
style: {
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
left: `${value.x * 100}%`,
|
|
1492
|
-
top: `${value.y * 100}%`,
|
|
1782
|
+
position: "absolute",
|
|
1783
|
+
inset: 0,
|
|
1784
|
+
zIndex: 10,
|
|
1493
1785
|
...style
|
|
1494
1786
|
},
|
|
1495
|
-
|
|
1496
|
-
|
|
1787
|
+
...props,
|
|
1788
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
1789
|
+
name: "PointsEditor.Container",
|
|
1790
|
+
children
|
|
1791
|
+
})
|
|
1792
|
+
});
|
|
1793
|
+
});
|
|
1794
|
+
//#endregion
|
|
1795
|
+
//#region src/components/PointsEditor/SelectionBox.tsx
|
|
1796
|
+
/**
|
|
1797
|
+
* The box a drag on empty space draws, and what it covers is selected.
|
|
1798
|
+
*
|
|
1799
|
+
* Nothing is rendered while no drag is running, and leaving it out leaves the
|
|
1800
|
+
* editor without one — the selection still works, it just cannot be seen.
|
|
1801
|
+
*/
|
|
1802
|
+
function SelectionBox({ children, className, style, ...props }) {
|
|
1803
|
+
useCheckPlacement("PointsEditor.SelectionBox", "PointsEditor.Container");
|
|
1804
|
+
const selectionBox = usePointsEditorContext((s) => s.selectionBox);
|
|
1805
|
+
if (!selectionBox) return null;
|
|
1806
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1807
|
+
className,
|
|
1808
|
+
...props,
|
|
1809
|
+
style: {
|
|
1810
|
+
position: "absolute",
|
|
1811
|
+
zIndex: 20,
|
|
1812
|
+
pointerEvents: "none",
|
|
1813
|
+
...style,
|
|
1814
|
+
left: `${selectionBox.x * 100}%`,
|
|
1815
|
+
top: `${selectionBox.y * 100}%`,
|
|
1816
|
+
width: `${selectionBox.width * 100}%`,
|
|
1817
|
+
height: `${selectionBox.height * 100}%`
|
|
1818
|
+
},
|
|
1819
|
+
children
|
|
1497
1820
|
});
|
|
1498
1821
|
}
|
|
1499
1822
|
//#endregion
|
|
1500
1823
|
//#region src/components/PointsEditor/index.tsx
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1824
|
+
/** One array for every editor with selection turned off, so memos hold still. */
|
|
1825
|
+
const EMPTY = [];
|
|
1826
|
+
/**
|
|
1827
|
+
* How far a point may move before something in the selection leaves its range.
|
|
1828
|
+
*
|
|
1829
|
+
* Clamping each point on its own would break the shape of the selection: the
|
|
1830
|
+
* one that reached the edge would stop while the rest carried on. One amount
|
|
1831
|
+
* for all of them means the whole selection stops together.
|
|
1832
|
+
*/
|
|
1833
|
+
function allowedDelta(delta, entries) {
|
|
1834
|
+
let loX = -Infinity;
|
|
1835
|
+
let hiX = Infinity;
|
|
1836
|
+
let loY = -Infinity;
|
|
1837
|
+
let hiY = Infinity;
|
|
1838
|
+
for (const { start, registration } of entries) {
|
|
1839
|
+
loX = Math.max(loX, (registration.min?.x ?? 0) - start.x);
|
|
1840
|
+
hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x);
|
|
1841
|
+
loY = Math.max(loY, (registration.min?.y ?? 0) - start.y);
|
|
1842
|
+
hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y);
|
|
1843
|
+
}
|
|
1844
|
+
return {
|
|
1845
|
+
x: hiX < loX ? 0 : clamp(delta.x, loX, hiX),
|
|
1846
|
+
y: hiY < loY ? 0 : clamp(delta.y, loY, hiY)
|
|
1847
|
+
};
|
|
1848
|
+
}
|
|
1849
|
+
/**
|
|
1850
|
+
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
1851
|
+
* in a hundred steps whatever its pixel size.
|
|
1852
|
+
*/
|
|
1853
|
+
const DEFAULT_WHEEL = ["normalized", .01];
|
|
1854
|
+
/**
|
|
1855
|
+
* Shift is the fine-adjustment key everywhere else, so it is bound here too
|
|
1856
|
+
* — but only on the keyboard. On the wheel it already means the x axis, and
|
|
1857
|
+
* browsers hand shift+wheel over as horizontal scrolling anyway.
|
|
1858
|
+
*/
|
|
1859
|
+
const DEFAULT_KEYBOARD = {
|
|
1860
|
+
default: ["normalized", .01],
|
|
1861
|
+
shift: ["normalized", .001]
|
|
1504
1862
|
};
|
|
1505
1863
|
/**
|
|
1506
1864
|
* Multiple Point Controller
|
|
1507
1865
|
*/
|
|
1508
1866
|
const PointsEditor = {
|
|
1509
|
-
Root: forwardRef(({ width =
|
|
1867
|
+
Root: /* @__PURE__ */ forwardRef(({ width, height, disabled = false, readonly = false, wheel = DEFAULT_WHEEL, keyboard = DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
|
|
1868
|
+
const containerRef = useRef(null);
|
|
1869
|
+
const { cursor = "grabbing" } = externalStyles ?? {};
|
|
1870
|
+
const controlled = selectionProp !== void 0;
|
|
1871
|
+
const [ownSelection, setOwnSelection] = useState(defaultSelection ?? []);
|
|
1872
|
+
const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
|
|
1873
|
+
const selectionRef = useRef(selection);
|
|
1874
|
+
const changeHandlerRef = useRef(onSelectionChange);
|
|
1875
|
+
useEffect(() => {
|
|
1876
|
+
selectionRef.current = selection;
|
|
1877
|
+
changeHandlerRef.current = onSelectionChange;
|
|
1878
|
+
});
|
|
1879
|
+
const changeSelection = useCallback((next) => {
|
|
1880
|
+
selectionRef.current = next;
|
|
1881
|
+
if (!controlled) setOwnSelection(next);
|
|
1882
|
+
changeHandlerRef.current?.(next);
|
|
1883
|
+
}, [controlled]);
|
|
1884
|
+
/**
|
|
1885
|
+
* Every mounted point, by id. A registration is a ref rather than a value:
|
|
1886
|
+
* the value inside changes on every frame of a drag, and a registry keyed
|
|
1887
|
+
* on it would be rebuilt just as often.
|
|
1888
|
+
*/
|
|
1889
|
+
const points = useRef(/* @__PURE__ */ new Map());
|
|
1890
|
+
const registerPoint = useCallback((id, entry) => {
|
|
1891
|
+
points.current.set(id, entry);
|
|
1892
|
+
return () => {
|
|
1893
|
+
points.current.delete(id);
|
|
1894
|
+
};
|
|
1895
|
+
}, []);
|
|
1896
|
+
/** What the current drag picked up, and where those points started. */
|
|
1897
|
+
const dragRef = useRef([]);
|
|
1898
|
+
const snapshot = useCallback((ids) => {
|
|
1899
|
+
return ids.flatMap((id) => {
|
|
1900
|
+
const registration = points.current.get(id)?.current;
|
|
1901
|
+
return registration ? [{
|
|
1902
|
+
id,
|
|
1903
|
+
start: { ...registration.value }
|
|
1904
|
+
}] : [];
|
|
1905
|
+
});
|
|
1906
|
+
}, []);
|
|
1907
|
+
const applyDeltaTo = useCallback((entries, delta) => {
|
|
1908
|
+
const withRegistration = entries.flatMap((entry) => {
|
|
1909
|
+
const registration = points.current.get(entry.id)?.current;
|
|
1910
|
+
return registration ? [{
|
|
1911
|
+
...entry,
|
|
1912
|
+
registration
|
|
1913
|
+
}] : [];
|
|
1914
|
+
});
|
|
1915
|
+
const allowed = allowedDelta(delta, withRegistration);
|
|
1916
|
+
for (const { start, registration } of withRegistration) {
|
|
1917
|
+
if (registration.readonly) continue;
|
|
1918
|
+
registration.onChange?.({
|
|
1919
|
+
x: toPrecision(start.x + allowed.x),
|
|
1920
|
+
y: toPrecision(start.y + allowed.y)
|
|
1921
|
+
});
|
|
1922
|
+
}
|
|
1923
|
+
}, []);
|
|
1924
|
+
const beginPointDrag = useCallback((id, modifiers) => {
|
|
1925
|
+
if (!selectable) {
|
|
1926
|
+
dragRef.current = snapshot([id]);
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
const current = selectionRef.current;
|
|
1930
|
+
const additive = modifiers.ctrlKey || modifiers.metaKey;
|
|
1931
|
+
let next;
|
|
1932
|
+
if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
1933
|
+
else if (current.includes(id)) next = current;
|
|
1934
|
+
else next = [id];
|
|
1935
|
+
changeSelection([...next]);
|
|
1936
|
+
dragRef.current = next.includes(id) ? snapshot(next) : [];
|
|
1937
|
+
}, [
|
|
1938
|
+
selectable,
|
|
1939
|
+
changeSelection,
|
|
1940
|
+
snapshot
|
|
1941
|
+
]);
|
|
1942
|
+
const movePointDrag = useCallback((delta) => applyDeltaTo(dragRef.current, delta), [applyDeltaTo]);
|
|
1943
|
+
const nudgeSelection = useCallback((id, delta) => {
|
|
1944
|
+
const ids = selectionRef.current.includes(id) ? selectionRef.current : [id];
|
|
1945
|
+
applyDeltaTo(snapshot(ids), delta);
|
|
1946
|
+
}, [applyDeltaTo, snapshot]);
|
|
1947
|
+
const isPointElement = useCallback((element) => {
|
|
1948
|
+
if (!element) return false;
|
|
1949
|
+
for (const [, entry] of points.current) if (entry.current.element?.contains(element)) return true;
|
|
1950
|
+
return false;
|
|
1951
|
+
}, []);
|
|
1952
|
+
const nudgeFocusedPoint = useCallback((axis, direction, modifiers) => {
|
|
1953
|
+
const container = containerRef.current;
|
|
1954
|
+
const active = container?.ownerDocument.activeElement;
|
|
1955
|
+
if (!container || !active || !container.contains(active)) return false;
|
|
1956
|
+
for (const [id, entry] of points.current) {
|
|
1957
|
+
const { element, wheel, readonly, onChange, value } = entry.current;
|
|
1958
|
+
if (!element?.contains(active)) continue;
|
|
1959
|
+
if (!wheel || readonly || !onChange) return false;
|
|
1960
|
+
const next = applyDelta(value[axis], direction, wheel, AXIS, modifiers);
|
|
1961
|
+
nudgeSelection(id, {
|
|
1962
|
+
x: axis === "x" ? next - value.x : 0,
|
|
1963
|
+
y: axis === "y" ? next - value.y : 0
|
|
1964
|
+
});
|
|
1965
|
+
return true;
|
|
1966
|
+
}
|
|
1967
|
+
return false;
|
|
1968
|
+
}, [nudgeSelection]);
|
|
1969
|
+
const [selectionBox, setSelectionBox] = useState(null);
|
|
1970
|
+
const selectionBoxRef = useRef(null);
|
|
1971
|
+
selectionBoxRef.current ??= createSelectionBox({
|
|
1972
|
+
*items() {
|
|
1973
|
+
for (const [id, entry] of points.current) {
|
|
1974
|
+
const { x, y } = entry.current.value;
|
|
1975
|
+
yield [id, [x, y]];
|
|
1976
|
+
}
|
|
1977
|
+
},
|
|
1978
|
+
onBoxChange: setSelectionBox
|
|
1979
|
+
});
|
|
1980
|
+
useEffect(() => {
|
|
1981
|
+
selectionBoxRef.current?.update({ onSelectionChange: changeSelection });
|
|
1982
|
+
}, [changeSelection]);
|
|
1983
|
+
useEffect(() => {
|
|
1984
|
+
const box = selectionBoxRef.current;
|
|
1985
|
+
return () => box?.destroy();
|
|
1986
|
+
}, []);
|
|
1987
|
+
const beginSelectionBox = useCallback((at, modifiers) => {
|
|
1988
|
+
if (!selectable) return;
|
|
1989
|
+
selectionBoxRef.current?.begin([at.x, at.y], {
|
|
1990
|
+
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
1991
|
+
selection: selectionRef.current
|
|
1992
|
+
});
|
|
1993
|
+
}, [selectable]);
|
|
1994
|
+
const moveSelectionBox = useCallback((to) => {
|
|
1995
|
+
selectionBoxRef.current?.move([to.x, to.y]);
|
|
1996
|
+
}, []);
|
|
1997
|
+
const endSelectionBox = useCallback(() => {
|
|
1998
|
+
if (!selectionBoxRef.current?.end()) return;
|
|
1999
|
+
const [first] = selectionRef.current;
|
|
2000
|
+
if (first) points.current.get(first)?.current.element?.focus();
|
|
2001
|
+
}, []);
|
|
2002
|
+
const context = useMemo(() => ({
|
|
2003
|
+
disabled,
|
|
2004
|
+
readonly,
|
|
2005
|
+
wheel,
|
|
2006
|
+
keyboard,
|
|
2007
|
+
dragSensitivity,
|
|
2008
|
+
externalStyles: { cursor },
|
|
2009
|
+
containerRef,
|
|
2010
|
+
selectable,
|
|
2011
|
+
selection,
|
|
2012
|
+
registerPoint,
|
|
2013
|
+
isPointElement,
|
|
2014
|
+
beginPointDrag,
|
|
2015
|
+
movePointDrag,
|
|
2016
|
+
nudgeSelection,
|
|
2017
|
+
nudgeFocusedPoint,
|
|
2018
|
+
selectionBox,
|
|
2019
|
+
beginSelectionBox,
|
|
2020
|
+
moveSelectionBox,
|
|
2021
|
+
endSelectionBox
|
|
2022
|
+
}), [
|
|
2023
|
+
disabled,
|
|
2024
|
+
readonly,
|
|
2025
|
+
wheel,
|
|
2026
|
+
keyboard,
|
|
2027
|
+
dragSensitivity,
|
|
2028
|
+
cursor,
|
|
2029
|
+
selectable,
|
|
2030
|
+
selection,
|
|
2031
|
+
registerPoint,
|
|
2032
|
+
isPointElement,
|
|
2033
|
+
beginPointDrag,
|
|
2034
|
+
movePointDrag,
|
|
2035
|
+
nudgeSelection,
|
|
2036
|
+
nudgeFocusedPoint,
|
|
2037
|
+
selectionBox,
|
|
2038
|
+
beginSelectionBox,
|
|
2039
|
+
moveSelectionBox,
|
|
2040
|
+
endSelectionBox
|
|
2041
|
+
]);
|
|
1510
2042
|
return /* @__PURE__ */ jsx(PointsEditorProvider, {
|
|
1511
|
-
|
|
1512
|
-
readonly,
|
|
1513
|
-
externalStyles: {
|
|
1514
|
-
...defaultExternalStyles$2,
|
|
1515
|
-
..._externalStyles
|
|
1516
|
-
},
|
|
2043
|
+
value: context,
|
|
1517
2044
|
children: /* @__PURE__ */ jsx("div", {
|
|
1518
2045
|
ref: forwardedRef,
|
|
1519
|
-
className
|
|
2046
|
+
className,
|
|
2047
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2048
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1520
2049
|
style: {
|
|
1521
|
-
width,
|
|
1522
|
-
height,
|
|
2050
|
+
"--width": cssLength(width),
|
|
2051
|
+
"--height": cssLength(height),
|
|
2052
|
+
position: "relative",
|
|
1523
2053
|
...style
|
|
1524
2054
|
},
|
|
1525
2055
|
...props,
|
|
@@ -1529,108 +2059,84 @@ const PointsEditor = {
|
|
|
1529
2059
|
}),
|
|
1530
2060
|
Background,
|
|
1531
2061
|
Container,
|
|
1532
|
-
Point
|
|
2062
|
+
Point,
|
|
2063
|
+
SelectionBox
|
|
1533
2064
|
};
|
|
1534
2065
|
//#endregion
|
|
1535
2066
|
//#region src/components/Slider/context.tsx
|
|
1536
|
-
const
|
|
1537
|
-
|
|
1538
|
-
min: 0,
|
|
1539
|
-
max: 0,
|
|
1540
|
-
step: 0,
|
|
1541
|
-
skew: 0,
|
|
1542
|
-
vertical: false,
|
|
1543
|
-
reverse: false,
|
|
1544
|
-
disabled: false,
|
|
1545
|
-
readonly: false
|
|
1546
|
-
};
|
|
1547
|
-
return createStore()(() => ({
|
|
1548
|
-
...DEFAULT_PROPS,
|
|
1549
|
-
...initProps
|
|
1550
|
-
}));
|
|
1551
|
-
};
|
|
1552
|
-
const SliderContext = createContext(null);
|
|
1553
|
-
function SliderProvider({ children, ...props }) {
|
|
1554
|
-
const storeRef = useRef(null);
|
|
1555
|
-
if (!storeRef.current) storeRef.current = createSliderStore(props);
|
|
1556
|
-
useEffect(() => {
|
|
1557
|
-
if (storeRef.current) storeRef.current.setState(props);
|
|
1558
|
-
else storeRef.current = createSliderStore(props);
|
|
1559
|
-
}, [props]);
|
|
1560
|
-
return /* @__PURE__ */ jsx(SliderContext.Provider, {
|
|
1561
|
-
value: storeRef.current,
|
|
1562
|
-
children
|
|
1563
|
-
});
|
|
1564
|
-
}
|
|
2067
|
+
const SliderContext = /* @__PURE__ */ createContext(null);
|
|
2068
|
+
const SliderProvider = SliderContext.Provider;
|
|
1565
2069
|
function useSliderContext(selector) {
|
|
1566
|
-
const
|
|
1567
|
-
if (!
|
|
1568
|
-
return
|
|
2070
|
+
const context = useContext(SliderContext);
|
|
2071
|
+
if (!context) throw new Error("Missing SliderContext.Provider in the tree");
|
|
2072
|
+
return selector ? selector(context) : context;
|
|
1569
2073
|
}
|
|
1570
2074
|
//#endregion
|
|
1571
|
-
//#region src/components/Slider/
|
|
1572
|
-
function
|
|
2075
|
+
//#region src/components/Slider/MarksOption.tsx
|
|
2076
|
+
function MarksOption({ value, mark = true, label, thickness, length, gap, labelWidth, classes, styles, className, style, ...props }) {
|
|
2077
|
+
useCheckPlacement("Slider.MarksOption", "Slider.Marks");
|
|
1573
2078
|
const min = useSliderContext((s) => s.min);
|
|
1574
2079
|
const max = useSliderContext((s) => s.max);
|
|
1575
|
-
const
|
|
2080
|
+
const scale = useSliderContext((s) => s.scale);
|
|
1576
2081
|
const vertical = useSliderContext((s) => s.vertical);
|
|
1577
2082
|
const reverse = useSliderContext((s) => s.reverse);
|
|
1578
|
-
const calcPercent = useCallback((
|
|
1579
|
-
|
|
2083
|
+
const calcPercent = useCallback((value) => {
|
|
2084
|
+
const percent = scale.normalize(value, min, max) * 100;
|
|
2085
|
+
return toFixed(xor(vertical, reverse) ? 100 - percent : percent);
|
|
1580
2086
|
}, [
|
|
1581
2087
|
vertical,
|
|
1582
2088
|
reverse,
|
|
1583
2089
|
max,
|
|
1584
2090
|
min,
|
|
1585
|
-
|
|
2091
|
+
scale
|
|
1586
2092
|
]);
|
|
1587
2093
|
return /* @__PURE__ */ jsxs("div", {
|
|
1588
|
-
className
|
|
2094
|
+
className,
|
|
1589
2095
|
style: {
|
|
2096
|
+
"--thickness": cssLength(thickness),
|
|
2097
|
+
"--length": cssLength(length),
|
|
2098
|
+
"--gap": cssLength(gap),
|
|
2099
|
+
"--label-width": cssLength(labelWidth),
|
|
2100
|
+
position: "absolute",
|
|
2101
|
+
translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
|
|
2102
|
+
zIndex: 10,
|
|
2103
|
+
...style,
|
|
1590
2104
|
left: !vertical ? `${calcPercent(value)}%` : void 0,
|
|
1591
|
-
top: vertical ? `${calcPercent(value)}%` : void 0
|
|
1592
|
-
...style
|
|
2105
|
+
top: vertical ? `${calcPercent(value)}%` : void 0
|
|
1593
2106
|
},
|
|
1594
|
-
"data-vertical":
|
|
2107
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
1595
2108
|
...props,
|
|
1596
|
-
children: [
|
|
1597
|
-
className:
|
|
1598
|
-
style:
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
},
|
|
1605
|
-
"data-vertical": vertical
|
|
1606
|
-
}), type != "mark" && /* @__PURE__ */ jsx("div", {
|
|
1607
|
-
className: clsx("tremolo-slider-scale-option-label", classes?.label),
|
|
1608
|
-
style: {
|
|
1609
|
-
width: labelWidth,
|
|
1610
|
-
...styles?.label
|
|
1611
|
-
},
|
|
1612
|
-
children: label || value
|
|
2109
|
+
children: [mark && /* @__PURE__ */ jsx("div", {
|
|
2110
|
+
className: classes?.mark,
|
|
2111
|
+
style: styles?.mark,
|
|
2112
|
+
"data-orientation": vertical ? "vertical" : "horizontal"
|
|
2113
|
+
}), label !== null && /* @__PURE__ */ jsx("div", {
|
|
2114
|
+
className: classes?.label,
|
|
2115
|
+
style: styles?.label,
|
|
2116
|
+
children: label ?? value
|
|
1613
2117
|
})]
|
|
1614
2118
|
});
|
|
1615
2119
|
}
|
|
1616
2120
|
//#endregion
|
|
1617
2121
|
//#region src/components/Slider/type.ts
|
|
1618
2122
|
function generateOptionsList(options, min, max, step) {
|
|
2123
|
+
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
1619
2124
|
const optionsList = [];
|
|
1620
|
-
const
|
|
1621
|
-
const count = Math.floor(max /
|
|
2125
|
+
const interval = per === "step" ? step : per;
|
|
2126
|
+
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
1622
2127
|
for (let i = 0; i < count; i++) {
|
|
1623
|
-
const value = toFixed(
|
|
2128
|
+
const value = toFixed(interval * (Math.ceil(min / interval) + i), decimalPart(interval)?.length);
|
|
1624
2129
|
optionsList.push({
|
|
1625
2130
|
value,
|
|
1626
|
-
|
|
2131
|
+
mark,
|
|
2132
|
+
label
|
|
1627
2133
|
});
|
|
1628
2134
|
}
|
|
1629
2135
|
return optionsList;
|
|
1630
2136
|
}
|
|
1631
2137
|
//#endregion
|
|
1632
|
-
//#region src/components/Slider/
|
|
1633
|
-
function
|
|
2138
|
+
//#region src/components/Slider/Marks.tsx
|
|
2139
|
+
function Marks({ gap, options, children, className, style, ...props }) {
|
|
1634
2140
|
const min = useSliderContext((s) => s.min);
|
|
1635
2141
|
const max = useSliderContext((s) => s.max);
|
|
1636
2142
|
const step = useSliderContext((s) => s.step);
|
|
@@ -1639,233 +2145,260 @@ function Scale({ gap = 6, options, children, className, style, ...props }) {
|
|
|
1639
2145
|
const optionsList = options ? generateOptionsList(options, min, max, step) : [];
|
|
1640
2146
|
if (xor(vertical, reverse)) optionsList.reverse();
|
|
1641
2147
|
return /* @__PURE__ */ jsx("div", {
|
|
1642
|
-
className
|
|
2148
|
+
className,
|
|
2149
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
1643
2150
|
style: {
|
|
1644
|
-
|
|
1645
|
-
|
|
2151
|
+
"--gap": cssLength(gap),
|
|
2152
|
+
position: "relative",
|
|
1646
2153
|
...style
|
|
1647
2154
|
},
|
|
1648
2155
|
...props,
|
|
1649
|
-
children:
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
2156
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2157
|
+
name: "Slider.Marks",
|
|
2158
|
+
children: options ? optionsList.map(({ value, mark, label }, index) => {
|
|
2159
|
+
return /* @__PURE__ */ jsx(MarksOption, {
|
|
2160
|
+
value,
|
|
2161
|
+
mark,
|
|
2162
|
+
label: label ? void 0 : null
|
|
2163
|
+
}, index);
|
|
2164
|
+
}) : children
|
|
2165
|
+
})
|
|
1655
2166
|
});
|
|
1656
2167
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
const
|
|
1661
|
-
const readonly = useSliderContext(
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
2168
|
+
//#endregion
|
|
2169
|
+
//#region src/components/Slider/Thumb.tsx
|
|
2170
|
+
const Thumb$1 = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
|
|
2171
|
+
const inputRef = useRef(null);
|
|
2172
|
+
const { value, min, max, step, vertical, disabled, readonly, onChange, percent, thumbRef } = useSliderContext();
|
|
2173
|
+
useCheckPlacement("Slider.Thumb", "Slider.Track");
|
|
2174
|
+
const methods = () => ({
|
|
2175
|
+
focus() {
|
|
2176
|
+
if (!disabled) inputRef.current?.focus();
|
|
2177
|
+
},
|
|
2178
|
+
blur() {
|
|
2179
|
+
inputRef.current?.blur();
|
|
2180
|
+
}
|
|
2181
|
+
});
|
|
2182
|
+
useImperativeHandle(forwardedRef, methods, [disabled]);
|
|
2183
|
+
useImperativeHandle(thumbRef, methods, [disabled]);
|
|
2184
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2185
|
+
className,
|
|
2186
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2187
|
+
"data-readonly": readonly ? "" : void 0,
|
|
2188
|
+
...props,
|
|
1674
2189
|
style: {
|
|
1675
|
-
|
|
1676
|
-
|
|
2190
|
+
"--color": color,
|
|
2191
|
+
position: "absolute",
|
|
2192
|
+
translate: "var(--translate, -50% -50%)",
|
|
2193
|
+
zIndex: 100,
|
|
2194
|
+
...style,
|
|
2195
|
+
top: vertical ? `${percent}%` : "50%",
|
|
2196
|
+
left: !vertical ? `${percent}%` : "50%"
|
|
1677
2197
|
},
|
|
1678
|
-
children:
|
|
1679
|
-
ref:
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2198
|
+
children: [/* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
2199
|
+
ref: inputRef,
|
|
2200
|
+
value,
|
|
2201
|
+
min,
|
|
2202
|
+
max,
|
|
2203
|
+
step,
|
|
2204
|
+
disabled,
|
|
1683
2205
|
"aria-readonly": readonly,
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
2206
|
+
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
2207
|
+
"aria-label": ariaLabel,
|
|
2208
|
+
"aria-labelledby": ariaLabelledby,
|
|
2209
|
+
"aria-describedby": ariaDescribedby,
|
|
2210
|
+
"aria-valuetext": ariaValuetext,
|
|
2211
|
+
onChange: (event) => {
|
|
2212
|
+
if (readonly) {
|
|
2213
|
+
event.currentTarget.value = String(value);
|
|
2214
|
+
return;
|
|
2215
|
+
}
|
|
2216
|
+
onChange?.(event.currentTarget.valueAsNumber);
|
|
1689
2217
|
}
|
|
1690
|
-
})
|
|
2218
|
+
}), children]
|
|
1691
2219
|
});
|
|
1692
2220
|
});
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
const disabled = useSliderContext(
|
|
1697
|
-
const
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
height: vertical ? length : thickness,
|
|
2221
|
+
//#endregion
|
|
2222
|
+
//#region src/components/Slider/Track.tsx
|
|
2223
|
+
const Track = /* @__PURE__ */ forwardRef(function Track({ length, thickness, active, inactive, children, className, style, ...props }, forwardedRef) {
|
|
2224
|
+
const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
|
|
2225
|
+
const composedRef = useComposedRefs(forwardedRef, trackRef);
|
|
2226
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2227
|
+
ref: composedRef,
|
|
2228
|
+
className,
|
|
2229
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2230
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2231
|
+
"data-flipped": xor(vertical, reverse) ? "" : void 0,
|
|
2232
|
+
style: {
|
|
2233
|
+
"--active": active,
|
|
2234
|
+
"--inactive": inactive,
|
|
2235
|
+
"--length": cssLength(length),
|
|
2236
|
+
"--thickness": cssLength(thickness),
|
|
2237
|
+
"--percent": `${percent}%`,
|
|
2238
|
+
position: "relative",
|
|
1712
2239
|
...style
|
|
1713
2240
|
},
|
|
1714
2241
|
...props,
|
|
1715
|
-
children:
|
|
2242
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2243
|
+
name: "Slider.Track",
|
|
2244
|
+
children
|
|
2245
|
+
})
|
|
1716
2246
|
});
|
|
1717
|
-
}
|
|
2247
|
+
});
|
|
1718
2248
|
//#endregion
|
|
1719
2249
|
//#region src/components/Slider/index.tsx
|
|
1720
|
-
const defaultExternalStyles$1 = {
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
2250
|
+
const defaultExternalStyles$1 = { cursor: "pointer" };
|
|
2251
|
+
/**
|
|
2252
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
2253
|
+
* the control does not change its value.
|
|
2254
|
+
*/
|
|
2255
|
+
const WHEEL_OPTIONS$1 = { requireFocus: true };
|
|
1724
2256
|
/**
|
|
1725
2257
|
* Customizable slider
|
|
1726
2258
|
*/
|
|
1727
2259
|
const Slider = {
|
|
1728
|
-
Root: forwardRef(({ value, min, max, step = 1,
|
|
1729
|
-
const
|
|
2260
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min, max, step = 1, scale = linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
|
|
2261
|
+
const trackRef = useRef(null);
|
|
1730
2262
|
const thumbRef = useRef(null);
|
|
1731
2263
|
const externalStyles = {
|
|
1732
2264
|
...defaultExternalStyles$1,
|
|
1733
2265
|
..._externalStyles
|
|
1734
2266
|
};
|
|
1735
|
-
const
|
|
2267
|
+
const inactive = disabled || readonly;
|
|
2268
|
+
const p = toFixed(scale.normalize(value, min, max) * 100);
|
|
1736
2269
|
const rev = toFixed(100 - p);
|
|
1737
2270
|
const displayReversed = useMemo(() => xor(vertical, reverse), [vertical, reverse]);
|
|
1738
2271
|
const percent = displayReversed ? rev : p;
|
|
1739
|
-
|
|
1740
|
-
let thumbProps = {};
|
|
1741
|
-
let scaleComponent = void 0;
|
|
1742
|
-
if (children != void 0) React.Children.forEach(children, (child) => {
|
|
1743
|
-
if (React.isValidElement(child)) if (child.type == Thumb$1) thumbProps = child.props;
|
|
1744
|
-
else if (child.type == Track) trackProps = child.props;
|
|
1745
|
-
else if (child.type == Scale) scaleComponent = child;
|
|
1746
|
-
else throw new Error("only <SliderThumb> or <SliderTrack>");
|
|
1747
|
-
else throw new Error("children is an invalid element.");
|
|
1748
|
-
});
|
|
1749
|
-
const onDrag = useCallback((nx, ny) => {
|
|
1750
|
-
if (!onChange || readonly) return;
|
|
1751
|
-
const n = vertical ? ny : nx;
|
|
1752
|
-
onChange(clamp(stepValue(rawValue(displayReversed ? 1 - n : n, min, max, skew), step), min, max));
|
|
1753
|
-
}, [
|
|
1754
|
-
displayReversed,
|
|
1755
|
-
max,
|
|
2272
|
+
const axis = useMemo(() => ({
|
|
1756
2273
|
min,
|
|
1757
|
-
onChange,
|
|
1758
|
-
readonly,
|
|
1759
|
-
skew,
|
|
1760
|
-
step,
|
|
1761
|
-
vertical
|
|
1762
|
-
]);
|
|
1763
|
-
const updateValueByEvent = useCallback((eventType, x) => {
|
|
1764
|
-
let newValue;
|
|
1765
|
-
if (eventType == "normalized") newValue = rawValue(normalizeValue(value, min, max, skew) + x, min, max, skew);
|
|
1766
|
-
else newValue = value + x;
|
|
1767
|
-
return clamp(stepValue(newValue, step), min, max);
|
|
1768
|
-
}, [
|
|
1769
2274
|
max,
|
|
2275
|
+
step,
|
|
2276
|
+
scale,
|
|
2277
|
+
reverse: displayReversed
|
|
2278
|
+
}), [
|
|
1770
2279
|
min,
|
|
1771
|
-
|
|
2280
|
+
max,
|
|
1772
2281
|
step,
|
|
1773
|
-
|
|
2282
|
+
scale,
|
|
2283
|
+
displayReversed
|
|
1774
2284
|
]);
|
|
2285
|
+
useCheckSteps({
|
|
2286
|
+
component: "Slider",
|
|
2287
|
+
range: axis,
|
|
2288
|
+
keyboard,
|
|
2289
|
+
wheel
|
|
2290
|
+
});
|
|
1775
2291
|
const handleKeyDown = useCallback((event) => {
|
|
1776
|
-
if (!keyboard || !onChange || readonly) return;
|
|
1777
2292
|
const key = event.key;
|
|
1778
|
-
if ([
|
|
2293
|
+
if (![
|
|
1779
2294
|
"ArrowRight",
|
|
1780
2295
|
"ArrowLeft",
|
|
1781
2296
|
"ArrowUp",
|
|
1782
2297
|
"ArrowDown"
|
|
1783
|
-
].includes(key))
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
2298
|
+
].includes(key)) return;
|
|
2299
|
+
event.preventDefault();
|
|
2300
|
+
if (!keyboard || !onChange || inactive) return;
|
|
2301
|
+
let direction = key === "ArrowRight" || key === "ArrowUp" ? 1 : -1;
|
|
2302
|
+
if (reverse) direction *= -1;
|
|
2303
|
+
onChange(applyDelta(value, direction, keyboard, axis, event));
|
|
1789
2304
|
}, [
|
|
1790
2305
|
keyboard,
|
|
1791
2306
|
onChange,
|
|
1792
|
-
|
|
2307
|
+
inactive,
|
|
1793
2308
|
reverse,
|
|
1794
|
-
|
|
2309
|
+
value,
|
|
2310
|
+
axis
|
|
1795
2311
|
]);
|
|
1796
|
-
const
|
|
1797
|
-
|
|
1798
|
-
|
|
2312
|
+
const valueOf = (v) => v[vertical ? 1 : 0];
|
|
2313
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
2314
|
+
axis,
|
|
2315
|
+
baseElementRef: trackRef,
|
|
2316
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2317
|
+
shouldStart: () => !inactive,
|
|
2318
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1799
2319
|
updateOnPointerDown: true,
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
2320
|
+
onChange: (v) => {
|
|
2321
|
+
if (inactive) return;
|
|
2322
|
+
onChange?.(valueOf(v));
|
|
2323
|
+
},
|
|
2324
|
+
onDragStart: (v) => {
|
|
2325
|
+
if (inactive) return;
|
|
1804
2326
|
thumbRef.current?.focus();
|
|
1805
|
-
onDragStart?.(
|
|
2327
|
+
onDragStart?.(valueOf(v));
|
|
1806
2328
|
},
|
|
1807
|
-
onDragEnd: (
|
|
1808
|
-
if (
|
|
1809
|
-
|
|
1810
|
-
onDragEnd?.(clamp(stepValue(rawValue(vertical ? ny : nx, min, max, skew), step), min, max));
|
|
2329
|
+
onDragEnd: (v) => {
|
|
2330
|
+
if (inactive) return;
|
|
2331
|
+
onDragEnd?.(valueOf(v));
|
|
1811
2332
|
}
|
|
1812
2333
|
});
|
|
1813
2334
|
const wheelRefCallback = useWheel((event) => {
|
|
1814
|
-
if (!wheel || !onChange ||
|
|
2335
|
+
if (!wheel || !onChange || inactive) return;
|
|
1815
2336
|
event.preventDefault();
|
|
1816
|
-
let
|
|
1817
|
-
if (!vertical && event.deltaX
|
|
2337
|
+
let direction;
|
|
2338
|
+
if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
1818
2339
|
else {
|
|
1819
|
-
if (event.deltaY
|
|
1820
|
-
|
|
2340
|
+
if (event.deltaY === 0) return;
|
|
2341
|
+
direction = event.deltaY > 0 ? -1 : 1;
|
|
1821
2342
|
}
|
|
1822
|
-
if (
|
|
1823
|
-
onChange(
|
|
1824
|
-
});
|
|
2343
|
+
if (reverse) direction *= -1;
|
|
2344
|
+
onChange(applyDelta(value, direction, wheel, axis, event));
|
|
2345
|
+
}, WHEEL_OPTIONS$1);
|
|
2346
|
+
const rootRefCallback = useComposedRefs(dragRefCallback, wheelRefCallback);
|
|
2347
|
+
const context = useMemo(() => ({
|
|
2348
|
+
value,
|
|
2349
|
+
min,
|
|
2350
|
+
max,
|
|
2351
|
+
step,
|
|
2352
|
+
scale,
|
|
2353
|
+
vertical,
|
|
2354
|
+
reverse,
|
|
2355
|
+
disabled,
|
|
2356
|
+
readonly,
|
|
2357
|
+
onChange,
|
|
2358
|
+
percent,
|
|
2359
|
+
trackRef,
|
|
2360
|
+
thumbRef
|
|
2361
|
+
}), [
|
|
2362
|
+
value,
|
|
2363
|
+
min,
|
|
2364
|
+
max,
|
|
2365
|
+
step,
|
|
2366
|
+
scale,
|
|
2367
|
+
vertical,
|
|
2368
|
+
reverse,
|
|
2369
|
+
disabled,
|
|
2370
|
+
readonly,
|
|
2371
|
+
onChange,
|
|
2372
|
+
percent
|
|
2373
|
+
]);
|
|
1825
2374
|
useImperativeHandle(forwardedRef, () => {
|
|
1826
2375
|
return {
|
|
1827
2376
|
focus() {
|
|
1828
|
-
thumbRef.current?.focus();
|
|
2377
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1829
2378
|
},
|
|
1830
2379
|
blur() {
|
|
1831
2380
|
thumbRef.current?.blur();
|
|
1832
2381
|
}
|
|
1833
2382
|
};
|
|
1834
|
-
}, []);
|
|
2383
|
+
}, [disabled]);
|
|
1835
2384
|
return /* @__PURE__ */ jsx(SliderProvider, {
|
|
1836
|
-
|
|
1837
|
-
max,
|
|
1838
|
-
step,
|
|
1839
|
-
skew,
|
|
1840
|
-
vertical,
|
|
1841
|
-
reverse,
|
|
1842
|
-
disabled,
|
|
1843
|
-
readonly,
|
|
2385
|
+
value: context,
|
|
1844
2386
|
children: /* @__PURE__ */ jsx("div", {
|
|
1845
|
-
className
|
|
1846
|
-
ref:
|
|
1847
|
-
|
|
1848
|
-
dragRefCallback(div);
|
|
1849
|
-
},
|
|
2387
|
+
className,
|
|
2388
|
+
ref: rootRefCallback,
|
|
2389
|
+
role: "group",
|
|
1850
2390
|
tabIndex: -1,
|
|
1851
|
-
|
|
1852
|
-
"
|
|
1853
|
-
"
|
|
1854
|
-
|
|
1855
|
-
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
1856
|
-
"aria-disabled": disabled,
|
|
1857
|
-
"aria-readonly": readonly,
|
|
1858
|
-
style: {
|
|
1859
|
-
margin: `calc(${styleHelper(thumbProps?.size ?? 22)} / 2)`,
|
|
1860
|
-
...style
|
|
1861
|
-
},
|
|
2391
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2392
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2393
|
+
"data-readonly": readonly ? "" : void 0,
|
|
2394
|
+
style,
|
|
1862
2395
|
onPointerDown,
|
|
1863
2396
|
onKeyDown: (event) => {
|
|
1864
2397
|
handleKeyDown(event);
|
|
1865
2398
|
onKeyDown?.(event);
|
|
1866
2399
|
},
|
|
1867
2400
|
onFocus: (event) => {
|
|
1868
|
-
thumbRef.current?.focus();
|
|
2401
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1869
2402
|
onFocus?.(event);
|
|
1870
2403
|
},
|
|
1871
2404
|
onBlur: (event) => {
|
|
@@ -1873,304 +2406,295 @@ const Slider = {
|
|
|
1873
2406
|
onBlur?.(event);
|
|
1874
2407
|
},
|
|
1875
2408
|
...props,
|
|
1876
|
-
children
|
|
1877
|
-
style: {
|
|
1878
|
-
display: "flex",
|
|
1879
|
-
flexDirection: vertical ? "row" : "column",
|
|
1880
|
-
width: !vertical ? "100%" : void 0,
|
|
1881
|
-
height: vertical ? "100%" : void 0
|
|
1882
|
-
},
|
|
1883
|
-
children: [/* @__PURE__ */ jsx("div", {
|
|
1884
|
-
className: "tremolo-slider-track-wrapper",
|
|
1885
|
-
ref: trackElementRef,
|
|
1886
|
-
children: /* @__PURE__ */ jsx(Track, {
|
|
1887
|
-
__percent: percent,
|
|
1888
|
-
__thumb: /* @__PURE__ */ jsx(Thumb$1, {
|
|
1889
|
-
ref: thumbRef,
|
|
1890
|
-
__percent: percent,
|
|
1891
|
-
...thumbProps
|
|
1892
|
-
}),
|
|
1893
|
-
...trackProps
|
|
1894
|
-
})
|
|
1895
|
-
}), scaleComponent]
|
|
1896
|
-
})
|
|
2409
|
+
children
|
|
1897
2410
|
})
|
|
1898
2411
|
});
|
|
1899
2412
|
}),
|
|
1900
2413
|
Thumb: Thumb$1,
|
|
1901
2414
|
Track,
|
|
1902
|
-
|
|
1903
|
-
|
|
2415
|
+
Marks,
|
|
2416
|
+
MarksOption
|
|
1904
2417
|
};
|
|
1905
2418
|
//#endregion
|
|
1906
|
-
//#region src/components/
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
else if (ref !== null && ref !== void 0) ref.current = value;
|
|
1914
|
-
}
|
|
1915
|
-
/**
|
|
1916
|
-
* A utility to compose multiple refs together
|
|
1917
|
-
* Accepts callback refs and RefObject(s)
|
|
1918
|
-
*/
|
|
1919
|
-
function composeRefs(...refs) {
|
|
1920
|
-
return (node) => {
|
|
1921
|
-
let hasCleanup = false;
|
|
1922
|
-
const cleanups = refs.map((ref) => {
|
|
1923
|
-
const cleanup = setRef(ref, node);
|
|
1924
|
-
if (!hasCleanup && typeof cleanup == "function") hasCleanup = true;
|
|
1925
|
-
return cleanup;
|
|
1926
|
-
});
|
|
1927
|
-
if (hasCleanup) return () => {
|
|
1928
|
-
for (let i = 0; i < cleanups.length; i++) {
|
|
1929
|
-
const cleanup = cleanups[i];
|
|
1930
|
-
if (typeof cleanup == "function") cleanup();
|
|
1931
|
-
else setRef(refs[i], null);
|
|
1932
|
-
}
|
|
1933
|
-
};
|
|
1934
|
-
};
|
|
2419
|
+
//#region src/components/XYPad/context.tsx
|
|
2420
|
+
const XYPadContext = /* @__PURE__ */ createContext(null);
|
|
2421
|
+
const XYPadProvider = XYPadContext.Provider;
|
|
2422
|
+
function useXYPadContext(selector) {
|
|
2423
|
+
const context = useContext(XYPadContext);
|
|
2424
|
+
if (!context) throw new Error("Missing XYPadContext.Provider in the tree");
|
|
2425
|
+
return selector ? selector(context) : context;
|
|
1935
2426
|
}
|
|
1936
2427
|
//#endregion
|
|
1937
2428
|
//#region src/components/XYPad/Area.tsx
|
|
1938
|
-
function Area({ width
|
|
1939
|
-
|
|
1940
|
-
|
|
2429
|
+
const Area = /* @__PURE__ */ forwardRef(function Area({ width, height, color, children, className, style, ...props }, forwardedRef) {
|
|
2430
|
+
const { areaRef } = useXYPadContext();
|
|
2431
|
+
const composedRef = useComposedRefs(forwardedRef, areaRef);
|
|
2432
|
+
return /* @__PURE__ */ jsx("div", {
|
|
2433
|
+
ref: composedRef,
|
|
2434
|
+
className,
|
|
1941
2435
|
style: {
|
|
1942
2436
|
"--color": color,
|
|
1943
|
-
width,
|
|
1944
|
-
height,
|
|
2437
|
+
"--width": cssLength(width),
|
|
2438
|
+
"--height": cssLength(height),
|
|
2439
|
+
position: "relative",
|
|
1945
2440
|
...style
|
|
1946
2441
|
},
|
|
1947
2442
|
...props,
|
|
1948
|
-
children:
|
|
2443
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2444
|
+
name: "XYPad.Area",
|
|
2445
|
+
children
|
|
2446
|
+
})
|
|
1949
2447
|
});
|
|
1950
|
-
}
|
|
2448
|
+
});
|
|
1951
2449
|
//#endregion
|
|
1952
2450
|
//#region src/components/XYPad/Thumb.tsx
|
|
1953
|
-
const Thumb = forwardRef(({
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1966
|
-
className: clsx("tremolo-xy-pad-thumb-wrapper", wrapperClassName),
|
|
1967
|
-
style: {
|
|
1968
|
-
...__css,
|
|
1969
|
-
...wrapperStyle
|
|
2451
|
+
const Thumb = /* @__PURE__ */ forwardRef(function Thumb({ color, children, className, style, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, ...props }, forwardedRef) {
|
|
2452
|
+
const xInputRef = useRef(null);
|
|
2453
|
+
const yInputRef = useRef(null);
|
|
2454
|
+
const { value, min, max, step, disabled, readonly, onChange, percent, thumbRef } = useXYPadContext();
|
|
2455
|
+
useCheckPlacement("XYPad.Thumb", "XYPad.Area");
|
|
2456
|
+
const labels = toXY(ariaLabel);
|
|
2457
|
+
const labelledby = toXY(ariaLabelledby);
|
|
2458
|
+
const describedby = toXY(ariaDescribedby);
|
|
2459
|
+
const valueText = toXY(ariaValuetext);
|
|
2460
|
+
const methods = () => ({
|
|
2461
|
+
focus() {
|
|
2462
|
+
if (!disabled) xInputRef.current?.focus();
|
|
1970
2463
|
},
|
|
2464
|
+
blur() {
|
|
2465
|
+
xInputRef.current?.blur();
|
|
2466
|
+
yInputRef.current?.blur();
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
useImperativeHandle(forwardedRef, methods, [disabled]);
|
|
2470
|
+
useImperativeHandle(thumbRef, methods, [disabled]);
|
|
2471
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2472
|
+
className,
|
|
2473
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2474
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1971
2475
|
...props,
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
2476
|
+
style: {
|
|
2477
|
+
"--color": color,
|
|
2478
|
+
position: "absolute",
|
|
2479
|
+
translate: "var(--translate, -50% -50%)",
|
|
2480
|
+
zIndex: 100,
|
|
2481
|
+
...style,
|
|
2482
|
+
left: `${percent[0]}%`,
|
|
2483
|
+
top: `${percent[1]}%`
|
|
2484
|
+
},
|
|
2485
|
+
children: [[0, 1].map((axis) => /* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
2486
|
+
ref: axis === 0 ? xInputRef : yInputRef,
|
|
2487
|
+
"data-axis": axis === 0 ? "x" : "y",
|
|
2488
|
+
value: value[axis],
|
|
2489
|
+
min: min[axis],
|
|
2490
|
+
max: max[axis],
|
|
2491
|
+
step: step[axis],
|
|
2492
|
+
disabled,
|
|
2493
|
+
"aria-readonly": readonly,
|
|
2494
|
+
"aria-orientation": axis === 0 ? "horizontal" : "vertical",
|
|
2495
|
+
"aria-label": labels[axis] ?? (axis === 0 ? "x" : "y"),
|
|
2496
|
+
"aria-labelledby": labelledby[axis],
|
|
2497
|
+
"aria-describedby": describedby[axis],
|
|
2498
|
+
"aria-valuetext": valueText[axis],
|
|
2499
|
+
onChange: (event) => {
|
|
2500
|
+
if (readonly) {
|
|
2501
|
+
event.currentTarget.value = String(value[axis]);
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
const next = [...value];
|
|
2505
|
+
next[axis] = event.currentTarget.valueAsNumber;
|
|
2506
|
+
onChange?.(next);
|
|
1983
2507
|
}
|
|
1984
|
-
})
|
|
2508
|
+
}, axis)), children]
|
|
1985
2509
|
});
|
|
1986
2510
|
});
|
|
1987
2511
|
//#endregion
|
|
1988
2512
|
//#region src/components/XYPad/index.tsx
|
|
1989
|
-
const
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
};
|
|
1996
|
-
const defaultExternalStyles = {
|
|
1997
|
-
userSelectNone: true,
|
|
1998
|
-
cursor: "pointer"
|
|
1999
|
-
};
|
|
2513
|
+
const defaultExternalStyles = { cursor: "pointer" };
|
|
2514
|
+
/**
|
|
2515
|
+
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
2516
|
+
* the control does not change its value.
|
|
2517
|
+
*/
|
|
2518
|
+
const WHEEL_OPTIONS = { requireFocus: true };
|
|
2000
2519
|
/**
|
|
2001
2520
|
* Simple XYPad
|
|
2002
2521
|
*/
|
|
2003
2522
|
const XYPad = {
|
|
2004
|
-
Root: forwardRef(({
|
|
2005
|
-
const x = useMemo(() => {
|
|
2006
|
-
return {
|
|
2007
|
-
...defaultValueOptions,
|
|
2008
|
-
..._x
|
|
2009
|
-
};
|
|
2010
|
-
}, [_x]);
|
|
2011
|
-
const y = useMemo(() => {
|
|
2012
|
-
return {
|
|
2013
|
-
...defaultValueOptions,
|
|
2014
|
-
..._y
|
|
2015
|
-
};
|
|
2016
|
-
}, [_y]);
|
|
2523
|
+
Root: /* @__PURE__ */ forwardRef(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = linearScale, reverse: _reverse = false, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
|
|
2017
2524
|
const rootRef = useRef(null);
|
|
2018
|
-
const
|
|
2525
|
+
const areaRef = useRef(null);
|
|
2019
2526
|
const thumbRef = useRef(null);
|
|
2020
2527
|
const externalStyles = {
|
|
2021
2528
|
...defaultExternalStyles,
|
|
2022
2529
|
..._externalStyles
|
|
2023
2530
|
};
|
|
2024
|
-
const
|
|
2025
|
-
const
|
|
2026
|
-
const
|
|
2027
|
-
const
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
else throw new Error("only <XYPadThumb> or <XYPadArea>");
|
|
2034
|
-
else throw new Error("children is an invalid element.");
|
|
2035
|
-
});
|
|
2036
|
-
const onDrag = useCallback((nx, ny) => {
|
|
2037
|
-
if (!onChange || readonly) return;
|
|
2038
|
-
const vx = rawValue(x.reverse ? 1 - nx : nx, x.min, x.max, x.skew);
|
|
2039
|
-
const vy = rawValue(y.reverse ? 1 - ny : ny, y.min, y.max, y.skew);
|
|
2040
|
-
onChange(clamp(stepValue(vx, x.step), x.min, x.max), clamp(stepValue(vy, y.step), y.min, y.max));
|
|
2531
|
+
const inactive = disabled || readonly;
|
|
2532
|
+
const min = useMemo(() => toXY(_min), [_min]);
|
|
2533
|
+
const max = useMemo(() => toXY(_max), [_max]);
|
|
2534
|
+
const step = useMemo(() => toXY(_step), [_step]);
|
|
2535
|
+
const scale = useMemo(() => toXY(_scale), [_scale]);
|
|
2536
|
+
const reverse = useMemo(() => toXY(_reverse), [_reverse]);
|
|
2537
|
+
const percent = useMemo(() => {
|
|
2538
|
+
const normalized = [0, 1].map((i) => scale[i].normalize(value[i], min[i], max[i]));
|
|
2539
|
+
return [0, 1].map((i) => toFixed((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100));
|
|
2041
2540
|
}, [
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2541
|
+
value,
|
|
2542
|
+
min,
|
|
2543
|
+
max,
|
|
2544
|
+
scale,
|
|
2545
|
+
reverse
|
|
2546
|
+
]);
|
|
2547
|
+
const axis = useMemo(() => [0, 1].map((i) => ({
|
|
2548
|
+
min: min[i],
|
|
2549
|
+
max: max[i],
|
|
2550
|
+
step: step[i],
|
|
2551
|
+
scale: scale[i],
|
|
2552
|
+
reverse: reverse[i]
|
|
2553
|
+
})), [
|
|
2554
|
+
min,
|
|
2555
|
+
max,
|
|
2556
|
+
step,
|
|
2557
|
+
scale,
|
|
2558
|
+
reverse
|
|
2559
|
+
]);
|
|
2560
|
+
useCheckSteps({
|
|
2561
|
+
component: "XYPad",
|
|
2562
|
+
axis: "x",
|
|
2563
|
+
range: axis[0],
|
|
2564
|
+
keyboard,
|
|
2565
|
+
wheel
|
|
2566
|
+
});
|
|
2567
|
+
useCheckSteps({
|
|
2568
|
+
component: "XYPad",
|
|
2569
|
+
axis: "y",
|
|
2570
|
+
range: axis[1],
|
|
2571
|
+
keyboard,
|
|
2572
|
+
wheel
|
|
2573
|
+
});
|
|
2574
|
+
const withAxis = useCallback((axis, next) => axis === 0 ? [next, value[1]] : [value[0], next], [value]);
|
|
2575
|
+
/** @param i 0 = x, 1 = y */
|
|
2576
|
+
const nudge = useCallback((i, direction, option, modifiers) => withAxis(i, applyDelta(value[i], direction, option, axis[i], modifiers)), [
|
|
2577
|
+
value,
|
|
2578
|
+
axis,
|
|
2579
|
+
withAxis
|
|
2054
2580
|
]);
|
|
2055
|
-
const updateValueByEvent = useCallback((eventType, target, x) => {
|
|
2056
|
-
let v;
|
|
2057
|
-
if (eventType == "normalized") v = rawValue(normalizeValue(target.value, target.min, target.max, target.skew) + x, target.min, target.max, target.skew);
|
|
2058
|
-
else v = target.value + x;
|
|
2059
|
-
return clamp(stepValue(v, target.step), target.min, target.max);
|
|
2060
|
-
}, []);
|
|
2061
2581
|
const handleKeyDown = useCallback((event) => {
|
|
2062
|
-
if (!onChange || readonly) return;
|
|
2063
2582
|
const key = event.key;
|
|
2064
|
-
if ([
|
|
2583
|
+
if (![
|
|
2065
2584
|
"ArrowRight",
|
|
2066
2585
|
"ArrowLeft",
|
|
2067
2586
|
"ArrowUp",
|
|
2068
2587
|
"ArrowDown"
|
|
2069
|
-
].includes(key))
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
const newValue = updateValueByEvent(target.keyboard[0], target, delta);
|
|
2078
|
-
if (isHorizontal) onChange(newValue, y.value);
|
|
2079
|
-
else onChange(x.value, newValue);
|
|
2080
|
-
}
|
|
2588
|
+
].includes(key)) return;
|
|
2589
|
+
const i = key === "ArrowRight" || key === "ArrowLeft" ? 0 : 1;
|
|
2590
|
+
event.preventDefault();
|
|
2591
|
+
if (!onChange || inactive || !keyboard) return;
|
|
2592
|
+
let direction = 1;
|
|
2593
|
+
if (key === "ArrowLeft" || key === "ArrowUp") direction *= -1;
|
|
2594
|
+
if (reverse[i]) direction *= -1;
|
|
2595
|
+
onChange(nudge(i, direction, keyboard, event));
|
|
2081
2596
|
}, [
|
|
2082
2597
|
onChange,
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2598
|
+
inactive,
|
|
2599
|
+
keyboard,
|
|
2600
|
+
reverse,
|
|
2601
|
+
nudge
|
|
2087
2602
|
]);
|
|
2088
|
-
const { refCallback: dragRefCallback } =
|
|
2089
|
-
|
|
2090
|
-
|
|
2603
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
2604
|
+
axis,
|
|
2605
|
+
baseElementRef: areaRef,
|
|
2606
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
2091
2607
|
updateOnPointerDown: true,
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
if (
|
|
2608
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2609
|
+
shouldStart: () => !inactive,
|
|
2610
|
+
onChange: (v) => {
|
|
2611
|
+
if (inactive) return;
|
|
2612
|
+
onChange?.(v);
|
|
2613
|
+
},
|
|
2614
|
+
onDragStart: (v) => {
|
|
2615
|
+
if (inactive) return;
|
|
2096
2616
|
thumbRef.current?.focus();
|
|
2097
|
-
|
|
2098
|
-
const valueY = clamp(stepValue(rawValue(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
|
|
2099
|
-
onDragStart?.(valueX, valueY);
|
|
2617
|
+
onDragStart?.(v);
|
|
2100
2618
|
},
|
|
2101
|
-
onDragEnd: (
|
|
2102
|
-
if (
|
|
2103
|
-
|
|
2104
|
-
const valueX = clamp(stepValue(rawValue(nx, x.min, x.max, x.skew), x.step), x.min, x.max);
|
|
2105
|
-
const valueY = clamp(stepValue(rawValue(ny, y.min, y.max, y.skew), y.step), y.min, y.max);
|
|
2106
|
-
onDragEnd?.(valueX, valueY);
|
|
2619
|
+
onDragEnd: (v) => {
|
|
2620
|
+
if (inactive) return;
|
|
2621
|
+
onDragEnd?.(v);
|
|
2107
2622
|
}
|
|
2108
2623
|
});
|
|
2109
2624
|
const wheelRefCallback = useWheel((event) => {
|
|
2110
|
-
if (!onChange ||
|
|
2111
|
-
const
|
|
2112
|
-
|
|
2625
|
+
if (!onChange || inactive || !wheel) return;
|
|
2626
|
+
const horizontal = event.deltaX !== 0;
|
|
2627
|
+
const delta = horizontal ? event.deltaX : event.deltaY;
|
|
2628
|
+
if (delta === 0) return;
|
|
2629
|
+
const i = horizontal || event.shiftKey ? 0 : 1;
|
|
2113
2630
|
event.preventDefault();
|
|
2114
|
-
let
|
|
2115
|
-
if (
|
|
2116
|
-
if (
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2631
|
+
let direction = 1;
|
|
2632
|
+
if (delta < 0) direction *= -1;
|
|
2633
|
+
if (reverse[i]) direction *= -1;
|
|
2634
|
+
onChange(nudge(i, direction, wheel, event));
|
|
2635
|
+
}, WHEEL_OPTIONS);
|
|
2636
|
+
const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
|
|
2637
|
+
const context = useMemo(() => ({
|
|
2638
|
+
value,
|
|
2639
|
+
min,
|
|
2640
|
+
max,
|
|
2641
|
+
step,
|
|
2642
|
+
scale,
|
|
2643
|
+
reverse,
|
|
2644
|
+
disabled,
|
|
2645
|
+
readonly,
|
|
2646
|
+
onChange,
|
|
2647
|
+
percent,
|
|
2648
|
+
areaRef,
|
|
2649
|
+
thumbRef
|
|
2650
|
+
}), [
|
|
2651
|
+
value,
|
|
2652
|
+
min,
|
|
2653
|
+
max,
|
|
2654
|
+
step,
|
|
2655
|
+
scale,
|
|
2656
|
+
reverse,
|
|
2657
|
+
disabled,
|
|
2658
|
+
readonly,
|
|
2659
|
+
onChange,
|
|
2660
|
+
percent
|
|
2661
|
+
]);
|
|
2121
2662
|
useImperativeHandle(forwardedRef, () => {
|
|
2122
2663
|
return {
|
|
2123
2664
|
focus() {
|
|
2124
|
-
thumbRef.current?.focus();
|
|
2665
|
+
if (!disabled) thumbRef.current?.focus();
|
|
2125
2666
|
},
|
|
2126
2667
|
blur() {
|
|
2127
2668
|
thumbRef.current?.blur();
|
|
2128
2669
|
},
|
|
2129
2670
|
original: rootRef
|
|
2130
2671
|
};
|
|
2131
|
-
}, []);
|
|
2132
|
-
return /* @__PURE__ */ jsx(
|
|
2133
|
-
|
|
2134
|
-
ref: composeRefs(rootRef, wheelRefCallback, dragRefCallback),
|
|
2135
|
-
tabIndex: -1,
|
|
2136
|
-
"aria-disabled": disabled,
|
|
2137
|
-
"aria-readonly": readonly,
|
|
2138
|
-
style: {
|
|
2139
|
-
margin: `calc(${styleHelper(thumbProps?.size ?? 22)} / 2)`,
|
|
2140
|
-
cursor: readonly ? "not-allowed" : "pointer",
|
|
2141
|
-
WebkitTapHighlightColor: "transparent",
|
|
2142
|
-
...style
|
|
2143
|
-
},
|
|
2144
|
-
onPointerDown,
|
|
2145
|
-
onKeyDown: (event) => {
|
|
2146
|
-
handleKeyDown(event);
|
|
2147
|
-
onKeyDown?.(event);
|
|
2148
|
-
},
|
|
2149
|
-
onFocus: (event) => {
|
|
2150
|
-
thumbRef.current?.focus();
|
|
2151
|
-
onFocus?.(event);
|
|
2152
|
-
},
|
|
2153
|
-
onBlur: (event) => {
|
|
2154
|
-
thumbRef.current?.blur();
|
|
2155
|
-
onBlur?.(event);
|
|
2156
|
-
},
|
|
2157
|
-
...props,
|
|
2672
|
+
}, [disabled]);
|
|
2673
|
+
return /* @__PURE__ */ jsx(XYPadProvider, {
|
|
2674
|
+
value: context,
|
|
2158
2675
|
children: /* @__PURE__ */ jsx("div", {
|
|
2159
|
-
className
|
|
2160
|
-
ref:
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2676
|
+
className,
|
|
2677
|
+
ref: rootRefCallback,
|
|
2678
|
+
role: "group",
|
|
2679
|
+
tabIndex: -1,
|
|
2680
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2681
|
+
"data-readonly": readonly ? "" : void 0,
|
|
2682
|
+
style,
|
|
2683
|
+
onPointerDown,
|
|
2684
|
+
onKeyDown: (event) => {
|
|
2685
|
+
handleKeyDown(event);
|
|
2686
|
+
onKeyDown?.(event);
|
|
2687
|
+
},
|
|
2688
|
+
onFocus: (event) => {
|
|
2689
|
+
if (!disabled) thumbRef.current?.focus();
|
|
2690
|
+
onFocus?.(event);
|
|
2691
|
+
},
|
|
2692
|
+
onBlur: (event) => {
|
|
2693
|
+
thumbRef.current?.blur();
|
|
2694
|
+
onBlur?.(event);
|
|
2695
|
+
},
|
|
2696
|
+
...props,
|
|
2697
|
+
children
|
|
2174
2698
|
})
|
|
2175
2699
|
});
|
|
2176
2700
|
}),
|
|
@@ -2181,67 +2705,153 @@ const XYPad = {
|
|
|
2181
2705
|
//#region src/hooks/useAnimationFrame.ts
|
|
2182
2706
|
function useAnimationFrame(callback = () => {}, deps = []) {
|
|
2183
2707
|
const reqIdRef = useRef(-1);
|
|
2184
|
-
const
|
|
2185
|
-
reqIdRef.current = requestAnimationFrame(loop);
|
|
2186
|
-
callback();
|
|
2187
|
-
}, [callback, ...deps]);
|
|
2708
|
+
const runCallback = useCallbackRef(callback);
|
|
2188
2709
|
useEffect(() => {
|
|
2710
|
+
const loop = () => {
|
|
2711
|
+
reqIdRef.current = requestAnimationFrame(loop);
|
|
2712
|
+
runCallback();
|
|
2713
|
+
};
|
|
2189
2714
|
reqIdRef.current = requestAnimationFrame(loop);
|
|
2190
2715
|
return () => cancelAnimationFrame(reqIdRef.current);
|
|
2191
|
-
}, [
|
|
2716
|
+
}, [runCallback, ...deps]);
|
|
2192
2717
|
}
|
|
2193
2718
|
//#endregion
|
|
2194
2719
|
//#region src/hooks/useMIDIAccess.ts
|
|
2720
|
+
const INITIAL_STATE = {
|
|
2721
|
+
midiAccess: null,
|
|
2722
|
+
error: null,
|
|
2723
|
+
inputs: []
|
|
2724
|
+
};
|
|
2725
|
+
function createMIDIAccessStore() {
|
|
2726
|
+
let instance = null;
|
|
2727
|
+
let unsubscribe = null;
|
|
2728
|
+
let state = INITIAL_STATE;
|
|
2729
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
2730
|
+
function emit() {
|
|
2731
|
+
for (const listener of listeners) listener();
|
|
2732
|
+
}
|
|
2733
|
+
function connect() {
|
|
2734
|
+
const nextInstance = createMIDIAccess();
|
|
2735
|
+
instance = nextInstance;
|
|
2736
|
+
state = nextInstance.getState();
|
|
2737
|
+
unsubscribe = nextInstance.subscribe(() => {
|
|
2738
|
+
state = nextInstance.getState();
|
|
2739
|
+
emit();
|
|
2740
|
+
});
|
|
2741
|
+
emit();
|
|
2742
|
+
return nextInstance;
|
|
2743
|
+
}
|
|
2744
|
+
function disconnect(currentInstance) {
|
|
2745
|
+
if (instance !== currentInstance) return;
|
|
2746
|
+
unsubscribe?.();
|
|
2747
|
+
unsubscribe = null;
|
|
2748
|
+
instance = null;
|
|
2749
|
+
state = INITIAL_STATE;
|
|
2750
|
+
currentInstance.destroy();
|
|
2751
|
+
emit();
|
|
2752
|
+
}
|
|
2753
|
+
function request(options) {
|
|
2754
|
+
instance?.request(options);
|
|
2755
|
+
}
|
|
2756
|
+
return {
|
|
2757
|
+
connect,
|
|
2758
|
+
disconnect,
|
|
2759
|
+
request,
|
|
2760
|
+
getState: () => state,
|
|
2761
|
+
getServerState: () => INITIAL_STATE,
|
|
2762
|
+
subscribe: (listener) => {
|
|
2763
|
+
listeners.add(listener);
|
|
2764
|
+
return () => listeners.delete(listener);
|
|
2765
|
+
}
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2195
2768
|
/**
|
|
2196
2769
|
* Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
|
|
2770
|
+
*
|
|
2771
|
+
* `inputs` follows the devices: it changes as one is plugged in or unplugged,
|
|
2772
|
+
* so a list of devices needs no `statechange` listener of its own.
|
|
2773
|
+
*
|
|
2774
|
+
* `request` takes `{ sysex: true }` when the app needs system exclusive
|
|
2775
|
+
* messages. Browsers treat that as a separate, more sensitive permission, so
|
|
2776
|
+
* ask for it only when it is actually used.
|
|
2197
2777
|
*/
|
|
2198
2778
|
function useMIDIAccess(requestOnMount = true) {
|
|
2199
|
-
const
|
|
2200
|
-
const { midiAccess, error } = useSyncExternalStore(
|
|
2779
|
+
const store = useMemo(() => createMIDIAccessStore(), []);
|
|
2780
|
+
const { midiAccess, error, inputs } = useSyncExternalStore(store.subscribe, store.getState, store.getServerState);
|
|
2201
2781
|
useEffect(() => {
|
|
2202
|
-
|
|
2203
|
-
|
|
2782
|
+
const instance = store.connect();
|
|
2783
|
+
return () => store.disconnect(instance);
|
|
2784
|
+
}, [store]);
|
|
2204
2785
|
useEffect(() => {
|
|
2205
|
-
|
|
2206
|
-
}, [
|
|
2786
|
+
if (requestOnMount) store.request();
|
|
2787
|
+
}, [store, requestOnMount]);
|
|
2207
2788
|
return {
|
|
2208
|
-
request:
|
|
2789
|
+
request: store.request,
|
|
2209
2790
|
midiAccess,
|
|
2210
|
-
error
|
|
2791
|
+
error,
|
|
2792
|
+
inputs
|
|
2211
2793
|
};
|
|
2212
2794
|
}
|
|
2213
2795
|
//#endregion
|
|
2214
2796
|
//#region src/hooks/useMIDIInput.ts
|
|
2215
2797
|
/**
|
|
2216
|
-
*
|
|
2798
|
+
* Handle MIDI input events. To be used with {@link useMIDIAccess}.
|
|
2799
|
+
*
|
|
2800
|
+
* The handlers are read fresh on every event, so writing them inline is fine:
|
|
2801
|
+
* the listeners are attached once per `midiAccess` and stay attached. Devices
|
|
2802
|
+
* plugged in later are picked up without anything having to be rebuilt.
|
|
2803
|
+
*
|
|
2804
|
+
* @example
|
|
2805
|
+
* const { midiAccess } = useMIDIAccess()
|
|
2806
|
+
* useMIDIInput(midiAccess, {
|
|
2807
|
+
* onNoteOnEvent: (note, velocity) => play(note, velocity / 127),
|
|
2808
|
+
* onNoteOffEvent: (note) => stop(note),
|
|
2809
|
+
* onControlChangeEvent: (controller, value) => {
|
|
2810
|
+
* if (controller === 1) setModulation(value / 127)
|
|
2811
|
+
* },
|
|
2812
|
+
* })
|
|
2217
2813
|
*/
|
|
2218
|
-
function useMIDIInput(midiAccess,
|
|
2814
|
+
function useMIDIInput(midiAccess, handlers) {
|
|
2815
|
+
const latest = useRef(handlers);
|
|
2816
|
+
const instanceRef = useRef(null);
|
|
2219
2817
|
useEffect(() => {
|
|
2220
|
-
const instance = createMIDIInput(midiAccess,
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
}, [
|
|
2227
|
-
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
]);
|
|
2818
|
+
const instance = createMIDIInput(midiAccess, latest.current);
|
|
2819
|
+
instanceRef.current = instance;
|
|
2820
|
+
return () => {
|
|
2821
|
+
instanceRef.current = null;
|
|
2822
|
+
instance.destroy();
|
|
2823
|
+
};
|
|
2824
|
+
}, [midiAccess]);
|
|
2825
|
+
useEffect(() => {
|
|
2826
|
+
latest.current = handlers;
|
|
2827
|
+
instanceRef.current?.update(handlers);
|
|
2828
|
+
});
|
|
2232
2829
|
}
|
|
2233
2830
|
//#endregion
|
|
2234
2831
|
//#region src/hooks/useMIDIMessage.ts
|
|
2235
2832
|
/**
|
|
2236
2833
|
* Hooks for when you want to process MIDI events in more detail than useMIDIInput.
|
|
2834
|
+
*
|
|
2835
|
+
* The handler is read fresh on every event, so writing it inline is fine: the
|
|
2836
|
+
* listeners are attached once per `midiAccess` and stay attached, and devices
|
|
2837
|
+
* plugged in later are picked up.
|
|
2237
2838
|
*/
|
|
2238
2839
|
function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
2840
|
+
const latest = useRef(onMIDIMessage);
|
|
2841
|
+
const instanceRef = useRef(null);
|
|
2239
2842
|
useEffect(() => {
|
|
2240
|
-
const instance = createMIDIMessage(midiAccess,
|
|
2241
|
-
|
|
2242
|
-
|
|
2843
|
+
const instance = createMIDIMessage(midiAccess, (event) => latest.current(event));
|
|
2844
|
+
instanceRef.current = instance;
|
|
2845
|
+
return () => {
|
|
2846
|
+
instanceRef.current = null;
|
|
2847
|
+
instance.destroy();
|
|
2848
|
+
};
|
|
2849
|
+
}, [midiAccess]);
|
|
2850
|
+
useEffect(() => {
|
|
2851
|
+
latest.current = onMIDIMessage;
|
|
2852
|
+
});
|
|
2243
2853
|
}
|
|
2244
2854
|
//#endregion
|
|
2245
|
-
export { AnimationCanvas, Knob, NOT_SUPPORTED, NumberInput, PERMISSION_DENIED, Piano, PointsEditor, SHORTCUTS, Slider, XYPad, clampPoint,
|
|
2855
|
+
export { AnimationCanvas, DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, Knob, NOT_SUPPORTED, NumberInput, PERMISSION_DENIED, PITCH_BEND_CENTER, Piano, PointsEditor, SHORTCUTS, Slider, UNAVAILABLE, XYPad, clampPoint, useAnimationFrame, useDrag, useDragValue, useEventListener, useInterval, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
|
|
2246
2856
|
|
|
2247
2857
|
//# sourceMappingURL=index.js.map
|