@tremolo-ui/react 0.6.0 → 0.8.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/index.cjs +522 -997
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -329
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +601 -329
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +487 -953
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/DropZone/index.tsx +101 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +139 -0
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +27 -21
- package/src/components/NumberInput/DecrementStepper.tsx +2 -13
- package/src/components/NumberInput/IncrementStepper.tsx +2 -14
- package/src/components/NumberInput/InputField.tsx +17 -38
- package/src/components/NumberInput/Stepper.tsx +44 -107
- package/src/components/NumberInput/StepperButton.tsx +16 -12
- package/src/components/NumberInput/context.tsx +8 -8
- package/src/components/NumberInput/index.tsx +41 -81
- package/src/components/Piano/index.tsx +39 -149
- package/src/components/PointsEditor/Background.tsx +2 -8
- package/src/components/PointsEditor/Container.tsx +17 -34
- package/src/components/PointsEditor/Point.tsx +41 -86
- package/src/components/PointsEditor/SelectionBox.tsx +2 -8
- package/src/components/PointsEditor/context.tsx +10 -61
- package/src/components/PointsEditor/index.tsx +31 -315
- package/src/components/Slider/Marks.tsx +8 -13
- package/src/components/Slider/MarksOption.tsx +11 -17
- package/src/components/Slider/Thumb.tsx +9 -15
- package/src/components/Slider/Track.tsx +7 -13
- package/src/components/Slider/index.tsx +33 -39
- package/src/components/XYPad/Area.tsx +26 -53
- package/src/components/XYPad/Thumb.tsx +8 -12
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +35 -50
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -147
- package/src/hooks/useDrag.ts +13 -4
- package/src/hooks/useDragValue.ts +17 -2
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +14 -36
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -44
- package/src/components/_util/css-length.ts +0 -13
- package/src/input-event.ts +0 -32
package/dist/index.cjs
CHANGED
|
@@ -92,114 +92,192 @@ const AnimationCanvas = /* @__PURE__ */ (0, react.forwardRef)(function Animation
|
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
94
|
//#endregion
|
|
95
|
-
//#region src/hooks/_internal/
|
|
96
|
-
/**
|
|
97
|
-
*
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
.
|
|
108
|
-
];
|
|
109
|
-
const NONE = {
|
|
110
|
-
shiftKey: false,
|
|
111
|
-
altKey: false,
|
|
112
|
-
ctrlKey: false,
|
|
113
|
-
metaKey: false
|
|
114
|
-
};
|
|
115
|
-
const MODIFIER_STATE = {
|
|
116
|
-
shift: {
|
|
117
|
-
...NONE,
|
|
118
|
-
shiftKey: true
|
|
119
|
-
},
|
|
120
|
-
alt: {
|
|
121
|
-
...NONE,
|
|
122
|
-
altKey: true
|
|
123
|
-
},
|
|
124
|
-
ctrl: {
|
|
125
|
-
...NONE,
|
|
126
|
-
ctrlKey: true
|
|
127
|
-
},
|
|
128
|
-
meta: {
|
|
129
|
-
...NONE,
|
|
130
|
-
metaKey: true
|
|
131
|
-
}
|
|
132
|
-
};
|
|
133
|
-
/** Each entry of a modifier-aware input option, as a name and held keys. */
|
|
134
|
-
function entries(options) {
|
|
135
|
-
if (Array.isArray(options)) return [{
|
|
136
|
-
name: "",
|
|
137
|
-
modifiers: NONE
|
|
138
|
-
}];
|
|
139
|
-
return Object.keys(options).map((key) => ({
|
|
140
|
-
name: key === "default" ? "" : key,
|
|
141
|
-
modifiers: MODIFIER_STATE[key] ?? NONE
|
|
142
|
-
}));
|
|
95
|
+
//#region src/hooks/_internal/useCallbackRef.ts
|
|
96
|
+
/**
|
|
97
|
+
* Internal
|
|
98
|
+
* @private
|
|
99
|
+
*/
|
|
100
|
+
function useCallbackRef(callback, deps = []) {
|
|
101
|
+
const callbackRef = (0, react.useRef)(() => {
|
|
102
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
103
|
+
});
|
|
104
|
+
(0, react.useInsertionEffect)(() => {
|
|
105
|
+
callbackRef.current = callback;
|
|
106
|
+
});
|
|
107
|
+
return (0, react.useCallback)((...args) => callbackRef.current?.(...args), deps);
|
|
143
108
|
}
|
|
109
|
+
//#endregion
|
|
110
|
+
//#region src/hooks/useDropZone.ts
|
|
144
111
|
/**
|
|
145
|
-
*
|
|
146
|
-
*
|
|
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.
|
|
147
116
|
*
|
|
148
|
-
*
|
|
149
|
-
* the whole point is that the amount, the step and the scale interact, and
|
|
150
|
-
* the pipeline is the only thing that knows how.
|
|
117
|
+
* @returns a ref callback for the element, and whether a drag is over it
|
|
151
118
|
*/
|
|
152
|
-
function
|
|
153
|
-
const
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
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
|
|
157
171
|
};
|
|
158
|
-
for (const { modifiers } of entries(options)) for (const position of PROBES) {
|
|
159
|
-
const value = scale.denormalize(position, min, max);
|
|
160
|
-
const up = (0, _tremolo_ui_functions.applyDelta)(value, 1, options, range, modifiers);
|
|
161
|
-
const down = (0, _tremolo_ui_functions.applyDelta)(value, -1, options, range, modifiers);
|
|
162
|
-
if (up !== value || down !== value) outcome.moved = true;
|
|
163
|
-
if (!format) continue;
|
|
164
|
-
const shown = format(value);
|
|
165
|
-
if (format(up) !== shown || format(down) !== shown) outcome.visible = true;
|
|
166
|
-
}
|
|
167
|
-
return outcome;
|
|
168
172
|
}
|
|
169
173
|
/**
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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/FileInput/context.tsx
|
|
196
|
+
const FileInputContext = /* @__PURE__ */ (0, react.createContext)(null);
|
|
197
|
+
const FileInputProvider = FileInputContext.Provider;
|
|
198
|
+
function useFileInputContext(selector) {
|
|
199
|
+
const context = (0, react.useContext)(FileInputContext);
|
|
200
|
+
if (!context) throw new Error("Missing FileInputContext.Provider in the tree");
|
|
201
|
+
return selector ? selector(context) : context;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
//#region src/components/FileInput/Trigger.tsx
|
|
205
|
+
/**
|
|
206
|
+
* Opens the file picker, and names the input for a screen reader.
|
|
180
207
|
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
208
|
+
* A `<label>` rather than a `<button>`: the browser forwards the click to the
|
|
209
|
+
* input on its own, the input stays the one thing in the tab order, and its
|
|
210
|
+
* accessible name comes from this text without an `aria-label` to keep in
|
|
211
|
+
* step. A disabled input ignores the click, so nothing has to be intercepted.
|
|
212
|
+
*/
|
|
213
|
+
const Trigger = /* @__PURE__ */ (0, react.forwardRef)(({ children, ...props }, ref) => {
|
|
214
|
+
const { inputId, disabled } = useFileInputContext();
|
|
215
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
|
|
216
|
+
ref,
|
|
217
|
+
htmlFor: inputId,
|
|
218
|
+
"data-disabled": disabled ? "" : void 0,
|
|
219
|
+
...props,
|
|
220
|
+
children
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
/**
|
|
224
|
+
* Picks files, and hands them over as `File`s.
|
|
225
|
+
*/
|
|
226
|
+
const FileInput = {
|
|
227
|
+
Root: /* @__PURE__ */ (0, react.forwardRef)(({ accept, multiple = false, disabled = false, onChange, onReject, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, ...props }, ref) => {
|
|
228
|
+
const inputId = (0, react.useId)();
|
|
229
|
+
const context = (0, react.useMemo)(() => ({
|
|
230
|
+
inputId,
|
|
231
|
+
disabled
|
|
232
|
+
}), [inputId, disabled]);
|
|
233
|
+
function handleChange(event) {
|
|
234
|
+
const { accepted, rejected } = (0, _tremolo_ui_dom.partitionByAccept)(Array.from(event.target.files ?? []), accept);
|
|
235
|
+
event.target.value = "";
|
|
236
|
+
if (accepted.length > 0) onChange?.(accepted);
|
|
237
|
+
if (rejected.length > 0) onReject?.(rejected);
|
|
238
|
+
}
|
|
239
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileInputProvider, {
|
|
240
|
+
value: context,
|
|
241
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
242
|
+
ref,
|
|
243
|
+
"data-disabled": disabled ? "" : void 0,
|
|
244
|
+
...props,
|
|
245
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
246
|
+
id: inputId,
|
|
247
|
+
type: "file",
|
|
248
|
+
accept,
|
|
249
|
+
multiple,
|
|
250
|
+
disabled,
|
|
251
|
+
style: _tremolo_ui_dom.visuallyHiddenStyle,
|
|
252
|
+
"aria-label": ariaLabel,
|
|
253
|
+
"aria-labelledby": ariaLabelledby,
|
|
254
|
+
"aria-describedby": ariaDescribedby,
|
|
255
|
+
onChange: handleChange
|
|
256
|
+
}), children]
|
|
257
|
+
})
|
|
258
|
+
});
|
|
259
|
+
}),
|
|
260
|
+
Trigger
|
|
261
|
+
};
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/hooks/_internal/useCheckSteps.ts
|
|
264
|
+
/**
|
|
265
|
+
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
266
|
+
* a change the user can see. The probing is `checkSteps` in the core.
|
|
184
267
|
*
|
|
185
268
|
* @internal
|
|
186
269
|
*/
|
|
187
|
-
function useCheckSteps(
|
|
270
|
+
function useCheckSteps(options) {
|
|
271
|
+
const { component, axis, range, keyboard, wheel } = options;
|
|
188
272
|
const { min, max, step, scale } = range ?? {};
|
|
189
|
-
const where = axis ? `${component} (${axis})` : component;
|
|
190
273
|
(0, react.useEffect)(() => {
|
|
191
274
|
try {
|
|
192
275
|
if (process.env.NODE_ENV === "production") return;
|
|
193
|
-
|
|
194
|
-
for (const [name, options] of [["keyboard", keyboard], ["wheel", wheel]]) {
|
|
195
|
-
if (!options) continue;
|
|
196
|
-
const { moved, visible } = probe(options, range, format);
|
|
197
|
-
if (!moved) console.warn(`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` + (step !== void 0 ? ` Each press is smaller than \`step\` (${step}), so it rounds` : " Each press rounds") + " straight back to where it started.");
|
|
198
|
-
else if (format && !visible) console.warn(`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\` shows the same text before and after, everywhere in the range. The display is too coarse for it to be seen.`);
|
|
199
|
-
}
|
|
276
|
+
for (const warning of (0, _tremolo_ui_dom.checkSteps)(options)) console.warn(warning);
|
|
200
277
|
} catch {}
|
|
201
278
|
}, [
|
|
202
|
-
|
|
279
|
+
component,
|
|
280
|
+
axis,
|
|
203
281
|
min,
|
|
204
282
|
max,
|
|
205
283
|
step,
|
|
@@ -209,21 +287,6 @@ function useCheckSteps({ component, axis, range, keyboard, wheel, format }) {
|
|
|
209
287
|
]);
|
|
210
288
|
}
|
|
211
289
|
//#endregion
|
|
212
|
-
//#region src/hooks/_internal/useCallbackRef.ts
|
|
213
|
-
/**
|
|
214
|
-
* Internal
|
|
215
|
-
* @private
|
|
216
|
-
*/
|
|
217
|
-
function useCallbackRef(callback, deps = []) {
|
|
218
|
-
const callbackRef = (0, react.useRef)(() => {
|
|
219
|
-
throw new Error("Cannot call an event handler while rendering.");
|
|
220
|
-
});
|
|
221
|
-
(0, react.useInsertionEffect)(() => {
|
|
222
|
-
callbackRef.current = callback;
|
|
223
|
-
});
|
|
224
|
-
return (0, react.useCallback)((...args) => callbackRef.current?.(...args), deps);
|
|
225
|
-
}
|
|
226
|
-
//#endregion
|
|
227
290
|
//#region src/hooks/useDragValue.ts
|
|
228
291
|
/**
|
|
229
292
|
* Drive a value with a pointer drag.
|
|
@@ -322,49 +385,6 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
|
322
385
|
return setNode;
|
|
323
386
|
}
|
|
324
387
|
//#endregion
|
|
325
|
-
//#region src/input-event.ts
|
|
326
|
-
/**
|
|
327
|
-
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
328
|
-
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
329
|
-
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
330
|
-
* `useCheckSteps` warns about.
|
|
331
|
-
*
|
|
332
|
-
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
333
|
-
* amount move at all — see `applyDelta` in `@tremolo-ui/functions`.
|
|
334
|
-
*/
|
|
335
|
-
const DEFAULT_KEYBOARD_OPTIONS = {
|
|
336
|
-
default: ["raw", 1],
|
|
337
|
-
shift: ["raw", .1]
|
|
338
|
-
};
|
|
339
|
-
/**
|
|
340
|
-
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
341
|
-
*
|
|
342
|
-
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
343
|
-
* and fills `deltaX`, so no modifier is bound here.
|
|
344
|
-
*/
|
|
345
|
-
const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
|
|
346
|
-
/**
|
|
347
|
-
* The drag sensitivity used by value controls by default. Shift makes the
|
|
348
|
-
* same movement cover a tenth of the range, matching the arrow keys.
|
|
349
|
-
*/
|
|
350
|
-
const DEFAULT_DRAG_SENSITIVITY = {
|
|
351
|
-
default: 1,
|
|
352
|
-
shift: .1
|
|
353
|
-
};
|
|
354
|
-
//#endregion
|
|
355
|
-
//#region src/components/_util/css-length.ts
|
|
356
|
-
/**
|
|
357
|
-
* A number as a CSS length, for a custom property.
|
|
358
|
-
*
|
|
359
|
-
* React appends `px` to a bare number for the properties it knows take a
|
|
360
|
-
* length, and a custom property is never one of them: `--size: 50` would come
|
|
361
|
-
* out as the invalid `50`. Anything already a string is passed through, so
|
|
362
|
-
* `'3rem'` and `'100%'` still work.
|
|
363
|
-
*/
|
|
364
|
-
function cssLength(value) {
|
|
365
|
-
return typeof value === "number" ? `${value}px` : value;
|
|
366
|
-
}
|
|
367
|
-
//#endregion
|
|
368
388
|
//#region src/components/_util/Placement.tsx
|
|
369
389
|
/**
|
|
370
390
|
* The subcomponent that established the current placement, if any.
|
|
@@ -406,49 +426,8 @@ function useCheckPlacement(child, parent) {
|
|
|
406
426
|
parent
|
|
407
427
|
]);
|
|
408
428
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
*
|
|
412
|
-
* 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
|
|
413
|
-
* それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
|
|
414
|
-
*/
|
|
415
|
-
function pointOnArc(angle, radius) {
|
|
416
|
-
return {
|
|
417
|
-
x: 50 + radius * Math.cos((0, _tremolo_ui_functions.radian)(angle - 90)),
|
|
418
|
-
y: 50 + radius * Math.sin((0, _tremolo_ui_functions.radian)(angle - 90))
|
|
419
|
-
};
|
|
420
|
-
}
|
|
421
|
-
/** 線の太さが viewBox からはみ出さないようにした半径 */
|
|
422
|
-
function arcRadius(strokeWidth) {
|
|
423
|
-
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
424
|
-
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
425
|
-
}
|
|
426
|
-
/** Build an SVG path for an arc, splitting full turns into drawable segments. */
|
|
427
|
-
function arcPath(startAngle, endAngle, radius) {
|
|
428
|
-
const start = pointOnArc(startAngle, radius);
|
|
429
|
-
const sweep = endAngle - startAngle;
|
|
430
|
-
const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180));
|
|
431
|
-
const segmentSweep = sweep / segmentCount;
|
|
432
|
-
let path = `M ${start.x} ${start.y}`;
|
|
433
|
-
for (let i = 1; i <= segmentCount; i += 1) {
|
|
434
|
-
const end = pointOnArc(startAngle + segmentSweep * i, radius);
|
|
435
|
-
path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`;
|
|
436
|
-
}
|
|
437
|
-
return path;
|
|
438
|
-
}
|
|
439
|
-
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
440
|
-
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
441
|
-
const p = scale.normalize(value, min, max);
|
|
442
|
-
const s = scale.normalize(startValue, min, max);
|
|
443
|
-
const r1 = -angleRange / 2;
|
|
444
|
-
return {
|
|
445
|
-
p,
|
|
446
|
-
r1,
|
|
447
|
-
r2: r1 + Math.min(p, s) * angleRange,
|
|
448
|
-
r3: r1 + Math.max(p, s) * angleRange,
|
|
449
|
-
r4: angleRange / 2
|
|
450
|
-
};
|
|
451
|
-
}
|
|
429
|
+
//#endregion
|
|
430
|
+
//#region src/components/Knob/context.tsx
|
|
452
431
|
const KnobContext = /* @__PURE__ */ (0, react.createContext)(null);
|
|
453
432
|
const KnobProvider = KnobContext.Provider;
|
|
454
433
|
function useKnobContext(selector) {
|
|
@@ -462,10 +441,10 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
|
|
|
462
441
|
useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
|
|
463
442
|
const r2 = useKnobContext((s) => s.r2);
|
|
464
443
|
const r3 = useKnobContext((s) => s.r3);
|
|
465
|
-
const radius =
|
|
444
|
+
const radius = (0, _tremolo_ui_dom.knobArcRadius)(strokeWidth);
|
|
466
445
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
467
446
|
className,
|
|
468
|
-
d:
|
|
447
|
+
d: (0, _tremolo_ui_dom.knobArcPath)(r2, r3, radius),
|
|
469
448
|
fill: "none",
|
|
470
449
|
stroke,
|
|
471
450
|
strokeWidth,
|
|
@@ -483,17 +462,17 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
483
462
|
const r2 = useKnobContext((s) => s.r2);
|
|
484
463
|
const r3 = useKnobContext((s) => s.r3);
|
|
485
464
|
const r4 = useKnobContext((s) => s.r4);
|
|
486
|
-
const radius =
|
|
465
|
+
const radius = (0, _tremolo_ui_dom.knobArcRadius)(strokeWidth);
|
|
487
466
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [startValue > min && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
488
467
|
className,
|
|
489
|
-
d:
|
|
468
|
+
d: (0, _tremolo_ui_dom.knobArcPath)(r1, r2, radius),
|
|
490
469
|
fill: "none",
|
|
491
470
|
stroke,
|
|
492
471
|
strokeWidth,
|
|
493
472
|
...props
|
|
494
473
|
}), startValue < max && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
495
474
|
className,
|
|
496
|
-
d:
|
|
475
|
+
d: (0, _tremolo_ui_dom.knobArcPath)(r3, r4, radius),
|
|
497
476
|
fill: "none",
|
|
498
477
|
stroke,
|
|
499
478
|
strokeWidth,
|
|
@@ -504,7 +483,7 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
504
483
|
//#region src/components/Knob/SVGRoot.tsx
|
|
505
484
|
function SVGRoot({ children, style, ...props }) {
|
|
506
485
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
507
|
-
viewBox: `0 0
|
|
486
|
+
viewBox: `0 0 ${_tremolo_ui_dom.KNOB_VIEWBOX_SIZE} ${_tremolo_ui_dom.KNOB_VIEWBOX_SIZE}`,
|
|
508
487
|
style: {
|
|
509
488
|
display: "block",
|
|
510
489
|
...style
|
|
@@ -534,7 +513,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
534
513
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
|
|
535
514
|
className: classes?.thumbLine,
|
|
536
515
|
x1: "50%",
|
|
537
|
-
y1: `${(
|
|
516
|
+
y1: `${(_tremolo_ui_dom.KNOB_VIEWBOX_SIZE - (0, _tremolo_ui_functions.clamp)(thumbSize, 0, 100)) / 2}%`,
|
|
538
517
|
x2: "50%",
|
|
539
518
|
y2: `${thumbLineLength}%`,
|
|
540
519
|
stroke: thumbLine,
|
|
@@ -558,7 +537,7 @@ const WHEEL_OPTIONS$2 = { requireFocus: true };
|
|
|
558
537
|
* Interactive rotary knob component implemented in SVG.
|
|
559
538
|
*/
|
|
560
539
|
const Knob = {
|
|
561
|
-
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
|
|
540
|
+
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, defaultValue = min, startValue = min, size, externalStyles: _externalStyles, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, pointerLock = false, enableDoubleClickDefault = true, disabled = false, readonly = false, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, style, children, ...props }, forwardedRef) => {
|
|
562
541
|
const elmRef = (0, react.useRef)(null);
|
|
563
542
|
const externalStyles = {
|
|
564
543
|
...defaultExternalStyles$2,
|
|
@@ -584,16 +563,10 @@ const Knob = {
|
|
|
584
563
|
});
|
|
585
564
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
586
565
|
if (!keyboard || !onChange || inactive) return;
|
|
587
|
-
const
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
"ArrowUp",
|
|
592
|
-
"ArrowDown"
|
|
593
|
-
].includes(key)) {
|
|
594
|
-
event.preventDefault();
|
|
595
|
-
onChange((0, _tremolo_ui_functions.applyDelta)(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
|
|
596
|
-
}
|
|
566
|
+
const direction = (0, _tremolo_ui_dom.arrowKeyDirection)(event.key);
|
|
567
|
+
if (direction === null) return;
|
|
568
|
+
event.preventDefault();
|
|
569
|
+
onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, keyboard, range, event));
|
|
597
570
|
}, [
|
|
598
571
|
keyboard,
|
|
599
572
|
onChange,
|
|
@@ -607,7 +580,7 @@ const Knob = {
|
|
|
607
580
|
reverse: true
|
|
608
581
|
}], [range]),
|
|
609
582
|
getValue: () => [value, value],
|
|
610
|
-
sensitivity: (state) => (0,
|
|
583
|
+
sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
|
|
611
584
|
threshold: 1,
|
|
612
585
|
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
613
586
|
pointerLock: inactive ? false : pointerLock,
|
|
@@ -619,9 +592,11 @@ const Knob = {
|
|
|
619
592
|
});
|
|
620
593
|
const wheelRefCallback = useWheel((event) => {
|
|
621
594
|
if (!wheel || inactive) return;
|
|
595
|
+
const direction = (0, _tremolo_ui_dom.wheelDirection)(event);
|
|
596
|
+
if (direction === null) return;
|
|
622
597
|
event.preventDefault();
|
|
623
|
-
if (!onChange
|
|
624
|
-
onChange((0,
|
|
598
|
+
if (!onChange) return;
|
|
599
|
+
onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, wheel, range, event));
|
|
625
600
|
}, WHEEL_OPTIONS$2);
|
|
626
601
|
const rootRefCallback = require_compose_refs.useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
|
|
627
602
|
const context = (0, react.useMemo)(() => {
|
|
@@ -636,7 +611,7 @@ const Knob = {
|
|
|
636
611
|
};
|
|
637
612
|
return {
|
|
638
613
|
...config,
|
|
639
|
-
...
|
|
614
|
+
...(0, _tremolo_ui_dom.knobAngles)(config)
|
|
640
615
|
};
|
|
641
616
|
}, [
|
|
642
617
|
value,
|
|
@@ -673,7 +648,7 @@ const Knob = {
|
|
|
673
648
|
"data-readonly": readonly ? "" : void 0,
|
|
674
649
|
"data-dragging": dragging ? "" : void 0,
|
|
675
650
|
style: {
|
|
676
|
-
"--knob-size": cssLength(size),
|
|
651
|
+
"--knob-size": (0, _tremolo_ui_dom.cssLength)(size),
|
|
677
652
|
...style
|
|
678
653
|
},
|
|
679
654
|
onPointerDown,
|
|
@@ -710,82 +685,36 @@ function useStepperContext() {
|
|
|
710
685
|
return (0, react.useContext)(StepperContext);
|
|
711
686
|
}
|
|
712
687
|
//#endregion
|
|
713
|
-
//#region src/hooks/
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
cleanupRef.current = cleanup;
|
|
725
|
-
return () => {
|
|
726
|
-
cleanup();
|
|
727
|
-
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
728
|
-
};
|
|
729
|
-
}, [
|
|
730
|
-
event,
|
|
731
|
-
target,
|
|
732
|
-
options,
|
|
733
|
-
listener
|
|
734
|
-
]);
|
|
735
|
-
return (0, react.useCallback)(() => cleanupRef.current(), []);
|
|
736
|
-
}
|
|
737
|
-
//#endregion
|
|
738
|
-
//#region src/hooks/useInterval.ts
|
|
739
|
-
function useInterval(callback, delay) {
|
|
740
|
-
const savedCallback = (0, react.useRef)(callback);
|
|
688
|
+
//#region src/hooks/useLongPress.ts
|
|
689
|
+
/**
|
|
690
|
+
* Repeat `callback` while a pointer is held down: once on the press, then
|
|
691
|
+
* every `interval` after `initialDelay`. The repeat itself is
|
|
692
|
+
* `createLongPress` in `@tremolo-ui/dom`.
|
|
693
|
+
*
|
|
694
|
+
* Returns the function that starts a press, for an `onPointerDown`.
|
|
695
|
+
*/
|
|
696
|
+
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
697
|
+
const onPress = useCallbackRef(callback);
|
|
698
|
+
const instanceRef = (0, react.useRef)(null);
|
|
741
699
|
(0, react.useEffect)(() => {
|
|
742
|
-
|
|
743
|
-
|
|
700
|
+
instanceRef.current?.update({
|
|
701
|
+
delay: initialDelay,
|
|
702
|
+
interval
|
|
703
|
+
});
|
|
704
|
+
});
|
|
744
705
|
(0, react.useEffect)(() => {
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
706
|
+
const instance = (0, _tremolo_ui_dom.createLongPress)({
|
|
707
|
+
onPress: () => onPress(),
|
|
708
|
+
delay: initialDelay,
|
|
709
|
+
interval
|
|
710
|
+
});
|
|
711
|
+
instanceRef.current = instance;
|
|
749
712
|
return () => {
|
|
750
|
-
|
|
713
|
+
instanceRef.current = null;
|
|
714
|
+
instance.destroy();
|
|
751
715
|
};
|
|
752
|
-
}, [
|
|
753
|
-
|
|
754
|
-
//#endregion
|
|
755
|
-
//#region src/hooks/useLongPress.ts
|
|
756
|
-
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
757
|
-
const [pressed, setPressed] = (0, react.useState)(false);
|
|
758
|
-
const [delay, setDelay] = (0, react.useState)(initialDelay);
|
|
759
|
-
const activePointerId = (0, react.useRef)(null);
|
|
760
|
-
const isPressed = (0, react.useRef)(false);
|
|
761
|
-
useInterval(() => {
|
|
762
|
-
callback();
|
|
763
|
-
setDelay(interval);
|
|
764
|
-
}, pressed ? delay : null);
|
|
765
|
-
const stop = (0, react.useCallback)((pointerId) => {
|
|
766
|
-
if (!isPressed.current) return;
|
|
767
|
-
if (pointerId !== void 0 && activePointerId.current !== null && activePointerId.current !== pointerId) return;
|
|
768
|
-
isPressed.current = false;
|
|
769
|
-
activePointerId.current = null;
|
|
770
|
-
setPressed(false);
|
|
771
|
-
setDelay(initialDelay);
|
|
772
|
-
}, [initialDelay]);
|
|
773
|
-
useEventListener(globalThis.window, "pointerup", (event) => {
|
|
774
|
-
stop(event.pointerId);
|
|
775
|
-
});
|
|
776
|
-
useEventListener(globalThis.window, "pointercancel", (event) => {
|
|
777
|
-
stop(event.pointerId);
|
|
778
|
-
});
|
|
779
|
-
useEventListener(globalThis.window, "blur", () => {
|
|
780
|
-
stop();
|
|
781
|
-
});
|
|
782
|
-
return (0, react.useCallback)((event) => {
|
|
783
|
-
if (isPressed.current || event && event.button !== 0) return;
|
|
784
|
-
isPressed.current = true;
|
|
785
|
-
activePointerId.current = event?.pointerId ?? null;
|
|
786
|
-
callback();
|
|
787
|
-
setPressed(true);
|
|
788
|
-
}, [callback]);
|
|
716
|
+
}, []);
|
|
717
|
+
return (0, react.useCallback)((event) => instanceRef.current?.start(event), []);
|
|
789
718
|
}
|
|
790
719
|
//#endregion
|
|
791
720
|
//#region src/components/NumberInput/StepperButton.tsx
|
|
@@ -797,7 +726,7 @@ function StepperButton({ direction, variant, icon, className, style, children, o
|
|
|
797
726
|
const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
798
727
|
const stepper = useStepperContext();
|
|
799
728
|
const press = useLongPress(() => {
|
|
800
|
-
if (stepper?.
|
|
729
|
+
if (stepper?.moved()) return;
|
|
801
730
|
nudge(direction, ["raw", step]);
|
|
802
731
|
});
|
|
803
732
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -856,21 +785,6 @@ function IncrementStepper(props) {
|
|
|
856
785
|
}
|
|
857
786
|
//#endregion
|
|
858
787
|
//#region src/components/NumberInput/InputField.tsx
|
|
859
|
-
/** The leading number of the displayed text, whatever the format put around it. */
|
|
860
|
-
const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
|
|
861
|
-
const numberEnd = (text) => text.match(NUMBER_PREFIX)?.[0].length ?? text.length;
|
|
862
|
-
/**
|
|
863
|
-
* The index the caret is measured against: the decimal point, or where one
|
|
864
|
-
* would go if the number has none.
|
|
865
|
-
*
|
|
866
|
-
* Measuring from an end instead would slide the caret across a digit whenever
|
|
867
|
-
* the number changed length — `9.9` to `10.0` gains a character in front, `10`
|
|
868
|
-
* to `9` loses one — which is exactly what stepping does.
|
|
869
|
-
*/
|
|
870
|
-
const decimalAnchor = (text) => {
|
|
871
|
-
const dot = text.indexOf(".");
|
|
872
|
-
return dot === -1 ? numberEnd(text) : dot;
|
|
873
|
-
};
|
|
874
788
|
/**
|
|
875
789
|
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
876
790
|
*
|
|
@@ -880,7 +794,7 @@ const decimalAnchor = (text) => {
|
|
|
880
794
|
* How it behaves on focus, Enter and the arrow keys is set on `Root`
|
|
881
795
|
* (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
|
|
882
796
|
*/
|
|
883
|
-
const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ className, style, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
797
|
+
const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ className, style, onChange, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
884
798
|
const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
885
799
|
const [focused, setFocused] = (0, react.useState)(false);
|
|
886
800
|
const shown = unformatOnFocus && focused && !editing ? String(value) : text;
|
|
@@ -889,7 +803,10 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
|
|
|
889
803
|
const input = inputRef.current;
|
|
890
804
|
if (!input) return;
|
|
891
805
|
if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
|
|
892
|
-
else
|
|
806
|
+
else {
|
|
807
|
+
const span = (0, _tremolo_ui_dom.numberSpan)(input.value);
|
|
808
|
+
if (span) input.setSelectionRange(span.start, span.end);
|
|
809
|
+
}
|
|
893
810
|
}, [
|
|
894
811
|
focused,
|
|
895
812
|
selectOnFocus,
|
|
@@ -906,8 +823,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
|
|
|
906
823
|
caretAfterStep.current = null;
|
|
907
824
|
const input = inputRef.current;
|
|
908
825
|
if (!input || input.value === pending.from) return;
|
|
909
|
-
const
|
|
910
|
-
const caret = Math.max(0, Math.min(place, numberEnd(input.value)));
|
|
826
|
+
const caret = (0, _tremolo_ui_dom.caretAtDecimalOffset)(input.value, pending.offset);
|
|
911
827
|
input.setSelectionRange(caret, caret);
|
|
912
828
|
});
|
|
913
829
|
const composedRef = require_compose_refs.useComposedRefs(forwardedRef, inputRef);
|
|
@@ -934,6 +850,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
|
|
|
934
850
|
onChange: (event) => {
|
|
935
851
|
caretAfterStep.current = null;
|
|
936
852
|
setDraft(event.currentTarget.value);
|
|
853
|
+
onChange?.(event);
|
|
937
854
|
},
|
|
938
855
|
onFocus: (event) => {
|
|
939
856
|
setFocused(true);
|
|
@@ -953,7 +870,7 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
|
|
|
953
870
|
event.preventDefault();
|
|
954
871
|
const input = event.currentTarget;
|
|
955
872
|
if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
|
|
956
|
-
offset: input.
|
|
873
|
+
offset: (0, _tremolo_ui_dom.caretDecimalOffset)(input.value, input.selectionStart),
|
|
957
874
|
from: input.value
|
|
958
875
|
};
|
|
959
876
|
nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
|
|
@@ -964,117 +881,54 @@ const InputField = /* @__PURE__ */ (0, react.forwardRef)(function InputField({ c
|
|
|
964
881
|
});
|
|
965
882
|
});
|
|
966
883
|
//#endregion
|
|
967
|
-
//#region src/hooks/useDrag.ts
|
|
968
|
-
/**
|
|
969
|
-
* Track a pointer drag on an element.
|
|
970
|
-
*
|
|
971
|
-
* @returns a ref callback to attach to the element being dragged
|
|
972
|
-
*/
|
|
973
|
-
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
974
|
-
const dragHandler = useCallbackRef(onDrag);
|
|
975
|
-
const dragStartHandler = useCallbackRef(onDragStart);
|
|
976
|
-
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
977
|
-
const [node, setNode] = (0, react.useState)(null);
|
|
978
|
-
(0, react.useEffect)(() => {
|
|
979
|
-
if (!node) return;
|
|
980
|
-
const instance = (0, _tremolo_ui_dom.createDrag)(node, {
|
|
981
|
-
threshold,
|
|
982
|
-
cursor,
|
|
983
|
-
pointerLock,
|
|
984
|
-
onDragStart: (state) => dragStartHandler(state),
|
|
985
|
-
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
986
|
-
onDragEnd: (state) => dragEndHandler(state)
|
|
987
|
-
});
|
|
988
|
-
return () => instance.destroy();
|
|
989
|
-
}, [
|
|
990
|
-
node,
|
|
991
|
-
threshold,
|
|
992
|
-
cursor,
|
|
993
|
-
pointerLock,
|
|
994
|
-
dragHandler,
|
|
995
|
-
dragStartHandler,
|
|
996
|
-
dragEndHandler
|
|
997
|
-
]);
|
|
998
|
-
return setNode;
|
|
999
|
-
}
|
|
1000
|
-
//#endregion
|
|
1001
884
|
//#region src/components/NumberInput/Stepper.tsx
|
|
1002
885
|
/**
|
|
1003
886
|
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
1004
|
-
* up and down moves the value one `step` every `drag` pixels.
|
|
887
|
+
* up and down moves the value one `step` every `drag` pixels. The counting is
|
|
888
|
+
* `createStepperDrag` in the core.
|
|
1005
889
|
*
|
|
1006
890
|
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
1007
891
|
* off text selection on whatever element it is attached to.
|
|
1008
892
|
*/
|
|
1009
893
|
const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ className, style, children, ...props }, forwardedRef) {
|
|
1010
|
-
const { value,
|
|
1011
|
-
const
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
* off the grid.
|
|
1018
|
-
*/
|
|
1019
|
-
const dragOptions = (0, react.useMemo)(() => (0, _tremolo_ui_functions.mapModifier)(dragSensitivity, (factor) => ["raw", step * factor]), [dragSensitivity, step]);
|
|
1020
|
-
/**
|
|
1021
|
-
* Set once the drag has actually moved the value, so that the press-and-hold
|
|
1022
|
-
* repeat of a stepper stands down and leaves the value to the drag. A ref,
|
|
1023
|
-
* because it is read from inside the repeat rather than rendered.
|
|
1024
|
-
*/
|
|
1025
|
-
const draggingRef = (0, react.useRef)(false);
|
|
1026
|
-
/**
|
|
1027
|
-
* Where the drag started, taken on the first move rather than on pointer
|
|
1028
|
-
* down: the steppers act on pointer down, so by then the value may already
|
|
1029
|
-
* have been nudged once, and the drag should carry on from there.
|
|
1030
|
-
*/
|
|
1031
|
-
const originRef = (0, react.useRef)(null);
|
|
1032
|
-
/**
|
|
1033
|
-
* Which modifier the drag is currently counting at, and where the previous
|
|
1034
|
-
* event was — a key produces no pointer event of its own, so a change is
|
|
1035
|
-
* only seen on the next move and has to be dated back to the one before it.
|
|
1036
|
-
*/
|
|
1037
|
-
const factorRef = (0, react.useRef)(1);
|
|
1038
|
-
const previousYRef = (0, react.useRef)(0);
|
|
1039
|
-
const dragRefCallback = useDrag({
|
|
1040
|
-
threshold: 1,
|
|
1041
|
-
cursor: inactive ? void 0 : "ns-resize",
|
|
1042
|
-
pointerLock: inactive ? false : pointerLock,
|
|
1043
|
-
onDragStart: (state) => {
|
|
1044
|
-
originRef.current = null;
|
|
1045
|
-
draggingRef.current = false;
|
|
1046
|
-
factorRef.current = (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value;
|
|
1047
|
-
},
|
|
1048
|
-
onDrag: (_x, y, _dx, _dy, state) => {
|
|
1049
|
-
if (inactive || drag === null) return;
|
|
1050
|
-
if (!originRef.current) {
|
|
1051
|
-
originRef.current = {
|
|
1052
|
-
y,
|
|
1053
|
-
value
|
|
1054
|
-
};
|
|
1055
|
-
previousYRef.current = y;
|
|
1056
|
-
return;
|
|
1057
|
-
}
|
|
1058
|
-
const factor = (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value;
|
|
1059
|
-
if (factor !== factorRef.current) {
|
|
1060
|
-
originRef.current = {
|
|
1061
|
-
y: previousYRef.current,
|
|
1062
|
-
value
|
|
1063
|
-
};
|
|
1064
|
-
factorRef.current = factor;
|
|
1065
|
-
}
|
|
1066
|
-
previousYRef.current = y;
|
|
1067
|
-
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
1068
|
-
if (steps === 0) return;
|
|
1069
|
-
draggingRef.current = true;
|
|
1070
|
-
changeValue((0, _tremolo_ui_functions.applyDelta)(originRef.current.value, steps, dragOptions, rawRange, state.event));
|
|
1071
|
-
},
|
|
1072
|
-
onDragEnd: () => {
|
|
1073
|
-
draggingRef.current = false;
|
|
1074
|
-
}
|
|
894
|
+
const { value, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
|
|
895
|
+
const enabled = drag !== null && !disabled && !readonly;
|
|
896
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
897
|
+
const instanceRef = (0, react.useRef)(null);
|
|
898
|
+
const latest = (0, react.useRef)({
|
|
899
|
+
value,
|
|
900
|
+
changeValue
|
|
1075
901
|
});
|
|
1076
|
-
const
|
|
1077
|
-
|
|
902
|
+
const options = {
|
|
903
|
+
range: rawRange,
|
|
904
|
+
pixels: drag ?? 1,
|
|
905
|
+
sensitivity: dragSensitivity,
|
|
906
|
+
pointerLock
|
|
907
|
+
};
|
|
908
|
+
const optionsRef = (0, react.useRef)(options);
|
|
909
|
+
(0, react.useEffect)(() => {
|
|
910
|
+
latest.current = {
|
|
911
|
+
value,
|
|
912
|
+
changeValue
|
|
913
|
+
};
|
|
914
|
+
optionsRef.current = options;
|
|
915
|
+
instanceRef.current?.update(options);
|
|
916
|
+
});
|
|
917
|
+
(0, react.useEffect)(() => {
|
|
918
|
+
if (!node || !enabled) return;
|
|
919
|
+
const instance = (0, _tremolo_ui_dom.createStepperDrag)(node, {
|
|
920
|
+
...optionsRef.current,
|
|
921
|
+
getValue: () => latest.current.value,
|
|
922
|
+
onChange: (next) => latest.current.changeValue(next)
|
|
923
|
+
});
|
|
924
|
+
instanceRef.current = instance;
|
|
925
|
+
return () => {
|
|
926
|
+
instanceRef.current = null;
|
|
927
|
+
instance.destroy();
|
|
928
|
+
};
|
|
929
|
+
}, [node, enabled]);
|
|
930
|
+
const composedRef = require_compose_refs.useComposedRefs(forwardedRef, setNode);
|
|
931
|
+
const context = (0, react.useMemo)(() => ({ moved: () => instanceRef.current?.moved() ?? false }), []);
|
|
1078
932
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(StepperProvider, {
|
|
1079
933
|
value: context,
|
|
1080
934
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -1088,22 +942,12 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
|
|
|
1088
942
|
});
|
|
1089
943
|
//#endregion
|
|
1090
944
|
//#region src/components/NumberInput/index.tsx
|
|
1091
|
-
/**
|
|
1092
|
-
* A number at the start of the text, and nothing read after it. A half-typed
|
|
1093
|
-
* entry still yields the number in front of it, but text with no number is
|
|
1094
|
-
* `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
|
|
1095
|
-
*/
|
|
1096
|
-
const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;
|
|
1097
945
|
const defaultFormat = (value) => String(value);
|
|
1098
|
-
const defaultParse = (text) => {
|
|
1099
|
-
const match = text.match(LEADING_NUMBER);
|
|
1100
|
-
return match ? Number(match[1]) : NaN;
|
|
1101
|
-
};
|
|
1102
946
|
/**
|
|
1103
947
|
* Input with some useful functions for entering numerical values.
|
|
1104
948
|
*/
|
|
1105
949
|
const NumberInput = {
|
|
1106
|
-
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, pointerLock = false, selectOnFocus = "none", unformatOnFocus = false, keepCaretOnStep = false, blurOnEnter = true, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
|
|
950
|
+
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, format: formatProp, parse: parseProp, clampValue = true, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, drag = 1, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, pointerLock = false, selectOnFocus = "none", unformatOnFocus = false, keepCaretOnStep = false, blurOnEnter = true, disabled = false, readonly = false, className, style, onChange, children, ...props }, forwardedRef) => {
|
|
1107
951
|
const inputRef = (0, react.useRef)(null);
|
|
1108
952
|
/**
|
|
1109
953
|
* The text being typed. The only state here: it is not a copy of `value`,
|
|
@@ -1112,24 +956,13 @@ const NumberInput = {
|
|
|
1112
956
|
const [draft, setDraft] = (0, react.useState)(null);
|
|
1113
957
|
const inactive = disabled || readonly;
|
|
1114
958
|
const format = formatProp ?? defaultFormat;
|
|
1115
|
-
const parse = parseProp ??
|
|
1116
|
-
const range = (0, react.useMemo)(() => ({
|
|
1117
|
-
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
1118
|
-
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
1119
|
-
step,
|
|
1120
|
-
scale
|
|
1121
|
-
}), [
|
|
1122
|
-
clampValue,
|
|
959
|
+
const parse = parseProp ?? _tremolo_ui_dom.parseNumberText;
|
|
960
|
+
const { normalized: range, raw: rawRange } = (0, react.useMemo)(() => (0, _tremolo_ui_dom.numberInputRanges)({
|
|
1123
961
|
min,
|
|
1124
962
|
max,
|
|
1125
963
|
step,
|
|
1126
|
-
scale
|
|
1127
|
-
|
|
1128
|
-
const rawRange = (0, react.useMemo)(() => ({
|
|
1129
|
-
min: (clampValue ? min : void 0) ?? -Number.MAX_VALUE,
|
|
1130
|
-
max: (clampValue ? max : void 0) ?? Number.MAX_VALUE,
|
|
1131
|
-
step,
|
|
1132
|
-
scale
|
|
964
|
+
scale,
|
|
965
|
+
clampValue
|
|
1133
966
|
}), [
|
|
1134
967
|
clampValue,
|
|
1135
968
|
min,
|
|
@@ -1137,6 +970,10 @@ const NumberInput = {
|
|
|
1137
970
|
step,
|
|
1138
971
|
scale
|
|
1139
972
|
]);
|
|
973
|
+
const ranges = (0, react.useMemo)(() => ({
|
|
974
|
+
normalized: range,
|
|
975
|
+
raw: rawRange
|
|
976
|
+
}), [range, rawRange]);
|
|
1140
977
|
useCheckSteps({
|
|
1141
978
|
component: "NumberInput",
|
|
1142
979
|
range: (0, react.useMemo)(() => min !== void 0 && max !== void 0 && min < max ? {
|
|
@@ -1156,7 +993,13 @@ const NumberInput = {
|
|
|
1156
993
|
});
|
|
1157
994
|
const text = draft ?? format(value);
|
|
1158
995
|
const editing = draft !== null;
|
|
1159
|
-
const
|
|
996
|
+
const bounds = (0, _tremolo_ui_dom.numberInputBounds)(value, {
|
|
997
|
+
min,
|
|
998
|
+
max,
|
|
999
|
+
clampValue
|
|
1000
|
+
});
|
|
1001
|
+
const { atMin, atMax } = bounds;
|
|
1002
|
+
const outOfRange = !editing && bounds.outOfRange;
|
|
1160
1003
|
const handleDraft = (0, react.useCallback)((next) => {
|
|
1161
1004
|
if (inactive) return;
|
|
1162
1005
|
setDraft(next);
|
|
@@ -1178,15 +1021,13 @@ const NumberInput = {
|
|
|
1178
1021
|
]);
|
|
1179
1022
|
const commitDraft = (0, react.useCallback)(() => {
|
|
1180
1023
|
if (draft === null || inactive) return;
|
|
1181
|
-
const
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
if (clampValue && max !== void 0) committed = Math.min(committed, max);
|
|
1189
|
-
changeValue(committed);
|
|
1024
|
+
const committed = (0, _tremolo_ui_dom.commitNumberInputText)(draft, parse, {
|
|
1025
|
+
min,
|
|
1026
|
+
max,
|
|
1027
|
+
clampValue
|
|
1028
|
+
});
|
|
1029
|
+
if (committed === null) setDraft(null);
|
|
1030
|
+
else changeValue(committed);
|
|
1190
1031
|
}, [
|
|
1191
1032
|
draft,
|
|
1192
1033
|
inactive,
|
|
@@ -1197,18 +1038,17 @@ const NumberInput = {
|
|
|
1197
1038
|
changeValue
|
|
1198
1039
|
]);
|
|
1199
1040
|
const nudge = (0, react.useCallback)((direction, option, modifiers) => {
|
|
1200
|
-
|
|
1201
|
-
changeValue((0, _tremolo_ui_functions.applyDelta)(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
|
|
1041
|
+
changeValue((0, _tremolo_ui_dom.nudgeNumberInput)(value, direction, option, ranges, modifiers));
|
|
1202
1042
|
}, [
|
|
1203
1043
|
changeValue,
|
|
1204
1044
|
value,
|
|
1205
|
-
|
|
1206
|
-
range
|
|
1045
|
+
ranges
|
|
1207
1046
|
]);
|
|
1208
1047
|
const wheelRefCallback = useWheel((event) => {
|
|
1209
|
-
|
|
1048
|
+
const direction = (0, _tremolo_ui_dom.wheelDirection)(event);
|
|
1049
|
+
if (!wheel || inactive || direction === null) return;
|
|
1210
1050
|
event.preventDefault();
|
|
1211
|
-
nudge(
|
|
1051
|
+
nudge(direction, wheel, event);
|
|
1212
1052
|
}, { requireFocus: true });
|
|
1213
1053
|
const context = (0, react.useMemo)(() => ({
|
|
1214
1054
|
value,
|
|
@@ -1231,8 +1071,8 @@ const NumberInput = {
|
|
|
1231
1071
|
unformatOnFocus,
|
|
1232
1072
|
keepCaretOnStep,
|
|
1233
1073
|
blurOnEnter,
|
|
1234
|
-
atMin
|
|
1235
|
-
atMax
|
|
1074
|
+
atMin,
|
|
1075
|
+
atMax,
|
|
1236
1076
|
drag,
|
|
1237
1077
|
format,
|
|
1238
1078
|
parse,
|
|
@@ -1256,6 +1096,8 @@ const NumberInput = {
|
|
|
1256
1096
|
text,
|
|
1257
1097
|
editing,
|
|
1258
1098
|
outOfRange,
|
|
1099
|
+
atMin,
|
|
1100
|
+
atMax,
|
|
1259
1101
|
drag,
|
|
1260
1102
|
dragSensitivity,
|
|
1261
1103
|
pointerLock,
|
|
@@ -1298,78 +1140,15 @@ const NumberInput = {
|
|
|
1298
1140
|
IncrementStepper,
|
|
1299
1141
|
DecrementStepper
|
|
1300
1142
|
};
|
|
1301
|
-
//#endregion
|
|
1302
|
-
//#region src/components/Piano/keyboard-shortcuts.ts
|
|
1303
|
-
/**
|
|
1304
|
-
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
1305
|
-
*/
|
|
1306
|
-
const SHORTCUTS = {
|
|
1307
|
-
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
1308
|
-
HOME_ROW: { keys: [
|
|
1309
|
-
"a",
|
|
1310
|
-
"w",
|
|
1311
|
-
"s",
|
|
1312
|
-
"e",
|
|
1313
|
-
"d",
|
|
1314
|
-
"f",
|
|
1315
|
-
"t",
|
|
1316
|
-
"g",
|
|
1317
|
-
"y",
|
|
1318
|
-
"h",
|
|
1319
|
-
"u",
|
|
1320
|
-
"j",
|
|
1321
|
-
"k",
|
|
1322
|
-
"o",
|
|
1323
|
-
"l",
|
|
1324
|
-
"p",
|
|
1325
|
-
";"
|
|
1326
|
-
] },
|
|
1327
|
-
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
1328
|
-
HOME_ROW_NATURAL: { keys: [
|
|
1329
|
-
"a",
|
|
1330
|
-
"",
|
|
1331
|
-
"s",
|
|
1332
|
-
"",
|
|
1333
|
-
"d",
|
|
1334
|
-
"f",
|
|
1335
|
-
"",
|
|
1336
|
-
"g",
|
|
1337
|
-
"",
|
|
1338
|
-
"h",
|
|
1339
|
-
"",
|
|
1340
|
-
"j",
|
|
1341
|
-
"k",
|
|
1342
|
-
"",
|
|
1343
|
-
"l",
|
|
1344
|
-
"",
|
|
1345
|
-
";"
|
|
1346
|
-
] }
|
|
1347
|
-
};
|
|
1348
|
-
//#endregion
|
|
1349
|
-
//#region src/components/Piano/index.tsx
|
|
1350
|
-
function isEditableTarget(target) {
|
|
1351
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
1352
|
-
if (target.matches("input, textarea, select")) return true;
|
|
1353
|
-
for (let element = target; element;) {
|
|
1354
|
-
const contentEditable = element.getAttribute("contenteditable");
|
|
1355
|
-
if (contentEditable !== null) return contentEditable.toLowerCase() !== "false";
|
|
1356
|
-
element = element.parentElement;
|
|
1357
|
-
}
|
|
1358
|
-
return false;
|
|
1359
|
-
}
|
|
1360
|
-
function shortcutKey(event) {
|
|
1361
|
-
return event.code || event.key;
|
|
1362
|
-
}
|
|
1363
1143
|
/**
|
|
1364
1144
|
* Customizable piano component.
|
|
1365
1145
|
*/
|
|
1366
|
-
const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6,
|
|
1146
|
+
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, style, className, label, keyProps, classes, onPlayNote, onStopNote, ...props }, forwardedRef) {
|
|
1367
1147
|
const [node, setNode] = (0, react.useState)(null);
|
|
1368
1148
|
const [activeNotes, setActiveNotes] = (0, react.useState)([]);
|
|
1369
1149
|
/** Set while `resizable` is on, measured from the parent. */
|
|
1370
1150
|
const [resizedKeyWidth, setResizedKeyWidth] = (0, react.useState)(whiteKeyWidth);
|
|
1371
|
-
const notes = (0, react.useMemo)(() => (0,
|
|
1372
|
-
const whiteKeyCount = (0, react.useMemo)(() => notes.filter(_tremolo_ui_functions.isWhiteKey).length, [notes]);
|
|
1151
|
+
const notes = (0, react.useMemo)(() => (0, _tremolo_ui_dom.getNoteRangeArray)(noteRange), [noteRange]);
|
|
1373
1152
|
const layout = (0, react.useMemo)(() => ({
|
|
1374
1153
|
noteRange,
|
|
1375
1154
|
whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
|
|
@@ -1389,21 +1168,20 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
|
|
|
1389
1168
|
layout,
|
|
1390
1169
|
glissando,
|
|
1391
1170
|
midiMax,
|
|
1171
|
+
keyboardShortcuts,
|
|
1172
|
+
keyboardShortcutsScope,
|
|
1392
1173
|
onPlayNote,
|
|
1393
1174
|
onStopNote
|
|
1394
1175
|
});
|
|
1395
1176
|
const instanceRef = (0, react.useRef)(null);
|
|
1396
|
-
const shortcutNotes = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
1397
|
-
const releaseShortcutNotes = (0, react.useCallback)(() => {
|
|
1398
|
-
for (const { note, source } of shortcutNotes.current.values()) instanceRef.current?.noteOff(note, { source });
|
|
1399
|
-
shortcutNotes.current.clear();
|
|
1400
|
-
}, []);
|
|
1401
1177
|
(0, react.useEffect)(() => {
|
|
1402
1178
|
if (!node) return;
|
|
1403
1179
|
const instance = (0, _tremolo_ui_dom.createPianoInput)(node, {
|
|
1404
1180
|
layout: latest.current.layout,
|
|
1405
1181
|
glissando: latest.current.glissando,
|
|
1406
1182
|
midiMax: latest.current.midiMax,
|
|
1183
|
+
keyboardShortcuts: latest.current.keyboardShortcuts,
|
|
1184
|
+
keyboardShortcutsScope: latest.current.keyboardShortcutsScope,
|
|
1407
1185
|
onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
|
|
1408
1186
|
onStopNote: (note) => latest.current.onStopNote?.(note),
|
|
1409
1187
|
onActiveNotesChange: setActiveNotes
|
|
@@ -1419,86 +1197,44 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
|
|
|
1419
1197
|
layout,
|
|
1420
1198
|
glissando,
|
|
1421
1199
|
midiMax,
|
|
1200
|
+
keyboardShortcuts,
|
|
1201
|
+
keyboardShortcutsScope,
|
|
1422
1202
|
onPlayNote,
|
|
1423
1203
|
onStopNote
|
|
1424
1204
|
};
|
|
1425
1205
|
instanceRef.current?.update({
|
|
1426
1206
|
layout,
|
|
1427
1207
|
glissando,
|
|
1428
|
-
midiMax
|
|
1208
|
+
midiMax,
|
|
1209
|
+
keyboardShortcuts,
|
|
1210
|
+
keyboardShortcutsScope
|
|
1429
1211
|
});
|
|
1430
1212
|
});
|
|
1213
|
+
const { first, last } = noteRange;
|
|
1431
1214
|
(0, react.useEffect)(() => {
|
|
1432
1215
|
if (!resizable || !node) return;
|
|
1433
1216
|
const parent = node.parentElement;
|
|
1434
1217
|
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1435
1218
|
const resizeObserver = new ResizeObserver(() => {
|
|
1436
|
-
setResizedKeyWidth(node.clientWidth
|
|
1219
|
+
setResizedKeyWidth((0, _tremolo_ui_dom.fitWhiteKeyWidth)(node.clientWidth, {
|
|
1220
|
+
noteRange: {
|
|
1221
|
+
first,
|
|
1222
|
+
last
|
|
1223
|
+
},
|
|
1224
|
+
keyGap,
|
|
1225
|
+
blackKeyWidthRatio
|
|
1226
|
+
}));
|
|
1437
1227
|
});
|
|
1438
1228
|
resizeObserver.observe(parent);
|
|
1439
1229
|
return () => resizeObserver.disconnect();
|
|
1440
1230
|
}, [
|
|
1441
1231
|
resizable,
|
|
1442
1232
|
node,
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
const hasShortcuts = shortcutKeys !== void 0;
|
|
1448
|
-
const shortcutMapping = shortcutKeys?.join("\0");
|
|
1449
|
-
(0, react.useEffect)(() => releaseShortcutNotes, [
|
|
1450
|
-
releaseShortcutNotes,
|
|
1451
|
-
shortcutMapping,
|
|
1452
|
-
keyboardShortcutsScope,
|
|
1453
|
-
noteRange.first,
|
|
1454
|
-
noteRange.last
|
|
1455
|
-
]);
|
|
1456
|
-
/** The note a shortcut key plays, or null when it has none. */
|
|
1457
|
-
function shortcutNote(key) {
|
|
1458
|
-
if (!shortcutKeys || key === "") return null;
|
|
1459
|
-
const index = shortcutKeys.indexOf(key);
|
|
1460
|
-
const note = noteRange.first + index;
|
|
1461
|
-
return index === -1 || note > noteRange.last ? null : note;
|
|
1462
|
-
}
|
|
1463
|
-
const shortcutTarget = (0, react.useMemo)(() => () => {
|
|
1464
|
-
if (!hasShortcuts) return null;
|
|
1465
|
-
return keyboardShortcutsScope === "window" ? globalThis.window : node;
|
|
1466
|
-
}, [
|
|
1467
|
-
hasShortcuts,
|
|
1468
|
-
keyboardShortcutsScope,
|
|
1469
|
-
node
|
|
1470
|
-
]);
|
|
1471
|
-
const focusOutTarget = (0, react.useMemo)(() => () => hasShortcuts && keyboardShortcutsScope === "root" ? node : null, [
|
|
1472
|
-
hasShortcuts,
|
|
1473
|
-
keyboardShortcutsScope,
|
|
1474
|
-
node
|
|
1233
|
+
first,
|
|
1234
|
+
last,
|
|
1235
|
+
keyGap,
|
|
1236
|
+
blackKeyWidthRatio
|
|
1475
1237
|
]);
|
|
1476
|
-
const windowBlurTarget = (0, react.useMemo)(() => () => hasShortcuts ? globalThis.window : null, [hasShortcuts]);
|
|
1477
|
-
useEventListener(shortcutTarget, "keydown", (e) => {
|
|
1478
|
-
const key = shortcutKey(e);
|
|
1479
|
-
if (e.repeat || shortcutNotes.current.has(key)) return;
|
|
1480
|
-
if (isEditableTarget(e.target)) return;
|
|
1481
|
-
const note = shortcutNote(e.key);
|
|
1482
|
-
if (note === null) return;
|
|
1483
|
-
const source = `keyboard:${key}`;
|
|
1484
|
-
shortcutNotes.current.set(key, {
|
|
1485
|
-
note,
|
|
1486
|
-
source
|
|
1487
|
-
});
|
|
1488
|
-
instanceRef.current?.noteOn(note, { source });
|
|
1489
|
-
});
|
|
1490
|
-
useEventListener(shortcutTarget, "keyup", (e) => {
|
|
1491
|
-
const key = shortcutKey(e);
|
|
1492
|
-
const shortcut = shortcutNotes.current.get(key);
|
|
1493
|
-
if (!shortcut) return;
|
|
1494
|
-
shortcutNotes.current.delete(key);
|
|
1495
|
-
instanceRef.current?.noteOff(shortcut.note, { source: shortcut.source });
|
|
1496
|
-
});
|
|
1497
|
-
useEventListener(focusOutTarget, "focusout", (e) => {
|
|
1498
|
-
if (e.relatedTarget instanceof Node && node?.contains(e.relatedTarget)) return;
|
|
1499
|
-
releaseShortcutNotes();
|
|
1500
|
-
});
|
|
1501
|
-
useEventListener(windowBlurTarget, "blur", releaseShortcutNotes);
|
|
1502
1238
|
(0, react.useImperativeHandle)(forwardedRef, () => ({
|
|
1503
1239
|
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1504
1240
|
source: "api",
|
|
@@ -1513,8 +1249,7 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
|
|
|
1513
1249
|
role: "group",
|
|
1514
1250
|
tabIndex: 0,
|
|
1515
1251
|
style: {
|
|
1516
|
-
width: resizable ? "100%" : (0,
|
|
1517
|
-
"--height": cssLength(height),
|
|
1252
|
+
width: resizable ? "100%" : (0, _tremolo_ui_dom.pianoWidth)(layout),
|
|
1518
1253
|
position: "relative",
|
|
1519
1254
|
...style
|
|
1520
1255
|
},
|
|
@@ -1540,8 +1275,8 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
|
|
|
1540
1275
|
position: "absolute",
|
|
1541
1276
|
zIndex: keyType === "white" ? 1 : 2,
|
|
1542
1277
|
...keyStyle,
|
|
1543
|
-
left: (0,
|
|
1544
|
-
width: keyType === "white" ? layout.whiteKeyWidth : (0,
|
|
1278
|
+
left: (0, _tremolo_ui_dom.notePosition)(note, layout),
|
|
1279
|
+
width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_dom.blackKeyWidth)(layout),
|
|
1545
1280
|
height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
|
|
1546
1281
|
},
|
|
1547
1282
|
children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -1580,43 +1315,58 @@ function usePointsEditorContext(selector) {
|
|
|
1580
1315
|
return selector ? selector(context) : context;
|
|
1581
1316
|
}
|
|
1582
1317
|
//#endregion
|
|
1318
|
+
//#region src/components/PointsEditor/Container.tsx
|
|
1319
|
+
const Container = /* @__PURE__ */ (0, react.forwardRef)(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1320
|
+
const { containerRef, disabled, selectable, editor } = usePointsEditorContext();
|
|
1321
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1322
|
+
axis: _tremolo_ui_dom.POINT_AXIS,
|
|
1323
|
+
baseElementRef: containerRef,
|
|
1324
|
+
shouldStart: (event) => !disabled && !editor.isPointElement(event.target),
|
|
1325
|
+
onDragStart: ([x, y], state) => {
|
|
1326
|
+
editor.beginSelectionBox({
|
|
1327
|
+
x,
|
|
1328
|
+
y
|
|
1329
|
+
}, state.event);
|
|
1330
|
+
},
|
|
1331
|
+
onChange: ([x, y]) => editor.moveSelectionBox({
|
|
1332
|
+
x,
|
|
1333
|
+
y
|
|
1334
|
+
}),
|
|
1335
|
+
onDragEnd: () => editor.endSelectionBox()
|
|
1336
|
+
});
|
|
1337
|
+
useWheel((event) => {
|
|
1338
|
+
const move = (0, _tremolo_ui_dom.wheelMove)(event);
|
|
1339
|
+
if (!move) return;
|
|
1340
|
+
const axis = move.axis === 0 ? "x" : "y";
|
|
1341
|
+
if (editor.nudgeFocusedPoint(axis, move.direction, event)) event.preventDefault();
|
|
1342
|
+
}, { target: containerRef });
|
|
1343
|
+
const composedRef = require_compose_refs.useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1344
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1345
|
+
ref: composedRef,
|
|
1346
|
+
className,
|
|
1347
|
+
style: {
|
|
1348
|
+
position: "absolute",
|
|
1349
|
+
inset: 0,
|
|
1350
|
+
zIndex: 10,
|
|
1351
|
+
...style
|
|
1352
|
+
},
|
|
1353
|
+
...props,
|
|
1354
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Placement, {
|
|
1355
|
+
name: "PointsEditor.Container",
|
|
1356
|
+
children
|
|
1357
|
+
})
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
//#endregion
|
|
1583
1361
|
//#region src/components/_util/VisuallyHiddenRangeInput.tsx
|
|
1584
|
-
const hiddenStyle = {
|
|
1585
|
-
position: "absolute",
|
|
1586
|
-
width: 1,
|
|
1587
|
-
height: 1,
|
|
1588
|
-
padding: 0,
|
|
1589
|
-
margin: -1,
|
|
1590
|
-
overflow: "hidden",
|
|
1591
|
-
clip: "rect(0, 0, 0, 0)",
|
|
1592
|
-
clipPath: "inset(50%)",
|
|
1593
|
-
whiteSpace: "nowrap",
|
|
1594
|
-
border: 0,
|
|
1595
|
-
pointerEvents: "none"
|
|
1596
|
-
};
|
|
1597
1362
|
const VisuallyHiddenRangeInput = /* @__PURE__ */ (0, react.forwardRef)((props, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
1598
1363
|
...props,
|
|
1599
1364
|
ref,
|
|
1600
1365
|
type: "range",
|
|
1601
|
-
style:
|
|
1366
|
+
style: _tremolo_ui_dom.visuallyHiddenStyle
|
|
1602
1367
|
}));
|
|
1603
1368
|
//#endregion
|
|
1604
1369
|
//#region src/components/PointsEditor/Point.tsx
|
|
1605
|
-
function clampPoint(point, min, max) {
|
|
1606
|
-
const { x, y } = point;
|
|
1607
|
-
return {
|
|
1608
|
-
x: (0, _tremolo_ui_functions.clamp)(x, min?.x ?? 0, max?.x ?? 1),
|
|
1609
|
-
y: (0, _tremolo_ui_functions.clamp)(y, min?.y ?? 0, max?.y ?? 1)
|
|
1610
|
-
};
|
|
1611
|
-
}
|
|
1612
|
-
/**
|
|
1613
|
-
* A point is placed by its position within the container, so its value is a
|
|
1614
|
-
* position: 0..1 on each axis, with y growing downwards.
|
|
1615
|
-
*/
|
|
1616
|
-
const AXIS = {
|
|
1617
|
-
min: 0,
|
|
1618
|
-
max: 1
|
|
1619
|
-
};
|
|
1620
1370
|
/**
|
|
1621
1371
|
* One setting for both axes, or one per axis. The point holds a range input
|
|
1622
1372
|
* for each, so anything that names or describes it comes in a pair.
|
|
@@ -1624,8 +1374,8 @@ const AXIS = {
|
|
|
1624
1374
|
function perAxis(value, axis) {
|
|
1625
1375
|
return typeof value === "string" ? value : value?.[axis];
|
|
1626
1376
|
}
|
|
1627
|
-
function Point({ value, children, id: idProp, min, max,
|
|
1628
|
-
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection,
|
|
1377
|
+
function Point({ value, children, id: idProp, min, max, color, disabled: _disabled, readonly: _readonly, wheel: _wheel, keyboard: _keyboard, "aria-label": ariaLabel, "aria-valuetext": ariaValuetext, onChange, onDragStart, onDragEnd, className, style, onPointerDown, onKeyDown, onFocus, ...props }) {
|
|
1378
|
+
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = usePointsEditorContext();
|
|
1629
1379
|
const generatedId = (0, react.useId)();
|
|
1630
1380
|
const id = idProp ?? generatedId;
|
|
1631
1381
|
const selected = selection.includes(id);
|
|
@@ -1658,65 +1408,48 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1658
1408
|
wheel
|
|
1659
1409
|
};
|
|
1660
1410
|
});
|
|
1661
|
-
(0, react.useEffect)(() => registerPoint(id, registration), [id,
|
|
1411
|
+
(0, react.useEffect)(() => editor.registerPoint(id, () => registration.current), [id, editor]);
|
|
1662
1412
|
/** Where the pointer was when the drag started, to measure the move from. */
|
|
1663
1413
|
const pointerOrigin = (0, react.useRef)(null);
|
|
1664
1414
|
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1665
|
-
axis:
|
|
1415
|
+
axis: _tremolo_ui_dom.POINT_AXIS,
|
|
1666
1416
|
baseElementRef: containerRef,
|
|
1667
|
-
sensitivity: (state) => (0,
|
|
1417
|
+
sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
|
|
1668
1418
|
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
1669
1419
|
shouldStart: () => !disabled,
|
|
1670
1420
|
onChange: ([x, y]) => {
|
|
1671
1421
|
const origin = pointerOrigin.current;
|
|
1672
1422
|
if (!origin) return;
|
|
1673
|
-
movePointDrag({
|
|
1423
|
+
editor.movePointDrag({
|
|
1674
1424
|
x: x - origin.x,
|
|
1675
1425
|
y: y - origin.y
|
|
1676
1426
|
});
|
|
1677
1427
|
},
|
|
1678
1428
|
onDragStart: ([x, y], state) => {
|
|
1679
|
-
beginPointDrag(id, state.event);
|
|
1429
|
+
editor.beginPointDrag(id, state.event);
|
|
1680
1430
|
pointerOrigin.current = {
|
|
1681
1431
|
x,
|
|
1682
1432
|
y
|
|
1683
1433
|
};
|
|
1684
1434
|
xInputRef.current?.focus();
|
|
1685
1435
|
if (inactive) return;
|
|
1686
|
-
onDragStart?.(clampPoint(value, min, max));
|
|
1436
|
+
onDragStart?.((0, _tremolo_ui_dom.clampPoint)(value, min, max));
|
|
1687
1437
|
},
|
|
1688
1438
|
onDragEnd: () => {
|
|
1689
1439
|
pointerOrigin.current = null;
|
|
1690
1440
|
if (inactive) return;
|
|
1691
|
-
onDragEnd?.(clampPoint(value, min, max));
|
|
1441
|
+
onDragEnd?.((0, _tremolo_ui_dom.clampPoint)(value, min, max));
|
|
1692
1442
|
}
|
|
1693
1443
|
});
|
|
1694
|
-
const nudge = (0, react.useCallback)((axis, direction, option, modifiers) => {
|
|
1695
|
-
const next = (0, _tremolo_ui_functions.applyDelta)(value[axis], direction, option, AXIS, modifiers);
|
|
1696
|
-
nudgeSelection(id, {
|
|
1697
|
-
x: axis === "x" ? next - value.x : 0,
|
|
1698
|
-
y: axis === "y" ? next - value.y : 0
|
|
1699
|
-
});
|
|
1700
|
-
}, [
|
|
1701
|
-
value,
|
|
1702
|
-
id,
|
|
1703
|
-
nudgeSelection
|
|
1704
|
-
]);
|
|
1705
1444
|
const refCallback = require_compose_refs.useComposedRefs(dragRefCallback, setElement);
|
|
1706
1445
|
const handleKeyDown = (event) => {
|
|
1707
|
-
const
|
|
1708
|
-
if (!
|
|
1709
|
-
"ArrowRight",
|
|
1710
|
-
"ArrowLeft",
|
|
1711
|
-
"ArrowUp",
|
|
1712
|
-
"ArrowDown"
|
|
1713
|
-
].includes(key)) return;
|
|
1714
|
-
const axis = key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y";
|
|
1446
|
+
const move = (0, _tremolo_ui_dom.arrowKeyMove)(event.key);
|
|
1447
|
+
if (!move) return;
|
|
1715
1448
|
event.preventDefault();
|
|
1716
1449
|
if (!onChange || inactive || !keyboard) return;
|
|
1717
|
-
|
|
1450
|
+
editor.nudgePoint(id, move.axis === 0 ? "x" : "y", move.direction, keyboard, event);
|
|
1718
1451
|
};
|
|
1719
|
-
const current = clampPoint(value, min, max);
|
|
1452
|
+
const current = (0, _tremolo_ui_dom.clampPoint)(value, min, max);
|
|
1720
1453
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1721
1454
|
ref: refCallback,
|
|
1722
1455
|
className,
|
|
@@ -1727,8 +1460,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1727
1460
|
"data-selected": selected ? "" : void 0,
|
|
1728
1461
|
style: {
|
|
1729
1462
|
"--color": color,
|
|
1730
|
-
"--width": cssLength(size ?? width),
|
|
1731
|
-
"--height": cssLength(size ?? height),
|
|
1732
1463
|
position: "absolute",
|
|
1733
1464
|
translate: "var(--translate, -50% -50%)",
|
|
1734
1465
|
...style,
|
|
@@ -1763,7 +1494,7 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1763
1494
|
return;
|
|
1764
1495
|
}
|
|
1765
1496
|
const delta = event.currentTarget.valueAsNumber - value[axis];
|
|
1766
|
-
nudgeSelection(id, {
|
|
1497
|
+
editor.nudgeSelection(id, {
|
|
1767
1498
|
x: axis === "x" ? delta : 0,
|
|
1768
1499
|
y: axis === "y" ? delta : 0
|
|
1769
1500
|
});
|
|
@@ -1772,50 +1503,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1772
1503
|
});
|
|
1773
1504
|
}
|
|
1774
1505
|
//#endregion
|
|
1775
|
-
//#region src/components/PointsEditor/Container.tsx
|
|
1776
|
-
const Container = /* @__PURE__ */ (0, react.forwardRef)(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1777
|
-
const { containerRef, disabled, selectable, isPointElement, nudgeFocusedPoint, beginSelectionBox, moveSelectionBox, endSelectionBox } = usePointsEditorContext();
|
|
1778
|
-
const { refCallback: dragRefCallback } = useDragValue({
|
|
1779
|
-
axis: AXIS,
|
|
1780
|
-
baseElementRef: containerRef,
|
|
1781
|
-
shouldStart: (event) => !disabled && !isPointElement(event.target),
|
|
1782
|
-
onDragStart: ([x, y], state) => {
|
|
1783
|
-
beginSelectionBox({
|
|
1784
|
-
x,
|
|
1785
|
-
y
|
|
1786
|
-
}, state.event);
|
|
1787
|
-
},
|
|
1788
|
-
onChange: ([x, y]) => moveSelectionBox({
|
|
1789
|
-
x,
|
|
1790
|
-
y
|
|
1791
|
-
}),
|
|
1792
|
-
onDragEnd: endSelectionBox
|
|
1793
|
-
});
|
|
1794
|
-
useWheel((event) => {
|
|
1795
|
-
const horizontal = event.deltaX !== 0;
|
|
1796
|
-
const delta = horizontal ? event.deltaX : event.deltaY;
|
|
1797
|
-
if (delta === 0) return;
|
|
1798
|
-
const axis = horizontal || event.shiftKey ? "x" : "y";
|
|
1799
|
-
if (nudgeFocusedPoint(axis, delta < 0 ? -1 : 1, event)) event.preventDefault();
|
|
1800
|
-
}, { target: containerRef });
|
|
1801
|
-
const composedRef = require_compose_refs.useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1802
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
1803
|
-
ref: composedRef,
|
|
1804
|
-
className,
|
|
1805
|
-
style: {
|
|
1806
|
-
position: "absolute",
|
|
1807
|
-
inset: 0,
|
|
1808
|
-
zIndex: 10,
|
|
1809
|
-
...style
|
|
1810
|
-
},
|
|
1811
|
-
...props,
|
|
1812
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Placement, {
|
|
1813
|
-
name: "PointsEditor.Container",
|
|
1814
|
-
children
|
|
1815
|
-
})
|
|
1816
|
-
});
|
|
1817
|
-
});
|
|
1818
|
-
//#endregion
|
|
1819
1506
|
//#region src/components/PointsEditor/SelectionBox.tsx
|
|
1820
1507
|
/**
|
|
1821
1508
|
* The box a drag on empty space draws, and what it covers is selected.
|
|
@@ -1848,181 +1535,28 @@ function SelectionBox({ children, className, style, ...props }) {
|
|
|
1848
1535
|
/** One array for every editor with selection turned off, so memos hold still. */
|
|
1849
1536
|
const EMPTY = [];
|
|
1850
1537
|
/**
|
|
1851
|
-
* How far a point may move before something in the selection leaves its range.
|
|
1852
|
-
*
|
|
1853
|
-
* Clamping each point on its own would break the shape of the selection: the
|
|
1854
|
-
* one that reached the edge would stop while the rest carried on. One amount
|
|
1855
|
-
* for all of them means the whole selection stops together.
|
|
1856
|
-
*/
|
|
1857
|
-
function allowedDelta(delta, entries) {
|
|
1858
|
-
let loX = -Infinity;
|
|
1859
|
-
let hiX = Infinity;
|
|
1860
|
-
let loY = -Infinity;
|
|
1861
|
-
let hiY = Infinity;
|
|
1862
|
-
for (const { start, registration } of entries) {
|
|
1863
|
-
loX = Math.max(loX, (registration.min?.x ?? 0) - start.x);
|
|
1864
|
-
hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x);
|
|
1865
|
-
loY = Math.max(loY, (registration.min?.y ?? 0) - start.y);
|
|
1866
|
-
hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y);
|
|
1867
|
-
}
|
|
1868
|
-
return {
|
|
1869
|
-
x: hiX < loX ? 0 : (0, _tremolo_ui_functions.clamp)(delta.x, loX, hiX),
|
|
1870
|
-
y: hiY < loY ? 0 : (0, _tremolo_ui_functions.clamp)(delta.y, loY, hiY)
|
|
1871
|
-
};
|
|
1872
|
-
}
|
|
1873
|
-
/**
|
|
1874
|
-
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
1875
|
-
* in a hundred steps whatever its pixel size.
|
|
1876
|
-
*/
|
|
1877
|
-
const DEFAULT_WHEEL = ["normalized", .01];
|
|
1878
|
-
/**
|
|
1879
|
-
* Shift is the fine-adjustment key everywhere else, so it is bound here too
|
|
1880
|
-
* — but only on the keyboard. On the wheel it already means the x axis, and
|
|
1881
|
-
* browsers hand shift+wheel over as horizontal scrolling anyway.
|
|
1882
|
-
*/
|
|
1883
|
-
const DEFAULT_KEYBOARD = {
|
|
1884
|
-
default: ["normalized", .01],
|
|
1885
|
-
shift: ["normalized", .001]
|
|
1886
|
-
};
|
|
1887
|
-
/**
|
|
1888
1538
|
* Multiple Point Controller
|
|
1889
1539
|
*/
|
|
1890
1540
|
const PointsEditor = {
|
|
1891
|
-
Root: /* @__PURE__ */ (0, react.forwardRef)(({
|
|
1541
|
+
Root: /* @__PURE__ */ (0, react.forwardRef)(({ disabled = false, readonly = false, wheel = _tremolo_ui_dom.POINTS_EDITOR_DEFAULT_WHEEL, keyboard = _tremolo_ui_dom.POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
|
|
1892
1542
|
const containerRef = (0, react.useRef)(null);
|
|
1893
1543
|
const { cursor = "grabbing" } = externalStyles ?? {};
|
|
1894
1544
|
const controlled = selectionProp !== void 0;
|
|
1895
1545
|
const [ownSelection, setOwnSelection] = (0, react.useState)(defaultSelection ?? []);
|
|
1896
1546
|
const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
|
|
1897
|
-
const selectionRef = (0, react.useRef)(selection);
|
|
1898
|
-
const changeHandlerRef = (0, react.useRef)(onSelectionChange);
|
|
1899
|
-
(0, react.useEffect)(() => {
|
|
1900
|
-
selectionRef.current = selection;
|
|
1901
|
-
changeHandlerRef.current = onSelectionChange;
|
|
1902
|
-
});
|
|
1903
|
-
const changeSelection = (0, react.useCallback)((next) => {
|
|
1904
|
-
selectionRef.current = next;
|
|
1905
|
-
if (!controlled) setOwnSelection(next);
|
|
1906
|
-
changeHandlerRef.current?.(next);
|
|
1907
|
-
}, [controlled]);
|
|
1908
|
-
/**
|
|
1909
|
-
* Every mounted point, by id. A registration is a ref rather than a value:
|
|
1910
|
-
* the value inside changes on every frame of a drag, and a registry keyed
|
|
1911
|
-
* on it would be rebuilt just as often.
|
|
1912
|
-
*/
|
|
1913
|
-
const points = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
1914
|
-
const registerPoint = (0, react.useCallback)((id, entry) => {
|
|
1915
|
-
points.current.set(id, entry);
|
|
1916
|
-
return () => {
|
|
1917
|
-
points.current.delete(id);
|
|
1918
|
-
};
|
|
1919
|
-
}, []);
|
|
1920
|
-
/** What the current drag picked up, and where those points started. */
|
|
1921
|
-
const dragRef = (0, react.useRef)([]);
|
|
1922
|
-
const snapshot = (0, react.useCallback)((ids) => {
|
|
1923
|
-
return ids.flatMap((id) => {
|
|
1924
|
-
const registration = points.current.get(id)?.current;
|
|
1925
|
-
return registration ? [{
|
|
1926
|
-
id,
|
|
1927
|
-
start: { ...registration.value }
|
|
1928
|
-
}] : [];
|
|
1929
|
-
});
|
|
1930
|
-
}, []);
|
|
1931
|
-
const applyDeltaTo = (0, react.useCallback)((entries, delta) => {
|
|
1932
|
-
const withRegistration = entries.flatMap((entry) => {
|
|
1933
|
-
const registration = points.current.get(entry.id)?.current;
|
|
1934
|
-
return registration ? [{
|
|
1935
|
-
...entry,
|
|
1936
|
-
registration
|
|
1937
|
-
}] : [];
|
|
1938
|
-
});
|
|
1939
|
-
const allowed = allowedDelta(delta, withRegistration);
|
|
1940
|
-
for (const { start, registration } of withRegistration) {
|
|
1941
|
-
if (registration.readonly) continue;
|
|
1942
|
-
registration.onChange?.({
|
|
1943
|
-
x: (0, _tremolo_ui_functions.toPrecision)(start.x + allowed.x),
|
|
1944
|
-
y: (0, _tremolo_ui_functions.toPrecision)(start.y + allowed.y)
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
}, []);
|
|
1948
|
-
const beginPointDrag = (0, react.useCallback)((id, modifiers) => {
|
|
1949
|
-
if (!selectable) {
|
|
1950
|
-
dragRef.current = snapshot([id]);
|
|
1951
|
-
return;
|
|
1952
|
-
}
|
|
1953
|
-
const current = selectionRef.current;
|
|
1954
|
-
const additive = modifiers.ctrlKey || modifiers.metaKey;
|
|
1955
|
-
let next;
|
|
1956
|
-
if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
1957
|
-
else if (current.includes(id)) next = current;
|
|
1958
|
-
else next = [id];
|
|
1959
|
-
changeSelection([...next]);
|
|
1960
|
-
dragRef.current = next.includes(id) ? snapshot(next) : [];
|
|
1961
|
-
}, [
|
|
1962
|
-
selectable,
|
|
1963
|
-
changeSelection,
|
|
1964
|
-
snapshot
|
|
1965
|
-
]);
|
|
1966
|
-
const movePointDrag = (0, react.useCallback)((delta) => applyDeltaTo(dragRef.current, delta), [applyDeltaTo]);
|
|
1967
|
-
const nudgeSelection = (0, react.useCallback)((id, delta) => {
|
|
1968
|
-
const ids = selectionRef.current.includes(id) ? selectionRef.current : [id];
|
|
1969
|
-
applyDeltaTo(snapshot(ids), delta);
|
|
1970
|
-
}, [applyDeltaTo, snapshot]);
|
|
1971
|
-
const isPointElement = (0, react.useCallback)((element) => {
|
|
1972
|
-
if (!element) return false;
|
|
1973
|
-
for (const [, entry] of points.current) if (entry.current.element?.contains(element)) return true;
|
|
1974
|
-
return false;
|
|
1975
|
-
}, []);
|
|
1976
|
-
const nudgeFocusedPoint = (0, react.useCallback)((axis, direction, modifiers) => {
|
|
1977
|
-
const container = containerRef.current;
|
|
1978
|
-
const active = container?.ownerDocument.activeElement;
|
|
1979
|
-
if (!container || !active || !container.contains(active)) return false;
|
|
1980
|
-
for (const [id, entry] of points.current) {
|
|
1981
|
-
const { element, wheel, readonly, onChange, value } = entry.current;
|
|
1982
|
-
if (!element?.contains(active)) continue;
|
|
1983
|
-
if (!wheel || readonly || !onChange) return false;
|
|
1984
|
-
const next = (0, _tremolo_ui_functions.applyDelta)(value[axis], direction, wheel, AXIS, modifiers);
|
|
1985
|
-
nudgeSelection(id, {
|
|
1986
|
-
x: axis === "x" ? next - value.x : 0,
|
|
1987
|
-
y: axis === "y" ? next - value.y : 0
|
|
1988
|
-
});
|
|
1989
|
-
return true;
|
|
1990
|
-
}
|
|
1991
|
-
return false;
|
|
1992
|
-
}, [nudgeSelection]);
|
|
1993
1547
|
const [selectionBox, setSelectionBox] = (0, react.useState)(null);
|
|
1994
|
-
const
|
|
1995
|
-
selectionBoxRef.current ??= (0, _tremolo_ui_dom.createSelectionBox)({
|
|
1996
|
-
*items() {
|
|
1997
|
-
for (const [id, entry] of points.current) {
|
|
1998
|
-
const { x, y } = entry.current.value;
|
|
1999
|
-
yield [id, [x, y]];
|
|
2000
|
-
}
|
|
2001
|
-
},
|
|
2002
|
-
onBoxChange: setSelectionBox
|
|
2003
|
-
});
|
|
1548
|
+
const [editor] = (0, react.useState)(() => (0, _tremolo_ui_dom.createPointsEditor)({ onSelectionBoxChange: setSelectionBox }));
|
|
2004
1549
|
(0, react.useEffect)(() => {
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
if (!selectable) return;
|
|
2013
|
-
selectionBoxRef.current?.begin([at.x, at.y], {
|
|
2014
|
-
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
2015
|
-
selection: selectionRef.current
|
|
1550
|
+
editor.update({
|
|
1551
|
+
selectable,
|
|
1552
|
+
selection,
|
|
1553
|
+
onSelectionChange: (next) => {
|
|
1554
|
+
if (!controlled) setOwnSelection(next);
|
|
1555
|
+
onSelectionChange?.(next);
|
|
1556
|
+
}
|
|
2016
1557
|
});
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
selectionBoxRef.current?.move([to.x, to.y]);
|
|
2020
|
-
}, []);
|
|
2021
|
-
const endSelectionBox = (0, react.useCallback)(() => {
|
|
2022
|
-
if (!selectionBoxRef.current?.end()) return;
|
|
2023
|
-
const [first] = selectionRef.current;
|
|
2024
|
-
if (first) points.current.get(first)?.current.element?.focus();
|
|
2025
|
-
}, []);
|
|
1558
|
+
});
|
|
1559
|
+
(0, react.useEffect)(() => () => editor.destroy(), [editor]);
|
|
2026
1560
|
const context = (0, react.useMemo)(() => ({
|
|
2027
1561
|
disabled,
|
|
2028
1562
|
readonly,
|
|
@@ -2033,16 +1567,8 @@ const PointsEditor = {
|
|
|
2033
1567
|
containerRef,
|
|
2034
1568
|
selectable,
|
|
2035
1569
|
selection,
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
beginPointDrag,
|
|
2039
|
-
movePointDrag,
|
|
2040
|
-
nudgeSelection,
|
|
2041
|
-
nudgeFocusedPoint,
|
|
2042
|
-
selectionBox,
|
|
2043
|
-
beginSelectionBox,
|
|
2044
|
-
moveSelectionBox,
|
|
2045
|
-
endSelectionBox
|
|
1570
|
+
editor,
|
|
1571
|
+
selectionBox
|
|
2046
1572
|
}), [
|
|
2047
1573
|
disabled,
|
|
2048
1574
|
readonly,
|
|
@@ -2052,16 +1578,8 @@ const PointsEditor = {
|
|
|
2052
1578
|
cursor,
|
|
2053
1579
|
selectable,
|
|
2054
1580
|
selection,
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
beginPointDrag,
|
|
2058
|
-
movePointDrag,
|
|
2059
|
-
nudgeSelection,
|
|
2060
|
-
nudgeFocusedPoint,
|
|
2061
|
-
selectionBox,
|
|
2062
|
-
beginSelectionBox,
|
|
2063
|
-
moveSelectionBox,
|
|
2064
|
-
endSelectionBox
|
|
1581
|
+
editor,
|
|
1582
|
+
selectionBox
|
|
2065
1583
|
]);
|
|
2066
1584
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PointsEditorProvider, {
|
|
2067
1585
|
value: context,
|
|
@@ -2071,8 +1589,6 @@ const PointsEditor = {
|
|
|
2071
1589
|
"data-disabled": disabled ? "" : void 0,
|
|
2072
1590
|
"data-readonly": readonly ? "" : void 0,
|
|
2073
1591
|
style: {
|
|
2074
|
-
"--width": cssLength(width),
|
|
2075
|
-
"--height": cssLength(height),
|
|
2076
1592
|
position: "relative",
|
|
2077
1593
|
...style
|
|
2078
1594
|
},
|
|
@@ -2097,36 +1613,30 @@ function useSliderContext(selector) {
|
|
|
2097
1613
|
}
|
|
2098
1614
|
//#endregion
|
|
2099
1615
|
//#region src/components/Slider/MarksOption.tsx
|
|
2100
|
-
function MarksOption({ value, mark = true, label, thickness, length, gap,
|
|
1616
|
+
function MarksOption({ value, mark = true, label, thickness, length, gap, classes, styles, className, style, ...props }) {
|
|
2101
1617
|
useCheckPlacement("Slider.MarksOption", "Slider.Marks");
|
|
2102
1618
|
const min = useSliderContext((s) => s.min);
|
|
2103
1619
|
const max = useSliderContext((s) => s.max);
|
|
2104
1620
|
const scale = useSliderContext((s) => s.scale);
|
|
2105
1621
|
const vertical = useSliderContext((s) => s.vertical);
|
|
2106
1622
|
const reverse = useSliderContext((s) => s.reverse);
|
|
2107
|
-
const
|
|
2108
|
-
const percent = scale.normalize(value, min, max) * 100;
|
|
2109
|
-
return (0, _tremolo_ui_functions.toFixed)((0, _tremolo_ui_functions.xor)(vertical, reverse) ? 100 - percent : percent);
|
|
2110
|
-
}, [
|
|
2111
|
-
vertical,
|
|
2112
|
-
reverse,
|
|
2113
|
-
max,
|
|
1623
|
+
const percent = (0, _tremolo_ui_dom.valuePercent)(value, {
|
|
2114
1624
|
min,
|
|
1625
|
+
max,
|
|
2115
1626
|
scale
|
|
2116
|
-
|
|
1627
|
+
}, vertical !== reverse);
|
|
2117
1628
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
2118
1629
|
className,
|
|
2119
1630
|
style: {
|
|
2120
|
-
"--thickness": cssLength(thickness),
|
|
2121
|
-
"--length": cssLength(length),
|
|
2122
|
-
"--gap": cssLength(gap),
|
|
2123
|
-
"--label-width": cssLength(labelWidth),
|
|
1631
|
+
"--thickness": (0, _tremolo_ui_dom.cssLength)(thickness),
|
|
1632
|
+
"--length": (0, _tremolo_ui_dom.cssLength)(length),
|
|
1633
|
+
"--gap": (0, _tremolo_ui_dom.cssLength)(gap),
|
|
2124
1634
|
position: "absolute",
|
|
2125
1635
|
translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
|
|
2126
1636
|
zIndex: 10,
|
|
2127
1637
|
...style,
|
|
2128
|
-
left: !vertical ? `${
|
|
2129
|
-
top: vertical ? `${
|
|
1638
|
+
left: !vertical ? `${percent}%` : void 0,
|
|
1639
|
+
top: vertical ? `${percent}%` : void 0
|
|
2130
1640
|
},
|
|
2131
1641
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2132
1642
|
...props,
|
|
@@ -2142,23 +1652,6 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, labelW
|
|
|
2142
1652
|
});
|
|
2143
1653
|
}
|
|
2144
1654
|
//#endregion
|
|
2145
|
-
//#region src/components/Slider/type.ts
|
|
2146
|
-
function generateOptionsList(options, min, max, step) {
|
|
2147
|
-
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
2148
|
-
const optionsList = [];
|
|
2149
|
-
const interval = per === "step" ? step : per;
|
|
2150
|
-
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
2151
|
-
for (let i = 0; i < count; i++) {
|
|
2152
|
-
const value = (0, _tremolo_ui_functions.toFixed)(interval * (Math.ceil(min / interval) + i), (0, _tremolo_ui_functions.decimalPart)(interval)?.length);
|
|
2153
|
-
optionsList.push({
|
|
2154
|
-
value,
|
|
2155
|
-
mark,
|
|
2156
|
-
label
|
|
2157
|
-
});
|
|
2158
|
-
}
|
|
2159
|
-
return optionsList;
|
|
2160
|
-
}
|
|
2161
|
-
//#endregion
|
|
2162
1655
|
//#region src/components/Slider/Marks.tsx
|
|
2163
1656
|
function Marks({ gap, options, children, className, style, ...props }) {
|
|
2164
1657
|
const min = useSliderContext((s) => s.min);
|
|
@@ -2166,13 +1659,13 @@ function Marks({ gap, options, children, className, style, ...props }) {
|
|
|
2166
1659
|
const step = useSliderContext((s) => s.step);
|
|
2167
1660
|
const vertical = useSliderContext((s) => s.vertical);
|
|
2168
1661
|
const reverse = useSliderContext((s) => s.reverse);
|
|
2169
|
-
const optionsList = options ?
|
|
2170
|
-
if (
|
|
1662
|
+
const optionsList = options ? (0, _tremolo_ui_dom.sliderMarks)(options, min, max, step) : [];
|
|
1663
|
+
if (vertical !== reverse) optionsList.reverse();
|
|
2171
1664
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2172
1665
|
className,
|
|
2173
1666
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2174
1667
|
style: {
|
|
2175
|
-
"--gap": cssLength(gap),
|
|
1668
|
+
"--gap": (0, _tremolo_ui_dom.cssLength)(gap),
|
|
2176
1669
|
position: "relative",
|
|
2177
1670
|
...style
|
|
2178
1671
|
},
|
|
@@ -2252,12 +1745,12 @@ const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thi
|
|
|
2252
1745
|
className,
|
|
2253
1746
|
"data-disabled": disabled ? "" : void 0,
|
|
2254
1747
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2255
|
-
"data-flipped":
|
|
1748
|
+
"data-flipped": vertical !== reverse ? "" : void 0,
|
|
2256
1749
|
style: {
|
|
2257
1750
|
"--active": active,
|
|
2258
1751
|
"--inactive": inactive,
|
|
2259
|
-
"--length": cssLength(length),
|
|
2260
|
-
"--thickness": cssLength(thickness),
|
|
1752
|
+
"--length": (0, _tremolo_ui_dom.cssLength)(length),
|
|
1753
|
+
"--thickness": (0, _tremolo_ui_dom.cssLength)(thickness),
|
|
2261
1754
|
"--percent": `${percent}%`,
|
|
2262
1755
|
position: "relative",
|
|
2263
1756
|
...style
|
|
@@ -2281,7 +1774,7 @@ const WHEEL_OPTIONS$1 = { requireFocus: true };
|
|
|
2281
1774
|
* Customizable slider
|
|
2282
1775
|
*/
|
|
2283
1776
|
const Slider = {
|
|
2284
|
-
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
|
|
1777
|
+
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min, max, step = 1, scale = _tremolo_ui_functions.linearScale, vertical = false, reverse = false, externalStyles: _externalStyles, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown, ...props }, forwardedRef) => {
|
|
2285
1778
|
const trackRef = (0, react.useRef)(null);
|
|
2286
1779
|
const thumbRef = (0, react.useRef)(null);
|
|
2287
1780
|
const externalStyles = {
|
|
@@ -2289,10 +1782,12 @@ const Slider = {
|
|
|
2289
1782
|
..._externalStyles
|
|
2290
1783
|
};
|
|
2291
1784
|
const inactive = disabled || readonly;
|
|
2292
|
-
const
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
1785
|
+
const displayReversed = vertical !== reverse;
|
|
1786
|
+
const percent = (0, _tremolo_ui_dom.valuePercent)(value, {
|
|
1787
|
+
min,
|
|
1788
|
+
max,
|
|
1789
|
+
scale
|
|
1790
|
+
}, displayReversed);
|
|
2296
1791
|
const axis = (0, react.useMemo)(() => ({
|
|
2297
1792
|
min,
|
|
2298
1793
|
max,
|
|
@@ -2313,18 +1808,11 @@ const Slider = {
|
|
|
2313
1808
|
wheel
|
|
2314
1809
|
});
|
|
2315
1810
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
2316
|
-
const
|
|
2317
|
-
if (
|
|
2318
|
-
"ArrowRight",
|
|
2319
|
-
"ArrowLeft",
|
|
2320
|
-
"ArrowUp",
|
|
2321
|
-
"ArrowDown"
|
|
2322
|
-
].includes(key)) return;
|
|
1811
|
+
const direction = (0, _tremolo_ui_dom.arrowKeyDirection)(event.key);
|
|
1812
|
+
if (direction === null) return;
|
|
2323
1813
|
event.preventDefault();
|
|
2324
1814
|
if (!keyboard || !onChange || inactive) return;
|
|
2325
|
-
|
|
2326
|
-
if (reverse) direction *= -1;
|
|
2327
|
-
onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, keyboard, axis, event));
|
|
1815
|
+
onChange((0, _tremolo_ui_dom.applyDelta)(value, reverse ? -direction : direction, keyboard, axis, event));
|
|
2328
1816
|
}, [
|
|
2329
1817
|
keyboard,
|
|
2330
1818
|
onChange,
|
|
@@ -2339,7 +1827,7 @@ const Slider = {
|
|
|
2339
1827
|
baseElementRef: trackRef,
|
|
2340
1828
|
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2341
1829
|
shouldStart: () => !inactive,
|
|
2342
|
-
sensitivity: (state) => (0,
|
|
1830
|
+
sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
|
|
2343
1831
|
updateOnPointerDown: true,
|
|
2344
1832
|
onChange: (v) => {
|
|
2345
1833
|
if (inactive) return;
|
|
@@ -2357,15 +1845,10 @@ const Slider = {
|
|
|
2357
1845
|
});
|
|
2358
1846
|
const wheelRefCallback = useWheel((event) => {
|
|
2359
1847
|
if (!wheel || !onChange || inactive) return;
|
|
1848
|
+
const direction = (0, _tremolo_ui_dom.wheelDirection)(event, { horizontal: !vertical });
|
|
1849
|
+
if (direction === null) return;
|
|
2360
1850
|
event.preventDefault();
|
|
2361
|
-
|
|
2362
|
-
if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
2363
|
-
else {
|
|
2364
|
-
if (event.deltaY === 0) return;
|
|
2365
|
-
direction = event.deltaY > 0 ? -1 : 1;
|
|
2366
|
-
}
|
|
2367
|
-
if (reverse) direction *= -1;
|
|
2368
|
-
onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, wheel, axis, event));
|
|
1851
|
+
onChange((0, _tremolo_ui_dom.applyDelta)(value, reverse ? -direction : direction, wheel, axis, event));
|
|
2369
1852
|
}, WHEEL_OPTIONS$1);
|
|
2370
1853
|
const rootRefCallback = require_compose_refs.useComposedRefs(dragRefCallback, wheelRefCallback);
|
|
2371
1854
|
const context = (0, react.useMemo)(() => ({
|
|
@@ -2450,16 +1933,13 @@ function useXYPadContext(selector) {
|
|
|
2450
1933
|
}
|
|
2451
1934
|
//#endregion
|
|
2452
1935
|
//#region src/components/XYPad/Area.tsx
|
|
2453
|
-
const Area = /* @__PURE__ */ (0, react.forwardRef)(function Area({
|
|
1936
|
+
const Area = /* @__PURE__ */ (0, react.forwardRef)(function Area({ children, className, style, ...props }, forwardedRef) {
|
|
2454
1937
|
const { areaRef } = useXYPadContext();
|
|
2455
1938
|
const composedRef = require_compose_refs.useComposedRefs(forwardedRef, areaRef);
|
|
2456
1939
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
2457
1940
|
ref: composedRef,
|
|
2458
1941
|
className,
|
|
2459
1942
|
style: {
|
|
2460
|
-
"--color": color,
|
|
2461
|
-
"--width": cssLength(width),
|
|
2462
|
-
"--height": cssLength(height),
|
|
2463
1943
|
position: "relative",
|
|
2464
1944
|
...style
|
|
2465
1945
|
},
|
|
@@ -2544,7 +2024,7 @@ const WHEEL_OPTIONS = { requireFocus: true };
|
|
|
2544
2024
|
* Simple XYPad
|
|
2545
2025
|
*/
|
|
2546
2026
|
const XYPad = {
|
|
2547
|
-
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = _tremolo_ui_functions.linearScale, reverse: _reverse = false, wheel = DEFAULT_WHEEL_OPTIONS, keyboard = DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
|
|
2027
|
+
Root: /* @__PURE__ */ (0, react.forwardRef)(({ value, min: _min, max: _max, step: _step = 1, scale: _scale = _tremolo_ui_functions.linearScale, reverse: _reverse = false, wheel = _tremolo_ui_dom.DEFAULT_WHEEL_OPTIONS, keyboard = _tremolo_ui_dom.DEFAULT_KEYBOARD_OPTIONS, dragSensitivity = _tremolo_ui_dom.DEFAULT_DRAG_SENSITIVITY, className, style, externalStyles: _externalStyles, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children, ...props }, forwardedRef) => {
|
|
2548
2028
|
const rootRef = (0, react.useRef)(null);
|
|
2549
2029
|
const areaRef = (0, react.useRef)(null);
|
|
2550
2030
|
const thumbRef = (0, react.useRef)(null);
|
|
@@ -2558,10 +2038,11 @@ const XYPad = {
|
|
|
2558
2038
|
const step = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_step), [_step]);
|
|
2559
2039
|
const scale = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_scale), [_scale]);
|
|
2560
2040
|
const reverse = (0, react.useMemo)(() => (0, _tremolo_ui_dom.toXY)(_reverse), [_reverse]);
|
|
2561
|
-
const percent = (0, react.useMemo)(() => {
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2041
|
+
const percent = (0, react.useMemo)(() => [0, 1].map((i) => (0, _tremolo_ui_dom.valuePercent)(value[i], {
|
|
2042
|
+
min: min[i],
|
|
2043
|
+
max: max[i],
|
|
2044
|
+
scale: scale[i]
|
|
2045
|
+
}, reverse[i])), [
|
|
2565
2046
|
value,
|
|
2566
2047
|
min,
|
|
2567
2048
|
max,
|
|
@@ -2597,26 +2078,18 @@ const XYPad = {
|
|
|
2597
2078
|
});
|
|
2598
2079
|
const withAxis = (0, react.useCallback)((axis, next) => axis === 0 ? [next, value[1]] : [value[0], next], [value]);
|
|
2599
2080
|
/** @param i 0 = x, 1 = y */
|
|
2600
|
-
const nudge = (0, react.useCallback)((i, direction, option, modifiers) => withAxis(i, (0,
|
|
2081
|
+
const nudge = (0, react.useCallback)((i, direction, option, modifiers) => withAxis(i, (0, _tremolo_ui_dom.applyDelta)(value[i], direction, option, axis[i], modifiers)), [
|
|
2601
2082
|
value,
|
|
2602
2083
|
axis,
|
|
2603
2084
|
withAxis
|
|
2604
2085
|
]);
|
|
2605
2086
|
const handleKeyDown = (0, react.useCallback)((event) => {
|
|
2606
|
-
const
|
|
2607
|
-
if (!
|
|
2608
|
-
"ArrowRight",
|
|
2609
|
-
"ArrowLeft",
|
|
2610
|
-
"ArrowUp",
|
|
2611
|
-
"ArrowDown"
|
|
2612
|
-
].includes(key)) return;
|
|
2613
|
-
const i = key === "ArrowRight" || key === "ArrowLeft" ? 0 : 1;
|
|
2087
|
+
const move = (0, _tremolo_ui_dom.arrowKeyMove)(event.key);
|
|
2088
|
+
if (!move) return;
|
|
2614
2089
|
event.preventDefault();
|
|
2615
2090
|
if (!onChange || inactive || !keyboard) return;
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
if (reverse[i]) direction *= -1;
|
|
2619
|
-
onChange(nudge(i, direction, keyboard, event));
|
|
2091
|
+
const { axis: i, direction } = move;
|
|
2092
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
|
|
2620
2093
|
}, [
|
|
2621
2094
|
onChange,
|
|
2622
2095
|
inactive,
|
|
@@ -2627,7 +2100,7 @@ const XYPad = {
|
|
|
2627
2100
|
const { refCallback: dragRefCallback } = useDragValue({
|
|
2628
2101
|
axis,
|
|
2629
2102
|
baseElementRef: areaRef,
|
|
2630
|
-
sensitivity: (state) => (0,
|
|
2103
|
+
sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
|
|
2631
2104
|
updateOnPointerDown: true,
|
|
2632
2105
|
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
2633
2106
|
shouldStart: () => !inactive,
|
|
@@ -2647,15 +2120,11 @@ const XYPad = {
|
|
|
2647
2120
|
});
|
|
2648
2121
|
const wheelRefCallback = useWheel((event) => {
|
|
2649
2122
|
if (!onChange || inactive || !wheel) return;
|
|
2650
|
-
const
|
|
2651
|
-
|
|
2652
|
-
if (delta === 0) return;
|
|
2653
|
-
const i = horizontal || event.shiftKey ? 0 : 1;
|
|
2123
|
+
const move = (0, _tremolo_ui_dom.wheelMove)(event);
|
|
2124
|
+
if (!move) return;
|
|
2654
2125
|
event.preventDefault();
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
if (reverse[i]) direction *= -1;
|
|
2658
|
-
onChange(nudge(i, direction, wheel, event));
|
|
2126
|
+
const { axis: i, direction } = move;
|
|
2127
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event));
|
|
2659
2128
|
}, WHEEL_OPTIONS);
|
|
2660
2129
|
const rootRefCallback = require_compose_refs.useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
|
|
2661
2130
|
const context = (0, react.useMemo)(() => ({
|
|
@@ -2740,6 +2209,82 @@ function useAnimationFrame(callback = () => {}, deps = []) {
|
|
|
2740
2209
|
}, [runCallback, ...deps]);
|
|
2741
2210
|
}
|
|
2742
2211
|
//#endregion
|
|
2212
|
+
//#region src/hooks/useDrag.ts
|
|
2213
|
+
/**
|
|
2214
|
+
* Track a pointer drag on an element.
|
|
2215
|
+
*
|
|
2216
|
+
* @returns a ref callback to attach to the element being dragged
|
|
2217
|
+
*/
|
|
2218
|
+
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
2219
|
+
const dragHandler = useCallbackRef(onDrag);
|
|
2220
|
+
const dragStartHandler = useCallbackRef(onDragStart);
|
|
2221
|
+
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
2222
|
+
const [node, setNode] = (0, react.useState)(null);
|
|
2223
|
+
(0, react.useEffect)(() => {
|
|
2224
|
+
if (!node) return;
|
|
2225
|
+
const instance = (0, _tremolo_ui_dom.createDrag)(node, {
|
|
2226
|
+
threshold,
|
|
2227
|
+
cursor,
|
|
2228
|
+
pointerLock,
|
|
2229
|
+
onDragStart: (state) => dragStartHandler(state),
|
|
2230
|
+
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
2231
|
+
onDragEnd: (state) => dragEndHandler(state)
|
|
2232
|
+
});
|
|
2233
|
+
return () => instance.destroy();
|
|
2234
|
+
}, [
|
|
2235
|
+
node,
|
|
2236
|
+
threshold,
|
|
2237
|
+
cursor,
|
|
2238
|
+
pointerLock,
|
|
2239
|
+
dragHandler,
|
|
2240
|
+
dragStartHandler,
|
|
2241
|
+
dragEndHandler
|
|
2242
|
+
]);
|
|
2243
|
+
return setNode;
|
|
2244
|
+
}
|
|
2245
|
+
//#endregion
|
|
2246
|
+
//#region src/hooks/useEventListener.ts
|
|
2247
|
+
function useEventListener(target, event, handler, options) {
|
|
2248
|
+
const listener = useCallbackRef(handler);
|
|
2249
|
+
const cleanupRef = (0, react.useRef)(() => {});
|
|
2250
|
+
(0, react.useEffect)(() => {
|
|
2251
|
+
const node = typeof target === "function" ? target() : target ?? document;
|
|
2252
|
+
if (!node) return;
|
|
2253
|
+
node.addEventListener(event, listener, options);
|
|
2254
|
+
const cleanup = () => {
|
|
2255
|
+
node.removeEventListener(event, listener, options);
|
|
2256
|
+
};
|
|
2257
|
+
cleanupRef.current = cleanup;
|
|
2258
|
+
return () => {
|
|
2259
|
+
cleanup();
|
|
2260
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
2261
|
+
};
|
|
2262
|
+
}, [
|
|
2263
|
+
event,
|
|
2264
|
+
target,
|
|
2265
|
+
options,
|
|
2266
|
+
listener
|
|
2267
|
+
]);
|
|
2268
|
+
return (0, react.useCallback)(() => cleanupRef.current(), []);
|
|
2269
|
+
}
|
|
2270
|
+
//#endregion
|
|
2271
|
+
//#region src/hooks/useInterval.ts
|
|
2272
|
+
function useInterval(callback, delay) {
|
|
2273
|
+
const savedCallback = (0, react.useRef)(callback);
|
|
2274
|
+
(0, react.useEffect)(() => {
|
|
2275
|
+
savedCallback.current = callback;
|
|
2276
|
+
}, [callback]);
|
|
2277
|
+
(0, react.useEffect)(() => {
|
|
2278
|
+
if (delay === null) return;
|
|
2279
|
+
const id = setInterval(() => {
|
|
2280
|
+
savedCallback.current();
|
|
2281
|
+
}, delay);
|
|
2282
|
+
return () => {
|
|
2283
|
+
clearInterval(id);
|
|
2284
|
+
};
|
|
2285
|
+
}, [delay]);
|
|
2286
|
+
}
|
|
2287
|
+
//#endregion
|
|
2743
2288
|
//#region src/hooks/useMIDIAccess.ts
|
|
2744
2289
|
const INITIAL_STATE = {
|
|
2745
2290
|
midiAccess: null,
|
|
@@ -2877,40 +2422,20 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
|
2877
2422
|
}
|
|
2878
2423
|
//#endregion
|
|
2879
2424
|
exports.AnimationCanvas = AnimationCanvas;
|
|
2880
|
-
exports.
|
|
2881
|
-
exports.
|
|
2882
|
-
exports.DEFAULT_WHEEL_OPTIONS = DEFAULT_WHEEL_OPTIONS;
|
|
2425
|
+
exports.DropZone = DropZone;
|
|
2426
|
+
exports.FileInput = FileInput;
|
|
2883
2427
|
exports.Knob = Knob;
|
|
2884
|
-
Object.defineProperty(exports, "NOT_SUPPORTED", {
|
|
2885
|
-
enumerable: true,
|
|
2886
|
-
get: function() {
|
|
2887
|
-
return _tremolo_ui_dom.NOT_SUPPORTED;
|
|
2888
|
-
}
|
|
2889
|
-
});
|
|
2890
2428
|
exports.NumberInput = NumberInput;
|
|
2891
|
-
Object.defineProperty(exports, "PERMISSION_DENIED", {
|
|
2892
|
-
enumerable: true,
|
|
2893
|
-
get: function() {
|
|
2894
|
-
return _tremolo_ui_dom.PERMISSION_DENIED;
|
|
2895
|
-
}
|
|
2896
|
-
});
|
|
2897
|
-
exports.PITCH_BEND_CENTER = _tremolo_ui_dom.PITCH_BEND_CENTER;
|
|
2898
2429
|
exports.Piano = Piano;
|
|
2899
2430
|
exports.PointsEditor = PointsEditor;
|
|
2900
|
-
exports.SHORTCUTS = SHORTCUTS;
|
|
2901
2431
|
exports.Slider = Slider;
|
|
2902
|
-
Object.defineProperty(exports, "UNAVAILABLE", {
|
|
2903
|
-
enumerable: true,
|
|
2904
|
-
get: function() {
|
|
2905
|
-
return _tremolo_ui_dom.UNAVAILABLE;
|
|
2906
|
-
}
|
|
2907
|
-
});
|
|
2908
2432
|
exports.XYPad = XYPad;
|
|
2909
|
-
exports.clampPoint = clampPoint;
|
|
2910
2433
|
exports.useAnimationFrame = useAnimationFrame;
|
|
2911
2434
|
exports.useDrag = useDrag;
|
|
2912
2435
|
exports.useDragValue = useDragValue;
|
|
2436
|
+
exports.useDropZone = useDropZone;
|
|
2913
2437
|
exports.useEventListener = useEventListener;
|
|
2438
|
+
exports.useFileInputContext = useFileInputContext;
|
|
2914
2439
|
exports.useInterval = useInterval;
|
|
2915
2440
|
exports.useKnobContext = useKnobContext;
|
|
2916
2441
|
exports.useLongPress = useLongPress;
|