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