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