@tremolo-ui/react 0.5.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 +1468 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1103 -435
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1103 -435
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1457 -534
- package/dist/index.js.map +1 -1
- package/package.json +28 -26
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/Knob/ActiveLine.tsx +7 -6
- package/src/components/Knob/InactiveLine.tsx +9 -10
- package/src/components/Knob/SVGRoot.tsx +5 -3
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +17 -1
- package/src/components/Knob/index.tsx +148 -55
- package/src/components/NumberInput/DecrementStepper.tsx +8 -4
- package/src/components/NumberInput/IncrementStepper.tsx +9 -4
- package/src/components/NumberInput/InputField.tsx +188 -105
- package/src/components/NumberInput/Stepper.tsx +123 -72
- package/src/components/NumberInput/{stepperButton.tsx → StepperButton.tsx} +27 -22
- package/src/components/NumberInput/context.tsx +35 -8
- package/src/components/NumberInput/index.tsx +272 -75
- package/src/components/Piano/index.tsx +354 -188
- package/src/components/PointsEditor/Background.tsx +11 -2
- package/src/components/PointsEditor/Container.tsx +87 -27
- package/src/components/PointsEditor/Point.tsx +239 -79
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -39
- package/src/components/Slider/Marks.tsx +48 -16
- package/src/components/Slider/MarksOption.tsx +57 -43
- package/src/components/Slider/Thumb.tsx +108 -50
- package/src/components/Slider/Track.tsx +69 -58
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +131 -65
- package/src/components/Slider/type.ts +26 -8
- package/src/components/XYPad/Area.tsx +45 -35
- package/src/components/XYPad/Thumb.tsx +135 -62
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +180 -48
- 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/_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 +38 -8
- package/src/hooks/useDragValue.ts +54 -9
- package/src/hooks/useEventListener.ts +10 -6
- 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/index.ts +38 -21
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -501
- package/src/Globals.d.ts +0 -3
- package/src/components/Knob/index.css +0 -81
- package/src/components/NumberInput/index.css +0 -121
- package/src/components/Piano/index.css +0 -90
- package/src/components/PointsEditor/index.css +0 -35
- package/src/components/Slider/index.css +0 -125
- package/src/components/XYPad/index.css +0 -60
- package/src/components/_util/index.ts +0 -28
- package/src/styles/global.css +0 -19
- /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { createContext, forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useInsertionEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
import { NOT_SUPPORTED, PERMISSION_DENIED, createAnimationCanvas, createDrag, createDragValue, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createWheel, elementMapping, relativeMapping, toXY } from "@tremolo-ui/dom";
|
|
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";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import { applyDelta, blackKeyWidth, clamp, decimalPart,
|
|
5
|
+
import { applyDelta, blackKeyWidth, clamp, decimalPart, getNoteRangeArray, isWhiteKey, linearScale, mapModifier, noteKey, notePosition, pianoWidth, radian, selectInputEvent, selectModifier, toFixed, toPrecision, xor } from "@tremolo-ui/functions";
|
|
6
6
|
//#region src/components/AnimationCanvas/index.tsx
|
|
7
|
-
|
|
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) {
|
|
8
11
|
const [node, setNode] = useState(null);
|
|
12
|
+
const composedRef = useComposedRefs(forwardedRef, setNode);
|
|
9
13
|
const latest = useRef({
|
|
10
14
|
draw,
|
|
11
15
|
init,
|
|
@@ -16,30 +20,6 @@ function AnimationCanvas({ draw, init, animate = true, options, width = 100, hei
|
|
|
16
20
|
options
|
|
17
21
|
});
|
|
18
22
|
const instanceRef = useRef(null);
|
|
19
|
-
/** Set while the instance below is newer than the effect that pushes into it. */
|
|
20
|
-
const justCreated = useRef(false);
|
|
21
|
-
useEffect(() => {
|
|
22
|
-
if (!node) return;
|
|
23
|
-
const current = latest.current;
|
|
24
|
-
const instance = createAnimationCanvas(node, {
|
|
25
|
-
draw: (context, frame) => latest.current.draw(context, frame),
|
|
26
|
-
init: (context, size) => latest.current.init?.(context, size),
|
|
27
|
-
animate: current.animate,
|
|
28
|
-
size: {
|
|
29
|
-
width: current.width,
|
|
30
|
-
height: current.height
|
|
31
|
-
},
|
|
32
|
-
reduceFlickering: current.reduceFlickering,
|
|
33
|
-
relativeSize,
|
|
34
|
-
contextAttributes: current.options
|
|
35
|
-
});
|
|
36
|
-
instanceRef.current = instance;
|
|
37
|
-
justCreated.current = true;
|
|
38
|
-
return () => {
|
|
39
|
-
instanceRef.current = null;
|
|
40
|
-
instance.destroy();
|
|
41
|
-
};
|
|
42
|
-
}, [node, relativeSize]);
|
|
43
23
|
useEffect(() => {
|
|
44
24
|
latest.current = {
|
|
45
25
|
draw,
|
|
@@ -50,10 +30,6 @@ function AnimationCanvas({ draw, init, animate = true, options, width = 100, hei
|
|
|
50
30
|
reduceFlickering,
|
|
51
31
|
options
|
|
52
32
|
};
|
|
53
|
-
if (justCreated.current) {
|
|
54
|
-
justCreated.current = false;
|
|
55
|
-
return;
|
|
56
|
-
}
|
|
57
33
|
instanceRef.current?.update({
|
|
58
34
|
animate,
|
|
59
35
|
size: {
|
|
@@ -63,12 +39,150 @@ function AnimationCanvas({ draw, init, animate = true, options, width = 100, hei
|
|
|
63
39
|
reduceFlickering
|
|
64
40
|
});
|
|
65
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();
|
|
61
|
+
};
|
|
62
|
+
}, [node, resizable]);
|
|
66
63
|
return /* @__PURE__ */ jsx("canvas", {
|
|
67
|
-
className
|
|
68
|
-
ref:
|
|
64
|
+
className,
|
|
65
|
+
ref: composedRef,
|
|
69
66
|
onContextMenu,
|
|
70
67
|
...props
|
|
71
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
|
+
]);
|
|
72
186
|
}
|
|
73
187
|
//#endregion
|
|
74
188
|
//#region src/hooks/_internal/useCallbackRef.ts
|
|
@@ -93,7 +207,7 @@ function useCallbackRef(callback, deps = []) {
|
|
|
93
207
|
* @returns a ref callback for the element that starts the drag, and whether a drag is in progress
|
|
94
208
|
*/
|
|
95
209
|
function useDragValue(options) {
|
|
96
|
-
const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor } = options;
|
|
210
|
+
const { axis, baseElementRef, pixelRange, updateOnPointerDown, threshold, cursor, pointerLock } = options;
|
|
97
211
|
if (!baseElementRef && !options.getValue) throw new Error("useDragValue: give either baseElementRef or getValue, so that the drag has something to move");
|
|
98
212
|
const [dragging, setDragging] = useState(false);
|
|
99
213
|
const valueGetter = useCallbackRef(options.getValue);
|
|
@@ -108,19 +222,24 @@ function useDragValue(options) {
|
|
|
108
222
|
if (!node) return;
|
|
109
223
|
const instance = createDragValue(node, {
|
|
110
224
|
axis: latest.current.axis,
|
|
111
|
-
mapping: baseElementRef ? elementMapping(() => baseElementRef.current
|
|
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
|
+
}),
|
|
112
229
|
getValue: () => valueGetter() ?? [0, 0],
|
|
113
230
|
updateOnPointerDown: latest.current.updateOnPointerDown,
|
|
114
231
|
threshold: latest.current.threshold,
|
|
115
232
|
cursor: latest.current.cursor,
|
|
116
|
-
|
|
117
|
-
|
|
233
|
+
pointerLock: latest.current.pointerLock,
|
|
234
|
+
shouldStart: (event) => latest.current.shouldStart?.(event) ?? true,
|
|
235
|
+
onChange: (value, state) => changeHandler(value, state),
|
|
236
|
+
onDragStart: (value, state) => {
|
|
118
237
|
setDragging(true);
|
|
119
|
-
dragStartHandler(value);
|
|
238
|
+
dragStartHandler(value, state);
|
|
120
239
|
},
|
|
121
|
-
onDragEnd: (value) => {
|
|
240
|
+
onDragEnd: (value, state) => {
|
|
122
241
|
setDragging(false);
|
|
123
|
-
dragEndHandler(value);
|
|
242
|
+
dragEndHandler(value, state);
|
|
124
243
|
}
|
|
125
244
|
});
|
|
126
245
|
instanceRef.current = instance;
|
|
@@ -144,7 +263,8 @@ function useDragValue(options) {
|
|
|
144
263
|
axis,
|
|
145
264
|
updateOnPointerDown,
|
|
146
265
|
threshold,
|
|
147
|
-
cursor
|
|
266
|
+
cursor,
|
|
267
|
+
pointerLock
|
|
148
268
|
});
|
|
149
269
|
});
|
|
150
270
|
return {
|
|
@@ -178,50 +298,89 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
|
178
298
|
return setNode;
|
|
179
299
|
}
|
|
180
300
|
//#endregion
|
|
181
|
-
//#region src/
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
+
};
|
|
188
330
|
//#endregion
|
|
189
|
-
//#region src/components/_util/
|
|
331
|
+
//#region src/components/_util/css-length.ts
|
|
190
332
|
/**
|
|
191
|
-
*
|
|
192
|
-
*
|
|
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.
|
|
193
339
|
*/
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
else if (ref !== null && ref !== void 0) ref.current = value;
|
|
340
|
+
function cssLength(value) {
|
|
341
|
+
return typeof value === "number" ? `${value}px` : value;
|
|
197
342
|
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/components/_util/Placement.tsx
|
|
198
345
|
/**
|
|
199
|
-
*
|
|
200
|
-
*
|
|
346
|
+
* The subcomponent that established the current placement, if any.
|
|
347
|
+
*
|
|
348
|
+
* @internal
|
|
201
349
|
*/
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (typeof cleanup == "function") cleanup();
|
|
214
|
-
else setRef(refs[i], null);
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
};
|
|
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
|
+
});
|
|
218
361
|
}
|
|
219
362
|
/**
|
|
220
|
-
*
|
|
221
|
-
*
|
|
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
|
|
222
372
|
*/
|
|
223
|
-
function
|
|
224
|
-
|
|
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
|
+
]);
|
|
225
384
|
}
|
|
226
385
|
/**
|
|
227
386
|
* 角度から円弧上の点を求める。
|
|
@@ -240,6 +399,19 @@ function arcRadius(strokeWidth) {
|
|
|
240
399
|
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
241
400
|
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
242
401
|
}
|
|
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;
|
|
414
|
+
}
|
|
243
415
|
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
244
416
|
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
245
417
|
const p = scale.normalize(value, min, max);
|
|
@@ -253,7 +425,7 @@ function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
|
253
425
|
r4: angleRange / 2
|
|
254
426
|
};
|
|
255
427
|
}
|
|
256
|
-
const KnobContext = createContext(null);
|
|
428
|
+
const KnobContext = /* @__PURE__ */ createContext(null);
|
|
257
429
|
const KnobProvider = KnobContext.Provider;
|
|
258
430
|
function useKnobContext(selector) {
|
|
259
431
|
const context = useContext(KnobContext);
|
|
@@ -263,14 +435,13 @@ function useKnobContext(selector) {
|
|
|
263
435
|
//#endregion
|
|
264
436
|
//#region src/components/Knob/ActiveLine.tsx
|
|
265
437
|
function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
438
|
+
useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
|
|
266
439
|
const r2 = useKnobContext((s) => s.r2);
|
|
267
440
|
const r3 = useKnobContext((s) => s.r3);
|
|
268
441
|
const radius = arcRadius(strokeWidth);
|
|
269
|
-
const start = pointOnArc(r2, radius);
|
|
270
|
-
const end = pointOnArc(r3, radius);
|
|
271
442
|
return /* @__PURE__ */ jsx("path", {
|
|
272
|
-
className
|
|
273
|
-
d:
|
|
443
|
+
className,
|
|
444
|
+
d: arcPath(r2, r3, radius),
|
|
274
445
|
fill: "none",
|
|
275
446
|
stroke,
|
|
276
447
|
strokeWidth,
|
|
@@ -280,6 +451,7 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
|
|
|
280
451
|
//#endregion
|
|
281
452
|
//#region src/components/Knob/InactiveLine.tsx
|
|
282
453
|
function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...props }) {
|
|
454
|
+
useCheckPlacement("Knob.InactiveLine", "Knob.SVGRoot");
|
|
283
455
|
const min = useKnobContext((s) => s.min);
|
|
284
456
|
const max = useKnobContext((s) => s.max);
|
|
285
457
|
const startValue = useKnobContext((s) => s.startValue);
|
|
@@ -288,20 +460,16 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
288
460
|
const r3 = useKnobContext((s) => s.r3);
|
|
289
461
|
const r4 = useKnobContext((s) => s.r4);
|
|
290
462
|
const radius = arcRadius(strokeWidth);
|
|
291
|
-
const p1 = pointOnArc(r1, radius);
|
|
292
|
-
const p2 = pointOnArc(r2, radius);
|
|
293
|
-
const p3 = pointOnArc(r3, radius);
|
|
294
|
-
const p4 = pointOnArc(r4, radius);
|
|
295
463
|
return /* @__PURE__ */ jsxs(Fragment, { children: [startValue > min && /* @__PURE__ */ jsx("path", {
|
|
296
|
-
className
|
|
297
|
-
d:
|
|
464
|
+
className,
|
|
465
|
+
d: arcPath(r1, r2, radius),
|
|
298
466
|
fill: "none",
|
|
299
467
|
stroke,
|
|
300
468
|
strokeWidth,
|
|
301
469
|
...props
|
|
302
470
|
}), startValue < max && /* @__PURE__ */ jsx("path", {
|
|
303
|
-
className
|
|
304
|
-
d:
|
|
471
|
+
className,
|
|
472
|
+
d: arcPath(r3, r4, radius),
|
|
305
473
|
fill: "none",
|
|
306
474
|
stroke,
|
|
307
475
|
strokeWidth,
|
|
@@ -318,17 +486,21 @@ function SVGRoot({ children, style, ...props }) {
|
|
|
318
486
|
...style
|
|
319
487
|
},
|
|
320
488
|
...props,
|
|
321
|
-
children
|
|
489
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
490
|
+
name: "Knob.SVGRoot",
|
|
491
|
+
children
|
|
492
|
+
})
|
|
322
493
|
});
|
|
323
494
|
}
|
|
324
495
|
//#endregion
|
|
325
496
|
//#region src/components/Knob/Thumb.tsx
|
|
326
497
|
function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor", thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, classes, ...props }) {
|
|
498
|
+
useCheckPlacement("Knob.Thumb", "Knob.SVGRoot");
|
|
327
499
|
const angleRange = useKnobContext((s) => s.angleRange);
|
|
328
500
|
const p = useKnobContext((s) => s.p);
|
|
329
501
|
const r1 = useKnobContext((s) => s.r1);
|
|
330
502
|
return /* @__PURE__ */ jsxs("svg", {
|
|
331
|
-
className
|
|
503
|
+
className,
|
|
332
504
|
...props,
|
|
333
505
|
children: [/* @__PURE__ */ jsx("circle", {
|
|
334
506
|
cx: "50%",
|
|
@@ -336,7 +508,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
336
508
|
r: `${thumbSize / 2}%`,
|
|
337
509
|
fill: thumb
|
|
338
510
|
}), /* @__PURE__ */ jsx("line", {
|
|
339
|
-
className:
|
|
511
|
+
className: classes?.thumbLine,
|
|
340
512
|
x1: "50%",
|
|
341
513
|
y1: `${(100 - clamp(thumbSize, 0, 100)) / 2}%`,
|
|
342
514
|
x2: "50%",
|
|
@@ -352,10 +524,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
352
524
|
}
|
|
353
525
|
//#endregion
|
|
354
526
|
//#region src/components/Knob/index.tsx
|
|
355
|
-
const defaultExternalStyles$2 = {
|
|
356
|
-
userSelectNone: true,
|
|
357
|
-
cursor: "grabbing"
|
|
358
|
-
};
|
|
527
|
+
const defaultExternalStyles$2 = { cursor: "grabbing" };
|
|
359
528
|
/**
|
|
360
529
|
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
361
530
|
* the control does not change its value.
|
|
@@ -365,12 +534,13 @@ const WHEEL_OPTIONS$2 = { requireFocus: true };
|
|
|
365
534
|
* Interactive rotary knob component implemented in SVG.
|
|
366
535
|
*/
|
|
367
536
|
const Knob = {
|
|
368
|
-
Root: forwardRef(({ value, min, max, step = 1, scale = linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel =
|
|
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) => {
|
|
369
538
|
const elmRef = useRef(null);
|
|
370
539
|
const externalStyles = {
|
|
371
540
|
...defaultExternalStyles$2,
|
|
372
541
|
..._externalStyles
|
|
373
542
|
};
|
|
543
|
+
const inactive = disabled || readonly;
|
|
374
544
|
const range = useMemo(() => ({
|
|
375
545
|
min,
|
|
376
546
|
max,
|
|
@@ -382,8 +552,14 @@ const Knob = {
|
|
|
382
552
|
step,
|
|
383
553
|
scale
|
|
384
554
|
]);
|
|
555
|
+
useCheckSteps({
|
|
556
|
+
component: "Knob",
|
|
557
|
+
range,
|
|
558
|
+
keyboard,
|
|
559
|
+
wheel
|
|
560
|
+
});
|
|
385
561
|
const handleKeyDown = useCallback((event) => {
|
|
386
|
-
if (!keyboard || !onChange ||
|
|
562
|
+
if (!keyboard || !onChange || inactive) return;
|
|
387
563
|
const key = event.key;
|
|
388
564
|
if ([
|
|
389
565
|
"ArrowRight",
|
|
@@ -392,12 +568,12 @@ const Knob = {
|
|
|
392
568
|
"ArrowDown"
|
|
393
569
|
].includes(key)) {
|
|
394
570
|
event.preventDefault();
|
|
395
|
-
onChange(applyDelta(value, key
|
|
571
|
+
onChange(applyDelta(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
|
|
396
572
|
}
|
|
397
573
|
}, [
|
|
398
574
|
keyboard,
|
|
399
575
|
onChange,
|
|
400
|
-
|
|
576
|
+
inactive,
|
|
401
577
|
value,
|
|
402
578
|
range
|
|
403
579
|
]);
|
|
@@ -407,25 +583,23 @@ const Knob = {
|
|
|
407
583
|
reverse: true
|
|
408
584
|
}], [range]),
|
|
409
585
|
getValue: () => [value, value],
|
|
586
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
410
587
|
threshold: 1,
|
|
411
|
-
cursor:
|
|
588
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
589
|
+
pointerLock: inactive ? false : pointerLock,
|
|
590
|
+
shouldStart: () => !inactive,
|
|
412
591
|
onChange: (v) => {
|
|
413
|
-
if (
|
|
592
|
+
if (inactive) return;
|
|
414
593
|
onChange?.(v[1]);
|
|
415
|
-
},
|
|
416
|
-
onDragStart: () => {
|
|
417
|
-
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
418
|
-
},
|
|
419
|
-
onDragEnd: () => {
|
|
420
|
-
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
421
594
|
}
|
|
422
595
|
});
|
|
423
|
-
const
|
|
424
|
-
if (!wheel ||
|
|
596
|
+
const wheelRefCallback = useWheel((event) => {
|
|
597
|
+
if (!wheel || inactive) return;
|
|
425
598
|
event.preventDefault();
|
|
426
|
-
if (!onChange || event.deltaY
|
|
427
|
-
onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range));
|
|
428
|
-
}, WHEEL_OPTIONS$2)
|
|
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);
|
|
429
603
|
const context = useMemo(() => {
|
|
430
604
|
const config = {
|
|
431
605
|
value,
|
|
@@ -452,34 +626,35 @@ const Knob = {
|
|
|
452
626
|
useImperativeHandle(forwardedRef, () => {
|
|
453
627
|
return {
|
|
454
628
|
focus() {
|
|
455
|
-
elmRef.current?.focus();
|
|
629
|
+
if (!disabled) elmRef.current?.focus();
|
|
456
630
|
},
|
|
457
631
|
blur() {
|
|
458
632
|
elmRef.current?.blur();
|
|
459
633
|
}
|
|
460
634
|
};
|
|
461
|
-
}, []);
|
|
635
|
+
}, [disabled]);
|
|
462
636
|
return /* @__PURE__ */ jsx(KnobProvider, {
|
|
463
637
|
value: context,
|
|
464
638
|
children: /* @__PURE__ */ jsx("div", {
|
|
465
639
|
ref: rootRefCallback,
|
|
466
|
-
className
|
|
467
|
-
tabIndex: 0,
|
|
640
|
+
className,
|
|
641
|
+
tabIndex: disabled ? -1 : 0,
|
|
468
642
|
role: "slider",
|
|
469
643
|
"aria-valuenow": value,
|
|
470
644
|
"aria-valuemin": min,
|
|
471
645
|
"aria-valuemax": max,
|
|
472
646
|
"aria-disabled": disabled,
|
|
473
647
|
"aria-readonly": readonly,
|
|
474
|
-
"data-
|
|
648
|
+
"data-disabled": disabled ? "" : void 0,
|
|
649
|
+
"data-readonly": readonly ? "" : void 0,
|
|
650
|
+
"data-dragging": dragging ? "" : void 0,
|
|
475
651
|
style: {
|
|
476
|
-
|
|
477
|
-
height: size,
|
|
652
|
+
"--knob-size": cssLength(size),
|
|
478
653
|
...style
|
|
479
654
|
},
|
|
480
655
|
onPointerDown,
|
|
481
656
|
onDoubleClick: (event) => {
|
|
482
|
-
if (enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
657
|
+
if (!inactive && enableDoubleClickDefault && onChange) onChange(defaultValue);
|
|
483
658
|
onDoubleClick?.(event);
|
|
484
659
|
},
|
|
485
660
|
onKeyDown: (event) => {
|
|
@@ -498,14 +673,14 @@ const Knob = {
|
|
|
498
673
|
};
|
|
499
674
|
//#endregion
|
|
500
675
|
//#region src/components/NumberInput/context.tsx
|
|
501
|
-
const NumberInputContext = createContext(null);
|
|
676
|
+
const NumberInputContext = /* @__PURE__ */ createContext(null);
|
|
502
677
|
const NumberInputProvider = NumberInputContext.Provider;
|
|
503
678
|
function useNumberInputContext(selector) {
|
|
504
679
|
const context = useContext(NumberInputContext);
|
|
505
680
|
if (!context) throw new Error("Missing NumberInputContext.Provider in the tree");
|
|
506
681
|
return selector ? selector(context) : context;
|
|
507
682
|
}
|
|
508
|
-
const StepperContext = createContext(null);
|
|
683
|
+
const StepperContext = /* @__PURE__ */ createContext(null);
|
|
509
684
|
const StepperProvider = StepperContext.Provider;
|
|
510
685
|
function useStepperContext() {
|
|
511
686
|
return useContext(StepperContext);
|
|
@@ -514,22 +689,26 @@ function useStepperContext() {
|
|
|
514
689
|
//#region src/hooks/useEventListener.ts
|
|
515
690
|
function useEventListener(target, event, handler, options) {
|
|
516
691
|
const listener = useCallbackRef(handler);
|
|
692
|
+
const cleanupRef = useRef(() => {});
|
|
517
693
|
useEffect(() => {
|
|
518
694
|
const node = typeof target === "function" ? target() : target ?? document;
|
|
519
695
|
if (!node) return;
|
|
520
696
|
node.addEventListener(event, listener, options);
|
|
521
|
-
|
|
697
|
+
const cleanup = () => {
|
|
522
698
|
node.removeEventListener(event, listener, options);
|
|
523
699
|
};
|
|
700
|
+
cleanupRef.current = cleanup;
|
|
701
|
+
return () => {
|
|
702
|
+
cleanup();
|
|
703
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
704
|
+
};
|
|
524
705
|
}, [
|
|
525
706
|
event,
|
|
526
707
|
target,
|
|
527
708
|
options,
|
|
528
709
|
listener
|
|
529
710
|
]);
|
|
530
|
-
return () =>
|
|
531
|
-
(typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
|
|
532
|
-
};
|
|
711
|
+
return useCallback(() => cleanupRef.current(), []);
|
|
533
712
|
}
|
|
534
713
|
//#endregion
|
|
535
714
|
//#region src/hooks/useInterval.ts
|
|
@@ -553,47 +732,61 @@ function useInterval(callback, delay) {
|
|
|
553
732
|
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
554
733
|
const [pressed, setPressed] = useState(false);
|
|
555
734
|
const [delay, setDelay] = useState(initialDelay);
|
|
735
|
+
const activePointerId = useRef(null);
|
|
736
|
+
const isPressed = useRef(false);
|
|
556
737
|
useInterval(() => {
|
|
557
738
|
callback();
|
|
558
739
|
setDelay(interval);
|
|
559
740
|
}, pressed ? delay : null);
|
|
560
|
-
|
|
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;
|
|
561
746
|
setPressed(false);
|
|
562
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();
|
|
563
757
|
});
|
|
564
|
-
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;
|
|
565
762
|
callback();
|
|
566
763
|
setPressed(true);
|
|
567
764
|
}, [callback]);
|
|
568
765
|
}
|
|
569
766
|
//#endregion
|
|
570
|
-
//#region src/components/NumberInput/
|
|
767
|
+
//#region src/components/NumberInput/StepperButton.tsx
|
|
571
768
|
/**
|
|
572
769
|
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
573
770
|
* in which way they move the value and which arrow they draw.
|
|
574
771
|
*/
|
|
575
772
|
function StepperButton({ direction, variant, icon, className, style, children, onPointerDown, ...props }) {
|
|
576
|
-
const { step, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
773
|
+
const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
577
774
|
const stepper = useStepperContext();
|
|
578
|
-
const press = useLongPress(
|
|
775
|
+
const press = useLongPress(() => {
|
|
579
776
|
if (stepper?.draggingRef.current) return;
|
|
580
777
|
nudge(direction, ["raw", step]);
|
|
581
|
-
}
|
|
582
|
-
stepper,
|
|
583
|
-
nudge,
|
|
584
|
-
direction,
|
|
585
|
-
step
|
|
586
|
-
]));
|
|
778
|
+
});
|
|
587
779
|
return /* @__PURE__ */ jsx("div", {
|
|
588
|
-
className
|
|
780
|
+
className,
|
|
589
781
|
role: "button",
|
|
590
782
|
tabIndex: -1,
|
|
591
783
|
"aria-label": direction > 0 ? "Increment" : "Decrement",
|
|
592
|
-
"aria-disabled": direction > 0 ? atMax : atMin,
|
|
593
|
-
"
|
|
784
|
+
"aria-disabled": disabled || (direction > 0 ? atMax : atMin),
|
|
785
|
+
"data-disabled": disabled || (direction > 0 ? atMax : atMin) ? "" : void 0,
|
|
786
|
+
"data-readonly": readonly ? "" : void 0,
|
|
594
787
|
style,
|
|
595
788
|
onPointerDown: (event) => {
|
|
596
|
-
if (!readonly) press();
|
|
789
|
+
if (!disabled && !readonly) press(event);
|
|
597
790
|
onPointerDown?.(event);
|
|
598
791
|
},
|
|
599
792
|
...props,
|
|
@@ -602,7 +795,10 @@ function StepperButton({ direction, variant, icon, className, style, children, o
|
|
|
602
795
|
}
|
|
603
796
|
function StepperArrow({ up }) {
|
|
604
797
|
return /* @__PURE__ */ jsx("svg", {
|
|
605
|
-
|
|
798
|
+
style: {
|
|
799
|
+
width: "var(--stepper-icon-size)",
|
|
800
|
+
height: "var(--stepper-icon-size)"
|
|
801
|
+
},
|
|
606
802
|
viewBox: "0 0 24 24",
|
|
607
803
|
fill: "none",
|
|
608
804
|
stroke: "currentColor",
|
|
@@ -638,56 +834,111 @@ function IncrementStepper(props) {
|
|
|
638
834
|
//#region src/components/NumberInput/InputField.tsx
|
|
639
835
|
/** The leading number of the displayed text, whatever the format put around it. */
|
|
640
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
|
+
};
|
|
641
850
|
/**
|
|
642
851
|
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
643
852
|
*
|
|
644
853
|
* While the user types, their own text stands rather than `format(value)`, so
|
|
645
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`).
|
|
646
858
|
*/
|
|
647
|
-
|
|
648
|
-
const { value, min, max, step, disabled, readonly, keyboard, text, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
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);
|
|
869
|
+
}, [
|
|
870
|
+
focused,
|
|
871
|
+
selectOnFocus,
|
|
872
|
+
inputRef
|
|
873
|
+
]);
|
|
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);
|
|
888
|
+
});
|
|
889
|
+
const composedRef = useComposedRefs(forwardedRef, inputRef);
|
|
649
890
|
return /* @__PURE__ */ jsx("input", {
|
|
650
|
-
ref:
|
|
651
|
-
className
|
|
891
|
+
ref: composedRef,
|
|
892
|
+
className,
|
|
652
893
|
type: "text",
|
|
653
894
|
inputMode: "decimal",
|
|
654
895
|
role: "spinbutton",
|
|
655
|
-
value:
|
|
896
|
+
value: shown,
|
|
897
|
+
disabled,
|
|
656
898
|
readOnly: readonly,
|
|
657
899
|
"aria-disabled": disabled,
|
|
658
900
|
"aria-readonly": readonly,
|
|
901
|
+
"data-disabled": disabled ? "" : void 0,
|
|
902
|
+
"data-readonly": readonly ? "" : void 0,
|
|
659
903
|
"aria-valuenow": value,
|
|
660
904
|
"aria-valuemin": min,
|
|
661
905
|
"aria-valuemax": max,
|
|
662
906
|
"aria-valuetext": text,
|
|
663
907
|
step,
|
|
664
|
-
"data-out-of-range": outOfRange,
|
|
908
|
+
"data-out-of-range": outOfRange ? "" : void 0,
|
|
665
909
|
style,
|
|
666
|
-
onChange: (event) =>
|
|
910
|
+
onChange: (event) => {
|
|
911
|
+
caretAfterStep.current = null;
|
|
912
|
+
setDraft(event.currentTarget.value);
|
|
913
|
+
},
|
|
667
914
|
onFocus: (event) => {
|
|
668
|
-
|
|
669
|
-
if (selectOnFocus == "all") input.setSelectionRange(0, input.value.length);
|
|
670
|
-
else if (selectOnFocus == "number") input.setSelectionRange(0, input.value.match(NUMBER_PREFIX)?.[0].length ?? 0);
|
|
915
|
+
setFocused(true);
|
|
671
916
|
onFocus?.(event);
|
|
672
917
|
},
|
|
673
918
|
onBlur: (event) => {
|
|
919
|
+
setFocused(false);
|
|
674
920
|
commitDraft();
|
|
675
921
|
onBlur?.(event);
|
|
676
922
|
},
|
|
677
923
|
onKeyDown: (event) => {
|
|
678
924
|
const key = event.key;
|
|
679
|
-
if (key
|
|
925
|
+
if (key === "Enter") {
|
|
680
926
|
commitDraft();
|
|
681
927
|
if (blurOnEnter) event.currentTarget.blur();
|
|
682
|
-
} else if (keyboard && !readonly && (key
|
|
928
|
+
} else if (keyboard && !disabled && !readonly && (key === "ArrowUp" || key === "ArrowDown") && !event.nativeEvent.isComposing) {
|
|
683
929
|
event.preventDefault();
|
|
684
|
-
|
|
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);
|
|
685
936
|
}
|
|
686
937
|
onKeyDown?.(event);
|
|
687
938
|
},
|
|
688
939
|
...props
|
|
689
940
|
});
|
|
690
|
-
}
|
|
941
|
+
});
|
|
691
942
|
//#endregion
|
|
692
943
|
//#region src/hooks/useDrag.ts
|
|
693
944
|
/**
|
|
@@ -695,7 +946,7 @@ function InputField({ selectOnFocus = "none", blurOnEnter = true, className, sty
|
|
|
695
946
|
*
|
|
696
947
|
* @returns a ref callback to attach to the element being dragged
|
|
697
948
|
*/
|
|
698
|
-
function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
949
|
+
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
699
950
|
const dragHandler = useCallbackRef(onDrag);
|
|
700
951
|
const dragStartHandler = useCallbackRef(onDragStart);
|
|
701
952
|
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
@@ -705,15 +956,17 @@ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
|
705
956
|
const instance = createDrag(node, {
|
|
706
957
|
threshold,
|
|
707
958
|
cursor,
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
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)
|
|
711
963
|
});
|
|
712
964
|
return () => instance.destroy();
|
|
713
965
|
}, [
|
|
714
966
|
node,
|
|
715
967
|
threshold,
|
|
716
968
|
cursor,
|
|
969
|
+
pointerLock,
|
|
717
970
|
dragHandler,
|
|
718
971
|
dragStartHandler,
|
|
719
972
|
dragEndHandler
|
|
@@ -729,8 +982,17 @@ function useDrag({ threshold = 1, cursor, onDrag, onDragStart, onDragEnd }) {
|
|
|
729
982
|
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
730
983
|
* off text selection on whatever element it is attached to.
|
|
731
984
|
*/
|
|
732
|
-
function Stepper({ className, style, children,
|
|
733
|
-
const { value, step, readonly, drag,
|
|
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]);
|
|
734
996
|
/**
|
|
735
997
|
* Set once the drag has actually moved the value, so that the press-and-hold
|
|
736
998
|
* repeat of a stepper stands down and leaves the value to the drag. A ref,
|
|
@@ -743,60 +1005,90 @@ function Stepper({ className, style, children, ref, ...props }) {
|
|
|
743
1005
|
* have been nudged once, and the drag should carry on from there.
|
|
744
1006
|
*/
|
|
745
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);
|
|
746
1015
|
const dragRefCallback = useDrag({
|
|
747
1016
|
threshold: 1,
|
|
748
|
-
cursor:
|
|
749
|
-
|
|
1017
|
+
cursor: inactive ? void 0 : "ns-resize",
|
|
1018
|
+
pointerLock: inactive ? false : pointerLock,
|
|
1019
|
+
onDragStart: (state) => {
|
|
750
1020
|
originRef.current = null;
|
|
751
1021
|
draggingRef.current = false;
|
|
1022
|
+
factorRef.current = selectModifier(dragSensitivity, state.event).value;
|
|
752
1023
|
},
|
|
753
|
-
onDrag: (_x, y) => {
|
|
754
|
-
if (
|
|
1024
|
+
onDrag: (_x, y, _dx, _dy, state) => {
|
|
1025
|
+
if (inactive || drag === null) return;
|
|
755
1026
|
if (!originRef.current) {
|
|
756
1027
|
originRef.current = {
|
|
757
1028
|
y,
|
|
758
1029
|
value
|
|
759
1030
|
};
|
|
1031
|
+
previousYRef.current = y;
|
|
760
1032
|
return;
|
|
761
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;
|
|
762
1043
|
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
763
|
-
if (steps
|
|
1044
|
+
if (steps === 0) return;
|
|
764
1045
|
draggingRef.current = true;
|
|
765
|
-
changeValue(applyDelta(originRef.current.value, steps,
|
|
1046
|
+
changeValue(applyDelta(originRef.current.value, steps, dragOptions, rawRange, state.event));
|
|
766
1047
|
},
|
|
767
1048
|
onDragEnd: () => {
|
|
768
1049
|
draggingRef.current = false;
|
|
769
1050
|
}
|
|
770
1051
|
});
|
|
771
|
-
const composedRef = useComposedRefs(
|
|
1052
|
+
const composedRef = useComposedRefs(forwardedRef, drag !== null && !inactive ? dragRefCallback : void 0);
|
|
1053
|
+
const context = useMemo(() => ({ draggingRef }), []);
|
|
772
1054
|
return /* @__PURE__ */ jsx(StepperProvider, {
|
|
773
|
-
value:
|
|
1055
|
+
value: context,
|
|
774
1056
|
children: /* @__PURE__ */ jsx("div", {
|
|
775
1057
|
ref: composedRef,
|
|
776
|
-
className
|
|
1058
|
+
className,
|
|
777
1059
|
style,
|
|
778
1060
|
...props,
|
|
779
1061
|
children
|
|
780
1062
|
})
|
|
781
1063
|
});
|
|
782
|
-
}
|
|
1064
|
+
});
|
|
1065
|
+
//#endregion
|
|
1066
|
+
//#region src/components/NumberInput/index.tsx
|
|
1067
|
+
/**
|
|
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.
|
|
1071
|
+
*/
|
|
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
|
+
};
|
|
783
1078
|
/**
|
|
784
1079
|
* Input with some useful functions for entering numerical values.
|
|
785
1080
|
*/
|
|
786
1081
|
const NumberInput = {
|
|
787
|
-
Root: forwardRef(({ value, min, max, step = 1, scale = linearScale,
|
|
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) => {
|
|
788
1083
|
const inputRef = useRef(null);
|
|
789
1084
|
/**
|
|
790
1085
|
* The text being typed. The only state here: it is not a copy of `value`,
|
|
791
1086
|
* but the half-finished entry that has no value to be derived from yet.
|
|
792
1087
|
*/
|
|
793
1088
|
const [draft, setDraft] = useState(null);
|
|
794
|
-
const
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
digit
|
|
798
|
-
]);
|
|
799
|
-
const parse = useCallback((t) => parseProp ? parseProp(t) : parseValue(t, units), [parseProp, units]);
|
|
1089
|
+
const inactive = disabled || readonly;
|
|
1090
|
+
const format = formatProp ?? defaultFormat;
|
|
1091
|
+
const parse = parseProp ?? defaultParse;
|
|
800
1092
|
const range = useMemo(() => ({
|
|
801
1093
|
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
802
1094
|
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
@@ -809,49 +1101,90 @@ const NumberInput = {
|
|
|
809
1101
|
step,
|
|
810
1102
|
scale
|
|
811
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
|
+
});
|
|
812
1133
|
const text = draft ?? format(value);
|
|
813
|
-
const editing = draft
|
|
814
|
-
const outOfRange = !editing && (min
|
|
1134
|
+
const editing = draft !== null;
|
|
1135
|
+
const outOfRange = !editing && (min !== void 0 && value < min || max !== void 0 && value > max);
|
|
815
1136
|
const handleDraft = useCallback((next) => {
|
|
816
|
-
if (
|
|
1137
|
+
if (inactive) return;
|
|
817
1138
|
setDraft(next);
|
|
818
1139
|
const parsed = parse(next);
|
|
819
1140
|
if (Number.isFinite(parsed)) onChange?.(parsed);
|
|
820
1141
|
}, [
|
|
821
|
-
|
|
1142
|
+
inactive,
|
|
822
1143
|
parse,
|
|
823
1144
|
onChange
|
|
824
1145
|
]);
|
|
825
1146
|
const changeValue = useCallback((next) => {
|
|
826
|
-
if (
|
|
1147
|
+
if (inactive) return;
|
|
827
1148
|
setDraft(null);
|
|
828
|
-
if (next
|
|
1149
|
+
if (next !== value) onChange?.(next);
|
|
829
1150
|
}, [
|
|
830
|
-
|
|
1151
|
+
inactive,
|
|
831
1152
|
value,
|
|
832
1153
|
onChange
|
|
833
1154
|
]);
|
|
834
1155
|
const commitDraft = useCallback(() => {
|
|
835
|
-
if (draft
|
|
836
|
-
|
|
1156
|
+
if (draft === null || inactive) return;
|
|
1157
|
+
const parsed = parse(draft);
|
|
1158
|
+
if (!Number.isFinite(parsed)) {
|
|
1159
|
+
setDraft(null);
|
|
1160
|
+
return;
|
|
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);
|
|
837
1166
|
}, [
|
|
838
1167
|
draft,
|
|
839
|
-
|
|
1168
|
+
inactive,
|
|
840
1169
|
parse,
|
|
841
|
-
|
|
1170
|
+
clampValue,
|
|
1171
|
+
min,
|
|
1172
|
+
max,
|
|
842
1173
|
changeValue
|
|
843
1174
|
]);
|
|
844
|
-
const nudge = useCallback((direction, option) => {
|
|
845
|
-
|
|
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));
|
|
846
1178
|
}, [
|
|
847
1179
|
changeValue,
|
|
848
1180
|
value,
|
|
1181
|
+
rawRange,
|
|
849
1182
|
range
|
|
850
1183
|
]);
|
|
851
1184
|
const wheelRefCallback = useWheel((event) => {
|
|
852
|
-
if (!wheel ||
|
|
1185
|
+
if (!wheel || inactive || event.deltaY === 0) return;
|
|
853
1186
|
event.preventDefault();
|
|
854
|
-
nudge(-Math.sign(event.deltaY), wheel);
|
|
1187
|
+
nudge(-Math.sign(event.deltaY), wheel, event);
|
|
855
1188
|
}, { requireFocus: true });
|
|
856
1189
|
const context = useMemo(() => ({
|
|
857
1190
|
value,
|
|
@@ -863,12 +1196,19 @@ const NumberInput = {
|
|
|
863
1196
|
readonly,
|
|
864
1197
|
clampValue,
|
|
865
1198
|
range,
|
|
1199
|
+
rawRange,
|
|
866
1200
|
keyboard,
|
|
867
1201
|
text,
|
|
868
1202
|
editing,
|
|
869
1203
|
outOfRange,
|
|
870
|
-
|
|
871
|
-
|
|
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,
|
|
872
1212
|
drag,
|
|
873
1213
|
format,
|
|
874
1214
|
parse,
|
|
@@ -887,11 +1227,18 @@ const NumberInput = {
|
|
|
887
1227
|
readonly,
|
|
888
1228
|
clampValue,
|
|
889
1229
|
range,
|
|
1230
|
+
rawRange,
|
|
890
1231
|
keyboard,
|
|
891
1232
|
text,
|
|
892
1233
|
editing,
|
|
893
1234
|
outOfRange,
|
|
894
1235
|
drag,
|
|
1236
|
+
dragSensitivity,
|
|
1237
|
+
pointerLock,
|
|
1238
|
+
selectOnFocus,
|
|
1239
|
+
unformatOnFocus,
|
|
1240
|
+
keepCaretOnStep,
|
|
1241
|
+
blurOnEnter,
|
|
895
1242
|
format,
|
|
896
1243
|
parse,
|
|
897
1244
|
handleDraft,
|
|
@@ -902,20 +1249,20 @@ const NumberInput = {
|
|
|
902
1249
|
useImperativeHandle(forwardedRef, () => {
|
|
903
1250
|
return {
|
|
904
1251
|
focus() {
|
|
905
|
-
inputRef.current?.focus();
|
|
1252
|
+
if (!disabled) inputRef.current?.focus();
|
|
906
1253
|
},
|
|
907
1254
|
blur() {
|
|
908
1255
|
inputRef.current?.blur();
|
|
909
1256
|
}
|
|
910
1257
|
};
|
|
911
|
-
}, []);
|
|
1258
|
+
}, [disabled]);
|
|
912
1259
|
return /* @__PURE__ */ jsx(NumberInputProvider, {
|
|
913
1260
|
value: context,
|
|
914
1261
|
children: /* @__PURE__ */ jsx("div", {
|
|
915
1262
|
ref: wheelRefCallback,
|
|
916
|
-
className
|
|
917
|
-
"
|
|
918
|
-
"
|
|
1263
|
+
className,
|
|
1264
|
+
"data-disabled": disabled ? "" : void 0,
|
|
1265
|
+
"data-readonly": readonly ? "" : void 0,
|
|
919
1266
|
style,
|
|
920
1267
|
...props,
|
|
921
1268
|
children
|
|
@@ -928,11 +1275,12 @@ const NumberInput = {
|
|
|
928
1275
|
DecrementStepper
|
|
929
1276
|
};
|
|
930
1277
|
//#endregion
|
|
931
|
-
//#region src/components/Piano/
|
|
1278
|
+
//#region src/components/Piano/keyboard-shortcuts.ts
|
|
932
1279
|
/**
|
|
933
1280
|
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
934
1281
|
*/
|
|
935
1282
|
const SHORTCUTS = {
|
|
1283
|
+
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
936
1284
|
HOME_ROW: { keys: [
|
|
937
1285
|
"a",
|
|
938
1286
|
"w",
|
|
@@ -952,6 +1300,7 @@ const SHORTCUTS = {
|
|
|
952
1300
|
"p",
|
|
953
1301
|
";"
|
|
954
1302
|
] },
|
|
1303
|
+
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
955
1304
|
HOME_ROW_NATURAL: { keys: [
|
|
956
1305
|
"a",
|
|
957
1306
|
"",
|
|
@@ -972,26 +1321,41 @@ const SHORTCUTS = {
|
|
|
972
1321
|
";"
|
|
973
1322
|
] }
|
|
974
1323
|
};
|
|
1324
|
+
//#endregion
|
|
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;
|
|
1338
|
+
}
|
|
975
1339
|
/**
|
|
976
1340
|
* Customizable piano component.
|
|
977
1341
|
*/
|
|
978
|
-
const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
|
|
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) {
|
|
979
1343
|
const [node, setNode] = useState(null);
|
|
980
1344
|
const [activeNotes, setActiveNotes] = useState([]);
|
|
981
|
-
/** Set while `
|
|
982
|
-
const [
|
|
1345
|
+
/** Set while `resizable` is on, measured from the parent. */
|
|
1346
|
+
const [resizedKeyWidth, setResizedKeyWidth] = useState(whiteKeyWidth);
|
|
983
1347
|
const notes = useMemo(() => getNoteRangeArray(noteRange), [noteRange]);
|
|
984
1348
|
const whiteKeyCount = useMemo(() => notes.filter(isWhiteKey).length, [notes]);
|
|
985
1349
|
const layout = useMemo(() => ({
|
|
986
1350
|
noteRange,
|
|
987
|
-
whiteKeyWidth:
|
|
1351
|
+
whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
|
|
988
1352
|
keyGap,
|
|
989
1353
|
blackKeyWidthRatio,
|
|
990
1354
|
blackKeyHeightRatio
|
|
991
1355
|
}), [
|
|
992
1356
|
noteRange,
|
|
993
|
-
|
|
994
|
-
|
|
1357
|
+
resizable,
|
|
1358
|
+
resizedKeyWidth,
|
|
995
1359
|
whiteKeyWidth,
|
|
996
1360
|
keyGap,
|
|
997
1361
|
blackKeyWidthRatio,
|
|
@@ -1005,6 +1369,11 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1005
1369
|
onStopNote
|
|
1006
1370
|
});
|
|
1007
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
|
+
}, []);
|
|
1008
1377
|
useEffect(() => {
|
|
1009
1378
|
if (!node) return;
|
|
1010
1379
|
const instance = createPianoInput(node, {
|
|
@@ -1036,35 +1405,76 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1036
1405
|
});
|
|
1037
1406
|
});
|
|
1038
1407
|
useEffect(() => {
|
|
1039
|
-
if (!
|
|
1408
|
+
if (!resizable || !node) return;
|
|
1040
1409
|
const parent = node.parentElement;
|
|
1041
1410
|
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1042
1411
|
const resizeObserver = new ResizeObserver(() => {
|
|
1043
|
-
|
|
1412
|
+
setResizedKeyWidth(node.clientWidth / Math.max(whiteKeyCount, 1) - keyGap);
|
|
1044
1413
|
});
|
|
1045
1414
|
resizeObserver.observe(parent);
|
|
1046
1415
|
return () => resizeObserver.disconnect();
|
|
1047
1416
|
}, [
|
|
1048
|
-
|
|
1417
|
+
resizable,
|
|
1049
1418
|
node,
|
|
1050
1419
|
whiteKeyCount,
|
|
1051
1420
|
keyGap
|
|
1052
1421
|
]);
|
|
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
|
+
]);
|
|
1053
1432
|
/** The note a shortcut key plays, or null when it has none. */
|
|
1054
1433
|
function shortcutNote(key) {
|
|
1055
|
-
if (!
|
|
1056
|
-
const index =
|
|
1057
|
-
|
|
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;
|
|
1058
1438
|
}
|
|
1059
|
-
|
|
1060
|
-
if (
|
|
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;
|
|
1061
1457
|
const note = shortcutNote(e.key);
|
|
1062
|
-
if (note
|
|
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 });
|
|
1063
1465
|
});
|
|
1064
|
-
useEventListener(
|
|
1065
|
-
const
|
|
1066
|
-
|
|
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();
|
|
1067
1476
|
});
|
|
1477
|
+
useEventListener(windowBlurTarget, "blur", releaseShortcutNotes);
|
|
1068
1478
|
useImperativeHandle(forwardedRef, () => ({
|
|
1069
1479
|
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1070
1480
|
source: "api",
|
|
@@ -1074,10 +1484,14 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1074
1484
|
}), []);
|
|
1075
1485
|
return /* @__PURE__ */ jsx("div", {
|
|
1076
1486
|
ref: setNode,
|
|
1077
|
-
className
|
|
1487
|
+
className,
|
|
1488
|
+
"data-resizable": resizable ? "" : void 0,
|
|
1489
|
+
role: "group",
|
|
1490
|
+
tabIndex: 0,
|
|
1078
1491
|
style: {
|
|
1079
|
-
width:
|
|
1080
|
-
height,
|
|
1492
|
+
width: resizable ? "100%" : pianoWidth(layout),
|
|
1493
|
+
"--height": cssLength(height),
|
|
1494
|
+
position: "relative",
|
|
1081
1495
|
...style
|
|
1082
1496
|
},
|
|
1083
1497
|
...props,
|
|
@@ -1092,22 +1506,24 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1092
1506
|
const { className: keyClassName, style: keyStyle, ...rest } = keyProps?.(note, state) ?? {};
|
|
1093
1507
|
const content = label?.(note, state);
|
|
1094
1508
|
return /* @__PURE__ */ jsx("div", {
|
|
1095
|
-
className:
|
|
1509
|
+
className: keyClassName,
|
|
1096
1510
|
"data-note": note,
|
|
1097
1511
|
"data-note-key": noteKey(note),
|
|
1098
|
-
"data-active": state.active,
|
|
1099
|
-
"
|
|
1512
|
+
"data-active": state.active ? "" : void 0,
|
|
1513
|
+
"data-disabled": state.disabled ? "" : void 0,
|
|
1100
1514
|
...rest,
|
|
1101
1515
|
style: {
|
|
1516
|
+
position: "absolute",
|
|
1517
|
+
zIndex: keyType === "white" ? 1 : 2,
|
|
1102
1518
|
...keyStyle,
|
|
1103
1519
|
left: notePosition(note, layout),
|
|
1104
1520
|
width: keyType === "white" ? layout.whiteKeyWidth : blackKeyWidth(layout),
|
|
1105
1521
|
height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
|
|
1106
1522
|
},
|
|
1107
1523
|
children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ jsx("div", {
|
|
1108
|
-
className:
|
|
1524
|
+
className: classes?.keyLabelWrapper,
|
|
1109
1525
|
children: /* @__PURE__ */ jsx("div", {
|
|
1110
|
-
className:
|
|
1526
|
+
className: classes?.keyLabel,
|
|
1111
1527
|
children: content
|
|
1112
1528
|
})
|
|
1113
1529
|
})
|
|
@@ -1117,16 +1533,22 @@ const Piano = { Root: forwardRef(function Root({ noteRange, glissando = true, mi
|
|
|
1117
1533
|
}) };
|
|
1118
1534
|
//#endregion
|
|
1119
1535
|
//#region src/components/PointsEditor/Background.tsx
|
|
1120
|
-
function Background({ className, children, ...props }) {
|
|
1536
|
+
function Background({ className, children, style, ...props }) {
|
|
1121
1537
|
return /* @__PURE__ */ jsx("div", {
|
|
1122
|
-
className
|
|
1538
|
+
className,
|
|
1539
|
+
style: {
|
|
1540
|
+
position: "absolute",
|
|
1541
|
+
inset: 0,
|
|
1542
|
+
zIndex: 0,
|
|
1543
|
+
...style
|
|
1544
|
+
},
|
|
1123
1545
|
...props,
|
|
1124
1546
|
children
|
|
1125
1547
|
});
|
|
1126
1548
|
}
|
|
1127
1549
|
//#endregion
|
|
1128
1550
|
//#region src/components/PointsEditor/context.tsx
|
|
1129
|
-
const PointsEditorContext = createContext(null);
|
|
1551
|
+
const PointsEditorContext = /* @__PURE__ */ createContext(null);
|
|
1130
1552
|
const PointsEditorProvider = PointsEditorContext.Provider;
|
|
1131
1553
|
function usePointsEditorContext(selector) {
|
|
1132
1554
|
const context = useContext(PointsEditorContext);
|
|
@@ -1134,15 +1556,26 @@ function usePointsEditorContext(selector) {
|
|
|
1134
1556
|
return selector ? selector(context) : context;
|
|
1135
1557
|
}
|
|
1136
1558
|
//#endregion
|
|
1137
|
-
//#region src/components/
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
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
|
+
}));
|
|
1146
1579
|
//#endregion
|
|
1147
1580
|
//#region src/components/PointsEditor/Point.tsx
|
|
1148
1581
|
function clampPoint(point, min, max) {
|
|
@@ -1160,138 +1593,463 @@ const AXIS = {
|
|
|
1160
1593
|
min: 0,
|
|
1161
1594
|
max: 1
|
|
1162
1595
|
};
|
|
1163
|
-
|
|
1164
|
-
|
|
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);
|
|
1165
1608
|
const disabled = _disabled ?? rootDisabled;
|
|
1166
1609
|
const readonly = _readonly ?? rootReadonly;
|
|
1610
|
+
const inactive = disabled || readonly;
|
|
1167
1611
|
const wheel = _wheel === void 0 ? rootWheel : _wheel;
|
|
1168
1612
|
const keyboard = _keyboard === void 0 ? rootKeyboard : _keyboard;
|
|
1613
|
+
useCheckPlacement("PointsEditor.Point", "PointsEditor.Container");
|
|
1169
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);
|
|
1170
1640
|
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1171
1641
|
axis: AXIS,
|
|
1172
1642
|
baseElementRef: containerRef,
|
|
1173
|
-
|
|
1643
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1644
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
1645
|
+
shouldStart: () => !disabled,
|
|
1174
1646
|
onChange: ([x, y]) => {
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1647
|
+
const origin = pointerOrigin.current;
|
|
1648
|
+
if (!origin) return;
|
|
1649
|
+
movePointDrag({
|
|
1650
|
+
x: x - origin.x,
|
|
1651
|
+
y: y - origin.y
|
|
1652
|
+
});
|
|
1180
1653
|
},
|
|
1181
|
-
onDragStart: ([x, y]) => {
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
onDragStart?.(clampPoint({
|
|
1654
|
+
onDragStart: ([x, y], state) => {
|
|
1655
|
+
beginPointDrag(id, state.event);
|
|
1656
|
+
pointerOrigin.current = {
|
|
1185
1657
|
x,
|
|
1186
1658
|
y
|
|
1187
|
-
}
|
|
1659
|
+
};
|
|
1660
|
+
xInputRef.current?.focus();
|
|
1661
|
+
if (inactive) return;
|
|
1662
|
+
onDragStart?.(clampPoint(value, min, max));
|
|
1188
1663
|
},
|
|
1189
|
-
onDragEnd: (
|
|
1190
|
-
|
|
1191
|
-
if (
|
|
1192
|
-
onDragEnd?.(clampPoint(
|
|
1193
|
-
x,
|
|
1194
|
-
y
|
|
1195
|
-
}, min, max));
|
|
1664
|
+
onDragEnd: () => {
|
|
1665
|
+
pointerOrigin.current = null;
|
|
1666
|
+
if (inactive) return;
|
|
1667
|
+
onDragEnd?.(clampPoint(value, min, max));
|
|
1196
1668
|
}
|
|
1197
1669
|
});
|
|
1198
|
-
const nudge = useCallback((axis, direction, option) => {
|
|
1199
|
-
const next = applyDelta(value[axis], direction, option, AXIS);
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}
|
|
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
|
+
});
|
|
1204
1676
|
}, [
|
|
1205
1677
|
value,
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
onChange
|
|
1678
|
+
id,
|
|
1679
|
+
nudgeSelection
|
|
1209
1680
|
]);
|
|
1210
|
-
useWheel((event) => {
|
|
1211
|
-
if (!onChange || readonly || !wheel) return;
|
|
1212
|
-
if (!element || element.ownerDocument.activeElement !== element) return;
|
|
1213
|
-
event.preventDefault();
|
|
1214
|
-
nudge(event.shiftKey ? "x" : "y", event.deltaY < 0 ? -1 : 1, wheel);
|
|
1215
|
-
}, { target: containerRef });
|
|
1216
1681
|
const refCallback = useComposedRefs(dragRefCallback, setElement);
|
|
1217
1682
|
const handleKeyDown = (event) => {
|
|
1218
|
-
if (!onChange || readonly || !keyboard) return;
|
|
1219
1683
|
const key = event.key;
|
|
1220
|
-
if ([
|
|
1684
|
+
if (![
|
|
1221
1685
|
"ArrowRight",
|
|
1222
1686
|
"ArrowLeft",
|
|
1223
1687
|
"ArrowUp",
|
|
1224
1688
|
"ArrowDown"
|
|
1225
|
-
].includes(key))
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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);
|
|
1229
1694
|
};
|
|
1230
|
-
const
|
|
1231
|
-
return /* @__PURE__ */
|
|
1695
|
+
const current = clampPoint(value, min, max);
|
|
1696
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
1232
1697
|
ref: refCallback,
|
|
1233
|
-
className
|
|
1234
|
-
|
|
1235
|
-
"
|
|
1236
|
-
|
|
1237
|
-
"data-dragging": dragging,
|
|
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,
|
|
1238
1704
|
style: {
|
|
1239
|
-
|
|
1240
|
-
width: size ?? width,
|
|
1241
|
-
height: size ?? height,
|
|
1705
|
+
"--color": color,
|
|
1706
|
+
"--width": cssLength(size ?? width),
|
|
1707
|
+
"--height": cssLength(size ?? height),
|
|
1708
|
+
position: "absolute",
|
|
1709
|
+
translate: "var(--translate, -50% -50%)",
|
|
1710
|
+
...style,
|
|
1242
1711
|
left: `${value.x * 100}%`,
|
|
1243
|
-
top: `${value.y * 100}
|
|
1244
|
-
...style
|
|
1712
|
+
top: `${value.y * 100}%`
|
|
1245
1713
|
},
|
|
1246
1714
|
onPointerDown,
|
|
1715
|
+
onFocus: (event) => {
|
|
1716
|
+
if (!disabled && event.target === event.currentTarget) xInputRef.current?.focus();
|
|
1717
|
+
onFocus?.(event);
|
|
1718
|
+
},
|
|
1247
1719
|
onKeyDown: (event) => {
|
|
1248
1720
|
handleKeyDown(event);
|
|
1249
1721
|
onKeyDown?.(event);
|
|
1250
1722
|
},
|
|
1251
|
-
...props
|
|
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({
|
|
1760
|
+
x,
|
|
1761
|
+
y
|
|
1762
|
+
}, state.event);
|
|
1763
|
+
},
|
|
1764
|
+
onChange: ([x, y]) => moveSelectionBox({
|
|
1765
|
+
x,
|
|
1766
|
+
y
|
|
1767
|
+
}),
|
|
1768
|
+
onDragEnd: endSelectionBox
|
|
1769
|
+
});
|
|
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);
|
|
1778
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1779
|
+
ref: composedRef,
|
|
1780
|
+
className,
|
|
1781
|
+
style: {
|
|
1782
|
+
position: "absolute",
|
|
1783
|
+
inset: 0,
|
|
1784
|
+
zIndex: 10,
|
|
1785
|
+
...style
|
|
1786
|
+
},
|
|
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
|
|
1252
1820
|
});
|
|
1253
1821
|
}
|
|
1254
1822
|
//#endregion
|
|
1255
1823
|
//#region src/components/PointsEditor/index.tsx
|
|
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
|
+
}
|
|
1256
1849
|
/**
|
|
1257
1850
|
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
1258
1851
|
* in a hundred steps whatever its pixel size.
|
|
1259
1852
|
*/
|
|
1260
|
-
const
|
|
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]
|
|
1862
|
+
};
|
|
1261
1863
|
/**
|
|
1262
1864
|
* Multiple Point Controller
|
|
1263
1865
|
*/
|
|
1264
1866
|
const PointsEditor = {
|
|
1265
|
-
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) => {
|
|
1266
1868
|
const containerRef = useRef(null);
|
|
1267
|
-
const {
|
|
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
|
+
]);
|
|
1268
2042
|
return /* @__PURE__ */ jsx(PointsEditorProvider, {
|
|
1269
|
-
value:
|
|
1270
|
-
disabled,
|
|
1271
|
-
readonly,
|
|
1272
|
-
wheel,
|
|
1273
|
-
keyboard,
|
|
1274
|
-
externalStyles: {
|
|
1275
|
-
userSelectNone,
|
|
1276
|
-
cursor
|
|
1277
|
-
},
|
|
1278
|
-
containerRef
|
|
1279
|
-
}), [
|
|
1280
|
-
disabled,
|
|
1281
|
-
readonly,
|
|
1282
|
-
wheel,
|
|
1283
|
-
keyboard,
|
|
1284
|
-
userSelectNone,
|
|
1285
|
-
cursor
|
|
1286
|
-
]),
|
|
2043
|
+
value: context,
|
|
1287
2044
|
children: /* @__PURE__ */ jsx("div", {
|
|
1288
2045
|
ref: forwardedRef,
|
|
1289
|
-
className
|
|
1290
|
-
"
|
|
1291
|
-
"
|
|
2046
|
+
className,
|
|
2047
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2048
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1292
2049
|
style: {
|
|
1293
|
-
width,
|
|
1294
|
-
height,
|
|
2050
|
+
"--width": cssLength(width),
|
|
2051
|
+
"--height": cssLength(height),
|
|
2052
|
+
position: "relative",
|
|
1295
2053
|
...style
|
|
1296
2054
|
},
|
|
1297
2055
|
...props,
|
|
@@ -1301,11 +2059,12 @@ const PointsEditor = {
|
|
|
1301
2059
|
}),
|
|
1302
2060
|
Background,
|
|
1303
2061
|
Container,
|
|
1304
|
-
Point
|
|
2062
|
+
Point,
|
|
2063
|
+
SelectionBox
|
|
1305
2064
|
};
|
|
1306
2065
|
//#endregion
|
|
1307
2066
|
//#region src/components/Slider/context.tsx
|
|
1308
|
-
const SliderContext = createContext(null);
|
|
2067
|
+
const SliderContext = /* @__PURE__ */ createContext(null);
|
|
1309
2068
|
const SliderProvider = SliderContext.Provider;
|
|
1310
2069
|
function useSliderContext(selector) {
|
|
1311
2070
|
const context = useContext(SliderContext);
|
|
@@ -1314,7 +2073,8 @@ function useSliderContext(selector) {
|
|
|
1314
2073
|
}
|
|
1315
2074
|
//#endregion
|
|
1316
2075
|
//#region src/components/Slider/MarksOption.tsx
|
|
1317
|
-
function MarksOption({ value,
|
|
2076
|
+
function MarksOption({ value, mark = true, label, thickness, length, gap, labelWidth, classes, styles, className, style, ...props }) {
|
|
2077
|
+
useCheckPlacement("Slider.MarksOption", "Slider.Marks");
|
|
1318
2078
|
const min = useSliderContext((s) => s.min);
|
|
1319
2079
|
const max = useSliderContext((s) => s.max);
|
|
1320
2080
|
const scale = useSliderContext((s) => s.scale);
|
|
@@ -1331,52 +2091,52 @@ function MarksOption({ value, type = "mark-number", label, thickness = 1, length
|
|
|
1331
2091
|
scale
|
|
1332
2092
|
]);
|
|
1333
2093
|
return /* @__PURE__ */ jsxs("div", {
|
|
1334
|
-
className
|
|
2094
|
+
className,
|
|
1335
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,
|
|
1336
2104
|
left: !vertical ? `${calcPercent(value)}%` : void 0,
|
|
1337
|
-
top: vertical ? `${calcPercent(value)}%` : void 0
|
|
1338
|
-
...style
|
|
2105
|
+
top: vertical ? `${calcPercent(value)}%` : void 0
|
|
1339
2106
|
},
|
|
1340
|
-
"data-vertical":
|
|
2107
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
1341
2108
|
...props,
|
|
1342
|
-
children: [
|
|
1343
|
-
className:
|
|
1344
|
-
style:
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
},
|
|
1351
|
-
"data-vertical": vertical
|
|
1352
|
-
}), type != "mark" && /* @__PURE__ */ jsx("div", {
|
|
1353
|
-
className: clsx("tremolo-slider-marks-option-label", classes?.label),
|
|
1354
|
-
style: {
|
|
1355
|
-
width: labelWidth,
|
|
1356
|
-
...styles?.label
|
|
1357
|
-
},
|
|
1358
|
-
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
|
|
1359
2117
|
})]
|
|
1360
2118
|
});
|
|
1361
2119
|
}
|
|
1362
2120
|
//#endregion
|
|
1363
2121
|
//#region src/components/Slider/type.ts
|
|
1364
2122
|
function generateOptionsList(options, min, max, step) {
|
|
2123
|
+
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
1365
2124
|
const optionsList = [];
|
|
1366
|
-
const
|
|
1367
|
-
const count = Math.floor(max /
|
|
2125
|
+
const interval = per === "step" ? step : per;
|
|
2126
|
+
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
1368
2127
|
for (let i = 0; i < count; i++) {
|
|
1369
|
-
const value = toFixed(
|
|
2128
|
+
const value = toFixed(interval * (Math.ceil(min / interval) + i), decimalPart(interval)?.length);
|
|
1370
2129
|
optionsList.push({
|
|
1371
2130
|
value,
|
|
1372
|
-
|
|
2131
|
+
mark,
|
|
2132
|
+
label
|
|
1373
2133
|
});
|
|
1374
2134
|
}
|
|
1375
2135
|
return optionsList;
|
|
1376
2136
|
}
|
|
1377
2137
|
//#endregion
|
|
1378
2138
|
//#region src/components/Slider/Marks.tsx
|
|
1379
|
-
function Marks({ gap
|
|
2139
|
+
function Marks({ gap, options, children, className, style, ...props }) {
|
|
1380
2140
|
const min = useSliderContext((s) => s.min);
|
|
1381
2141
|
const max = useSliderContext((s) => s.max);
|
|
1382
2142
|
const step = useSliderContext((s) => s.step);
|
|
@@ -1385,86 +2145,109 @@ function Marks({ gap = 6, options, children, className, style, ...props }) {
|
|
|
1385
2145
|
const optionsList = options ? generateOptionsList(options, min, max, step) : [];
|
|
1386
2146
|
if (xor(vertical, reverse)) optionsList.reverse();
|
|
1387
2147
|
return /* @__PURE__ */ jsx("div", {
|
|
1388
|
-
className
|
|
2148
|
+
className,
|
|
2149
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
1389
2150
|
style: {
|
|
1390
|
-
|
|
1391
|
-
|
|
2151
|
+
"--gap": cssLength(gap),
|
|
2152
|
+
position: "relative",
|
|
1392
2153
|
...style
|
|
1393
2154
|
},
|
|
1394
2155
|
...props,
|
|
1395
|
-
children:
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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
|
+
})
|
|
1401
2166
|
});
|
|
1402
2167
|
}
|
|
1403
2168
|
//#endregion
|
|
1404
2169
|
//#region src/components/Slider/Thumb.tsx
|
|
1405
|
-
|
|
1406
|
-
const
|
|
1407
|
-
const { vertical, disabled, readonly, percent, thumbRef } = useSliderContext();
|
|
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");
|
|
1408
2174
|
const methods = () => ({
|
|
1409
2175
|
focus() {
|
|
1410
|
-
|
|
2176
|
+
if (!disabled) inputRef.current?.focus();
|
|
1411
2177
|
},
|
|
1412
2178
|
blur() {
|
|
1413
|
-
|
|
2179
|
+
inputRef.current?.blur();
|
|
1414
2180
|
}
|
|
1415
2181
|
});
|
|
1416
|
-
useImperativeHandle(
|
|
1417
|
-
useImperativeHandle(thumbRef, methods, []);
|
|
1418
|
-
return /* @__PURE__ */
|
|
1419
|
-
className
|
|
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,
|
|
1420
2189
|
style: {
|
|
2190
|
+
"--color": color,
|
|
2191
|
+
position: "absolute",
|
|
2192
|
+
translate: "var(--translate, -50% -50%)",
|
|
2193
|
+
zIndex: 100,
|
|
2194
|
+
...style,
|
|
1421
2195
|
top: vertical ? `${percent}%` : "50%",
|
|
1422
2196
|
left: !vertical ? `${percent}%` : "50%"
|
|
1423
2197
|
},
|
|
1424
|
-
children:
|
|
1425
|
-
ref:
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
2198
|
+
children: [/* @__PURE__ */ jsx(VisuallyHiddenRangeInput, {
|
|
2199
|
+
ref: inputRef,
|
|
2200
|
+
value,
|
|
2201
|
+
min,
|
|
2202
|
+
max,
|
|
2203
|
+
step,
|
|
2204
|
+
disabled,
|
|
1429
2205
|
"aria-readonly": readonly,
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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);
|
|
1433
2217
|
}
|
|
1434
|
-
})
|
|
2218
|
+
}), children]
|
|
1435
2219
|
});
|
|
1436
|
-
}
|
|
1437
|
-
|
|
2220
|
+
});
|
|
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) {
|
|
1438
2224
|
const { vertical, reverse, disabled, percent, trackRef } = useSliderContext();
|
|
1439
|
-
const composedRef = useComposedRefs(
|
|
1440
|
-
const direction = vertical ? "bottom" : "right";
|
|
1441
|
-
const colors = {
|
|
1442
|
-
"--active": active,
|
|
1443
|
-
"--inactive": inactive
|
|
1444
|
-
};
|
|
2225
|
+
const composedRef = useComposedRefs(forwardedRef, trackRef);
|
|
1445
2226
|
return /* @__PURE__ */ jsx("div", {
|
|
1446
2227
|
ref: composedRef,
|
|
1447
|
-
className
|
|
1448
|
-
"
|
|
1449
|
-
"data-vertical":
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
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",
|
|
1456
2239
|
...style
|
|
1457
2240
|
},
|
|
1458
2241
|
...props,
|
|
1459
|
-
children
|
|
2242
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2243
|
+
name: "Slider.Track",
|
|
2244
|
+
children
|
|
2245
|
+
})
|
|
1460
2246
|
});
|
|
1461
|
-
}
|
|
2247
|
+
});
|
|
1462
2248
|
//#endregion
|
|
1463
2249
|
//#region src/components/Slider/index.tsx
|
|
1464
|
-
const defaultExternalStyles$1 = {
|
|
1465
|
-
userSelectNone: true,
|
|
1466
|
-
cursor: "pointer"
|
|
1467
|
-
};
|
|
2250
|
+
const defaultExternalStyles$1 = { cursor: "pointer" };
|
|
1468
2251
|
/**
|
|
1469
2252
|
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
1470
2253
|
* the control does not change its value.
|
|
@@ -1474,13 +2257,14 @@ const WHEEL_OPTIONS$1 = { requireFocus: true };
|
|
|
1474
2257
|
* Customizable slider
|
|
1475
2258
|
*/
|
|
1476
2259
|
const Slider = {
|
|
1477
|
-
Root: forwardRef(({ value, min, max, step = 1, scale = linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel =
|
|
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) => {
|
|
1478
2261
|
const trackRef = useRef(null);
|
|
1479
2262
|
const thumbRef = useRef(null);
|
|
1480
2263
|
const externalStyles = {
|
|
1481
2264
|
...defaultExternalStyles$1,
|
|
1482
2265
|
..._externalStyles
|
|
1483
2266
|
};
|
|
2267
|
+
const inactive = disabled || readonly;
|
|
1484
2268
|
const p = toFixed(scale.normalize(value, min, max) * 100);
|
|
1485
2269
|
const rev = toFixed(100 - p);
|
|
1486
2270
|
const displayReversed = useMemo(() => xor(vertical, reverse), [vertical, reverse]);
|
|
@@ -1498,24 +2282,29 @@ const Slider = {
|
|
|
1498
2282
|
scale,
|
|
1499
2283
|
displayReversed
|
|
1500
2284
|
]);
|
|
2285
|
+
useCheckSteps({
|
|
2286
|
+
component: "Slider",
|
|
2287
|
+
range: axis,
|
|
2288
|
+
keyboard,
|
|
2289
|
+
wheel
|
|
2290
|
+
});
|
|
1501
2291
|
const handleKeyDown = useCallback((event) => {
|
|
1502
|
-
if (!keyboard || !onChange || readonly) return;
|
|
1503
2292
|
const key = event.key;
|
|
1504
|
-
if ([
|
|
2293
|
+
if (![
|
|
1505
2294
|
"ArrowRight",
|
|
1506
2295
|
"ArrowLeft",
|
|
1507
2296
|
"ArrowUp",
|
|
1508
2297
|
"ArrowDown"
|
|
1509
|
-
].includes(key))
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
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));
|
|
1515
2304
|
}, [
|
|
1516
2305
|
keyboard,
|
|
1517
2306
|
onChange,
|
|
1518
|
-
|
|
2307
|
+
inactive,
|
|
1519
2308
|
reverse,
|
|
1520
2309
|
value,
|
|
1521
2310
|
axis
|
|
@@ -1524,36 +2313,37 @@ const Slider = {
|
|
|
1524
2313
|
const { refCallback: dragRefCallback } = useDragValue({
|
|
1525
2314
|
axis,
|
|
1526
2315
|
baseElementRef: trackRef,
|
|
1527
|
-
cursor:
|
|
2316
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2317
|
+
shouldStart: () => !inactive,
|
|
2318
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1528
2319
|
updateOnPointerDown: true,
|
|
1529
2320
|
onChange: (v) => {
|
|
1530
|
-
if (
|
|
2321
|
+
if (inactive) return;
|
|
1531
2322
|
onChange?.(valueOf(v));
|
|
1532
2323
|
},
|
|
1533
2324
|
onDragStart: (v) => {
|
|
1534
|
-
if (
|
|
1535
|
-
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
2325
|
+
if (inactive) return;
|
|
1536
2326
|
thumbRef.current?.focus();
|
|
1537
2327
|
onDragStart?.(valueOf(v));
|
|
1538
2328
|
},
|
|
1539
2329
|
onDragEnd: (v) => {
|
|
1540
|
-
if (
|
|
1541
|
-
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
2330
|
+
if (inactive) return;
|
|
1542
2331
|
onDragEnd?.(valueOf(v));
|
|
1543
2332
|
}
|
|
1544
2333
|
});
|
|
1545
|
-
const
|
|
1546
|
-
if (!wheel || !onChange ||
|
|
2334
|
+
const wheelRefCallback = useWheel((event) => {
|
|
2335
|
+
if (!wheel || !onChange || inactive) return;
|
|
1547
2336
|
event.preventDefault();
|
|
1548
2337
|
let direction;
|
|
1549
|
-
if (!vertical && event.deltaX
|
|
2338
|
+
if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
1550
2339
|
else {
|
|
1551
|
-
if (event.deltaY
|
|
2340
|
+
if (event.deltaY === 0) return;
|
|
1552
2341
|
direction = event.deltaY > 0 ? -1 : 1;
|
|
1553
2342
|
}
|
|
1554
|
-
if (
|
|
1555
|
-
onChange(applyDelta(value, direction, wheel, axis));
|
|
1556
|
-
}, WHEEL_OPTIONS$1)
|
|
2343
|
+
if (reverse) direction *= -1;
|
|
2344
|
+
onChange(applyDelta(value, direction, wheel, axis, event));
|
|
2345
|
+
}, WHEEL_OPTIONS$1);
|
|
2346
|
+
const rootRefCallback = useComposedRefs(dragRefCallback, wheelRefCallback);
|
|
1557
2347
|
const context = useMemo(() => ({
|
|
1558
2348
|
value,
|
|
1559
2349
|
min,
|
|
@@ -1564,6 +2354,7 @@ const Slider = {
|
|
|
1564
2354
|
reverse,
|
|
1565
2355
|
disabled,
|
|
1566
2356
|
readonly,
|
|
2357
|
+
onChange,
|
|
1567
2358
|
percent,
|
|
1568
2359
|
trackRef,
|
|
1569
2360
|
thumbRef
|
|
@@ -1577,32 +2368,29 @@ const Slider = {
|
|
|
1577
2368
|
reverse,
|
|
1578
2369
|
disabled,
|
|
1579
2370
|
readonly,
|
|
2371
|
+
onChange,
|
|
1580
2372
|
percent
|
|
1581
2373
|
]);
|
|
1582
2374
|
useImperativeHandle(forwardedRef, () => {
|
|
1583
2375
|
return {
|
|
1584
2376
|
focus() {
|
|
1585
|
-
thumbRef.current?.focus();
|
|
2377
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1586
2378
|
},
|
|
1587
2379
|
blur() {
|
|
1588
2380
|
thumbRef.current?.blur();
|
|
1589
2381
|
}
|
|
1590
2382
|
};
|
|
1591
|
-
}, []);
|
|
2383
|
+
}, [disabled]);
|
|
1592
2384
|
return /* @__PURE__ */ jsx(SliderProvider, {
|
|
1593
2385
|
value: context,
|
|
1594
2386
|
children: /* @__PURE__ */ jsx("div", {
|
|
1595
|
-
className
|
|
2387
|
+
className,
|
|
1596
2388
|
ref: rootRefCallback,
|
|
2389
|
+
role: "group",
|
|
1597
2390
|
tabIndex: -1,
|
|
1598
|
-
|
|
1599
|
-
"
|
|
1600
|
-
"
|
|
1601
|
-
"aria-valuemax": max,
|
|
1602
|
-
"aria-orientation": vertical ? "vertical" : "horizontal",
|
|
1603
|
-
"aria-disabled": disabled,
|
|
1604
|
-
"aria-readonly": readonly,
|
|
1605
|
-
"data-vertical": vertical,
|
|
2391
|
+
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2392
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2393
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1606
2394
|
style,
|
|
1607
2395
|
onPointerDown,
|
|
1608
2396
|
onKeyDown: (event) => {
|
|
@@ -1610,7 +2398,7 @@ const Slider = {
|
|
|
1610
2398
|
onKeyDown?.(event);
|
|
1611
2399
|
},
|
|
1612
2400
|
onFocus: (event) => {
|
|
1613
|
-
thumbRef.current?.focus();
|
|
2401
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1614
2402
|
onFocus?.(event);
|
|
1615
2403
|
},
|
|
1616
2404
|
onBlur: (event) => {
|
|
@@ -1629,7 +2417,7 @@ const Slider = {
|
|
|
1629
2417
|
};
|
|
1630
2418
|
//#endregion
|
|
1631
2419
|
//#region src/components/XYPad/context.tsx
|
|
1632
|
-
const XYPadContext = createContext(null);
|
|
2420
|
+
const XYPadContext = /* @__PURE__ */ createContext(null);
|
|
1633
2421
|
const XYPadProvider = XYPadContext.Provider;
|
|
1634
2422
|
function useXYPadContext(selector) {
|
|
1635
2423
|
const context = useContext(XYPadContext);
|
|
@@ -1638,63 +2426,91 @@ function useXYPadContext(selector) {
|
|
|
1638
2426
|
}
|
|
1639
2427
|
//#endregion
|
|
1640
2428
|
//#region src/components/XYPad/Area.tsx
|
|
1641
|
-
function Area({ width
|
|
2429
|
+
const Area = /* @__PURE__ */ forwardRef(function Area({ width, height, color, children, className, style, ...props }, forwardedRef) {
|
|
1642
2430
|
const { areaRef } = useXYPadContext();
|
|
2431
|
+
const composedRef = useComposedRefs(forwardedRef, areaRef);
|
|
1643
2432
|
return /* @__PURE__ */ jsx("div", {
|
|
1644
|
-
ref:
|
|
1645
|
-
className
|
|
2433
|
+
ref: composedRef,
|
|
2434
|
+
className,
|
|
1646
2435
|
style: {
|
|
1647
2436
|
"--color": color,
|
|
1648
|
-
width,
|
|
1649
|
-
height,
|
|
2437
|
+
"--width": cssLength(width),
|
|
2438
|
+
"--height": cssLength(height),
|
|
2439
|
+
position: "relative",
|
|
1650
2440
|
...style
|
|
1651
2441
|
},
|
|
1652
2442
|
...props,
|
|
1653
|
-
children
|
|
2443
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
2444
|
+
name: "XYPad.Area",
|
|
2445
|
+
children
|
|
2446
|
+
})
|
|
1654
2447
|
});
|
|
1655
|
-
}
|
|
2448
|
+
});
|
|
1656
2449
|
//#endregion
|
|
1657
2450
|
//#region src/components/XYPad/Thumb.tsx
|
|
1658
|
-
function Thumb({ color, children,
|
|
1659
|
-
const
|
|
1660
|
-
const
|
|
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);
|
|
1661
2460
|
const methods = () => ({
|
|
1662
2461
|
focus() {
|
|
1663
|
-
|
|
2462
|
+
if (!disabled) xInputRef.current?.focus();
|
|
1664
2463
|
},
|
|
1665
2464
|
blur() {
|
|
1666
|
-
|
|
2465
|
+
xInputRef.current?.blur();
|
|
2466
|
+
yInputRef.current?.blur();
|
|
1667
2467
|
}
|
|
1668
2468
|
});
|
|
1669
|
-
useImperativeHandle(
|
|
1670
|
-
useImperativeHandle(thumbRef, methods, []);
|
|
1671
|
-
return /* @__PURE__ */
|
|
1672
|
-
className
|
|
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,
|
|
2475
|
+
...props,
|
|
1673
2476
|
style: {
|
|
2477
|
+
"--color": color,
|
|
2478
|
+
position: "absolute",
|
|
2479
|
+
translate: "var(--translate, -50% -50%)",
|
|
2480
|
+
zIndex: 100,
|
|
2481
|
+
...style,
|
|
1674
2482
|
left: `${percent[0]}%`,
|
|
1675
|
-
top: `${percent[1]}
|
|
1676
|
-
...wrapperStyle
|
|
2483
|
+
top: `${percent[1]}%`
|
|
1677
2484
|
},
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
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,
|
|
1684
2493
|
"aria-readonly": readonly,
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
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);
|
|
1688
2507
|
}
|
|
1689
|
-
})
|
|
2508
|
+
}, axis)), children]
|
|
1690
2509
|
});
|
|
1691
|
-
}
|
|
2510
|
+
});
|
|
1692
2511
|
//#endregion
|
|
1693
2512
|
//#region src/components/XYPad/index.tsx
|
|
1694
|
-
const defaultExternalStyles = {
|
|
1695
|
-
userSelectNone: true,
|
|
1696
|
-
cursor: "pointer"
|
|
1697
|
-
};
|
|
2513
|
+
const defaultExternalStyles = { cursor: "pointer" };
|
|
1698
2514
|
/**
|
|
1699
2515
|
* The wheel only acts while the focus is inside, so that scrolling a page past
|
|
1700
2516
|
* the control does not change its value.
|
|
@@ -1704,7 +2520,7 @@ const WHEEL_OPTIONS = { requireFocus: true };
|
|
|
1704
2520
|
* Simple XYPad
|
|
1705
2521
|
*/
|
|
1706
2522
|
const XYPad = {
|
|
1707
|
-
Root: forwardRef(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = linearScale, reverse: _reverse = false, wheel =
|
|
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) => {
|
|
1708
2524
|
const rootRef = useRef(null);
|
|
1709
2525
|
const areaRef = useRef(null);
|
|
1710
2526
|
const thumbRef = useRef(null);
|
|
@@ -1712,6 +2528,7 @@ const XYPad = {
|
|
|
1712
2528
|
...defaultExternalStyles,
|
|
1713
2529
|
..._externalStyles
|
|
1714
2530
|
};
|
|
2531
|
+
const inactive = disabled || readonly;
|
|
1715
2532
|
const min = useMemo(() => toXY(_min), [_min]);
|
|
1716
2533
|
const max = useMemo(() => toXY(_max), [_max]);
|
|
1717
2534
|
const step = useMemo(() => toXY(_step), [_step]);
|
|
@@ -1740,32 +2557,45 @@ const XYPad = {
|
|
|
1740
2557
|
scale,
|
|
1741
2558
|
reverse
|
|
1742
2559
|
]);
|
|
1743
|
-
|
|
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]);
|
|
1744
2575
|
/** @param i 0 = x, 1 = y */
|
|
1745
|
-
const nudge = useCallback((i, direction, option) => withAxis(i, applyDelta(value[i], direction, option, axis[i])), [
|
|
2576
|
+
const nudge = useCallback((i, direction, option, modifiers) => withAxis(i, applyDelta(value[i], direction, option, axis[i], modifiers)), [
|
|
1746
2577
|
value,
|
|
1747
2578
|
axis,
|
|
1748
2579
|
withAxis
|
|
1749
2580
|
]);
|
|
1750
2581
|
const handleKeyDown = useCallback((event) => {
|
|
1751
|
-
if (!onChange || readonly || !keyboard) return;
|
|
1752
2582
|
const key = event.key;
|
|
1753
|
-
if ([
|
|
2583
|
+
if (![
|
|
1754
2584
|
"ArrowRight",
|
|
1755
2585
|
"ArrowLeft",
|
|
1756
2586
|
"ArrowUp",
|
|
1757
2587
|
"ArrowDown"
|
|
1758
|
-
].includes(key))
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
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));
|
|
1766
2596
|
}, [
|
|
1767
2597
|
onChange,
|
|
1768
|
-
|
|
2598
|
+
inactive,
|
|
1769
2599
|
keyboard,
|
|
1770
2600
|
reverse,
|
|
1771
2601
|
nudge
|
|
@@ -1773,33 +2603,37 @@ const XYPad = {
|
|
|
1773
2603
|
const { refCallback: dragRefCallback } = useDragValue({
|
|
1774
2604
|
axis,
|
|
1775
2605
|
baseElementRef: areaRef,
|
|
2606
|
+
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1776
2607
|
updateOnPointerDown: true,
|
|
1777
|
-
cursor:
|
|
2608
|
+
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2609
|
+
shouldStart: () => !inactive,
|
|
1778
2610
|
onChange: (v) => {
|
|
1779
|
-
if (
|
|
2611
|
+
if (inactive) return;
|
|
1780
2612
|
onChange?.(v);
|
|
1781
2613
|
},
|
|
1782
2614
|
onDragStart: (v) => {
|
|
1783
|
-
if (
|
|
1784
|
-
if (externalStyles.userSelectNone) addUserSelectNone();
|
|
2615
|
+
if (inactive) return;
|
|
1785
2616
|
thumbRef.current?.focus();
|
|
1786
2617
|
onDragStart?.(v);
|
|
1787
2618
|
},
|
|
1788
2619
|
onDragEnd: (v) => {
|
|
1789
|
-
if (
|
|
1790
|
-
if (externalStyles.userSelectNone) removeUserSelectNone();
|
|
2620
|
+
if (inactive) return;
|
|
1791
2621
|
onDragEnd?.(v);
|
|
1792
2622
|
}
|
|
1793
2623
|
});
|
|
1794
|
-
const
|
|
1795
|
-
if (!onChange ||
|
|
1796
|
-
const
|
|
2624
|
+
const wheelRefCallback = useWheel((event) => {
|
|
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;
|
|
1797
2630
|
event.preventDefault();
|
|
1798
2631
|
let direction = 1;
|
|
1799
|
-
if (
|
|
2632
|
+
if (delta < 0) direction *= -1;
|
|
1800
2633
|
if (reverse[i]) direction *= -1;
|
|
1801
|
-
onChange(nudge(i, direction, wheel));
|
|
1802
|
-
}, WHEEL_OPTIONS)
|
|
2634
|
+
onChange(nudge(i, direction, wheel, event));
|
|
2635
|
+
}, WHEEL_OPTIONS);
|
|
2636
|
+
const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
|
|
1803
2637
|
const context = useMemo(() => ({
|
|
1804
2638
|
value,
|
|
1805
2639
|
min,
|
|
@@ -1809,6 +2643,7 @@ const XYPad = {
|
|
|
1809
2643
|
reverse,
|
|
1810
2644
|
disabled,
|
|
1811
2645
|
readonly,
|
|
2646
|
+
onChange,
|
|
1812
2647
|
percent,
|
|
1813
2648
|
areaRef,
|
|
1814
2649
|
thumbRef
|
|
@@ -1821,27 +2656,29 @@ const XYPad = {
|
|
|
1821
2656
|
reverse,
|
|
1822
2657
|
disabled,
|
|
1823
2658
|
readonly,
|
|
2659
|
+
onChange,
|
|
1824
2660
|
percent
|
|
1825
2661
|
]);
|
|
1826
2662
|
useImperativeHandle(forwardedRef, () => {
|
|
1827
2663
|
return {
|
|
1828
2664
|
focus() {
|
|
1829
|
-
thumbRef.current?.focus();
|
|
2665
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1830
2666
|
},
|
|
1831
2667
|
blur() {
|
|
1832
2668
|
thumbRef.current?.blur();
|
|
1833
2669
|
},
|
|
1834
2670
|
original: rootRef
|
|
1835
2671
|
};
|
|
1836
|
-
}, []);
|
|
2672
|
+
}, [disabled]);
|
|
1837
2673
|
return /* @__PURE__ */ jsx(XYPadProvider, {
|
|
1838
2674
|
value: context,
|
|
1839
2675
|
children: /* @__PURE__ */ jsx("div", {
|
|
1840
|
-
className
|
|
2676
|
+
className,
|
|
1841
2677
|
ref: rootRefCallback,
|
|
2678
|
+
role: "group",
|
|
1842
2679
|
tabIndex: -1,
|
|
1843
|
-
"
|
|
1844
|
-
"
|
|
2680
|
+
"data-disabled": disabled ? "" : void 0,
|
|
2681
|
+
"data-readonly": readonly ? "" : void 0,
|
|
1845
2682
|
style,
|
|
1846
2683
|
onPointerDown,
|
|
1847
2684
|
onKeyDown: (event) => {
|
|
@@ -1849,7 +2686,7 @@ const XYPad = {
|
|
|
1849
2686
|
onKeyDown?.(event);
|
|
1850
2687
|
},
|
|
1851
2688
|
onFocus: (event) => {
|
|
1852
|
-
thumbRef.current?.focus();
|
|
2689
|
+
if (!disabled) thumbRef.current?.focus();
|
|
1853
2690
|
onFocus?.(event);
|
|
1854
2691
|
},
|
|
1855
2692
|
onBlur: (event) => {
|
|
@@ -1868,67 +2705,153 @@ const XYPad = {
|
|
|
1868
2705
|
//#region src/hooks/useAnimationFrame.ts
|
|
1869
2706
|
function useAnimationFrame(callback = () => {}, deps = []) {
|
|
1870
2707
|
const reqIdRef = useRef(-1);
|
|
1871
|
-
const
|
|
1872
|
-
reqIdRef.current = requestAnimationFrame(loop);
|
|
1873
|
-
callback();
|
|
1874
|
-
}, [callback, ...deps]);
|
|
2708
|
+
const runCallback = useCallbackRef(callback);
|
|
1875
2709
|
useEffect(() => {
|
|
2710
|
+
const loop = () => {
|
|
2711
|
+
reqIdRef.current = requestAnimationFrame(loop);
|
|
2712
|
+
runCallback();
|
|
2713
|
+
};
|
|
1876
2714
|
reqIdRef.current = requestAnimationFrame(loop);
|
|
1877
2715
|
return () => cancelAnimationFrame(reqIdRef.current);
|
|
1878
|
-
}, [
|
|
2716
|
+
}, [runCallback, ...deps]);
|
|
1879
2717
|
}
|
|
1880
2718
|
//#endregion
|
|
1881
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
|
+
}
|
|
1882
2768
|
/**
|
|
1883
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.
|
|
1884
2777
|
*/
|
|
1885
2778
|
function useMIDIAccess(requestOnMount = true) {
|
|
1886
|
-
const
|
|
1887
|
-
const { midiAccess, error } = useSyncExternalStore(
|
|
2779
|
+
const store = useMemo(() => createMIDIAccessStore(), []);
|
|
2780
|
+
const { midiAccess, error, inputs } = useSyncExternalStore(store.subscribe, store.getState, store.getServerState);
|
|
1888
2781
|
useEffect(() => {
|
|
1889
|
-
|
|
1890
|
-
|
|
2782
|
+
const instance = store.connect();
|
|
2783
|
+
return () => store.disconnect(instance);
|
|
2784
|
+
}, [store]);
|
|
1891
2785
|
useEffect(() => {
|
|
1892
|
-
|
|
1893
|
-
}, [
|
|
2786
|
+
if (requestOnMount) store.request();
|
|
2787
|
+
}, [store, requestOnMount]);
|
|
1894
2788
|
return {
|
|
1895
|
-
request:
|
|
2789
|
+
request: store.request,
|
|
1896
2790
|
midiAccess,
|
|
1897
|
-
error
|
|
2791
|
+
error,
|
|
2792
|
+
inputs
|
|
1898
2793
|
};
|
|
1899
2794
|
}
|
|
1900
2795
|
//#endregion
|
|
1901
2796
|
//#region src/hooks/useMIDIInput.ts
|
|
1902
2797
|
/**
|
|
1903
|
-
*
|
|
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
|
+
* })
|
|
1904
2813
|
*/
|
|
1905
|
-
function useMIDIInput(midiAccess,
|
|
2814
|
+
function useMIDIInput(midiAccess, handlers) {
|
|
2815
|
+
const latest = useRef(handlers);
|
|
2816
|
+
const instanceRef = useRef(null);
|
|
1906
2817
|
useEffect(() => {
|
|
1907
|
-
const instance = createMIDIInput(midiAccess,
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
}, [
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
]);
|
|
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
|
+
});
|
|
1919
2829
|
}
|
|
1920
2830
|
//#endregion
|
|
1921
2831
|
//#region src/hooks/useMIDIMessage.ts
|
|
1922
2832
|
/**
|
|
1923
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.
|
|
1924
2838
|
*/
|
|
1925
2839
|
function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
2840
|
+
const latest = useRef(onMIDIMessage);
|
|
2841
|
+
const instanceRef = useRef(null);
|
|
1926
2842
|
useEffect(() => {
|
|
1927
|
-
const instance = createMIDIMessage(midiAccess,
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
+
});
|
|
1930
2853
|
}
|
|
1931
2854
|
//#endregion
|
|
1932
|
-
export { AnimationCanvas, Knob, NOT_SUPPORTED, NumberInput, PERMISSION_DENIED, Piano, PointsEditor, SHORTCUTS, Slider, XYPad, clampPoint, useAnimationFrame, useDrag, useDragValue, useEventListener, useInterval, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel };
|
|
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 };
|
|
1933
2856
|
|
|
1934
2857
|
//# sourceMappingURL=index.js.map
|