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