@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.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useComposedRefs } from "./compose-refs.js";
|
|
2
2
|
import { createContext, forwardRef, useCallback, useContext, useEffect, useId, useImperativeHandle, useInsertionEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
|
|
3
|
-
import {
|
|
3
|
+
import { DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, KNOB_VIEWBOX_SIZE, POINTS_EDITOR_DEFAULT_KEYBOARD, POINTS_EDITOR_DEFAULT_WHEEL, POINT_AXIS, applyDelta, arrowKeyDirection, arrowKeyMove, blackKeyWidth, caretAtDecimalOffset, caretDecimalOffset, checkSteps, clampPoint, commitNumberInputText, createAnimationCanvas, createDrag, createDragValue, createDropZone, createLongPress, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createPointsEditor, createStepperDrag, createWheel, cssLength, elementMapping, fitWhiteKeyWidth, getNoteRangeArray, knobAngles, knobArcPath, knobArcRadius, notePosition, nudgeNumberInput, numberInputBounds, numberInputRanges, numberSpan, parseNumberText, partitionByAccept, pianoWidth, relativeMapping, selectModifier, sliderMarks, toXY, valuePercent, visuallyHiddenStyle, wheelDirection, wheelMove } from "@tremolo-ui/dom";
|
|
4
4
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
-
import {
|
|
5
|
+
import { clamp, isWhiteKey, linearScale, noteKey } from "@tremolo-ui/functions";
|
|
6
6
|
//#region src/components/AnimationCanvas/index.tsx
|
|
7
7
|
/**
|
|
8
8
|
* A simple animatable canvas with requestAnimationFrame()
|
|
@@ -68,114 +68,181 @@ const AnimationCanvas = /* @__PURE__ */ forwardRef(function AnimationCanvas({ dr
|
|
|
68
68
|
});
|
|
69
69
|
});
|
|
70
70
|
//#endregion
|
|
71
|
-
//#region src/hooks/_internal/
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
.
|
|
84
|
-
];
|
|
85
|
-
const NONE = {
|
|
86
|
-
shiftKey: false,
|
|
87
|
-
altKey: false,
|
|
88
|
-
ctrlKey: false,
|
|
89
|
-
metaKey: false
|
|
90
|
-
};
|
|
91
|
-
const MODIFIER_STATE = {
|
|
92
|
-
shift: {
|
|
93
|
-
...NONE,
|
|
94
|
-
shiftKey: true
|
|
95
|
-
},
|
|
96
|
-
alt: {
|
|
97
|
-
...NONE,
|
|
98
|
-
altKey: true
|
|
99
|
-
},
|
|
100
|
-
ctrl: {
|
|
101
|
-
...NONE,
|
|
102
|
-
ctrlKey: true
|
|
103
|
-
},
|
|
104
|
-
meta: {
|
|
105
|
-
...NONE,
|
|
106
|
-
metaKey: true
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
/** Each entry of a modifier-aware input option, as a name and held keys. */
|
|
110
|
-
function entries(options) {
|
|
111
|
-
if (Array.isArray(options)) return [{
|
|
112
|
-
name: "",
|
|
113
|
-
modifiers: NONE
|
|
114
|
-
}];
|
|
115
|
-
return Object.keys(options).map((key) => ({
|
|
116
|
-
name: key === "default" ? "" : key,
|
|
117
|
-
modifiers: MODIFIER_STATE[key] ?? NONE
|
|
118
|
-
}));
|
|
71
|
+
//#region src/hooks/_internal/useCallbackRef.ts
|
|
72
|
+
/**
|
|
73
|
+
* Internal
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
function useCallbackRef(callback, deps = []) {
|
|
77
|
+
const callbackRef = useRef(() => {
|
|
78
|
+
throw new Error("Cannot call an event handler while rendering.");
|
|
79
|
+
});
|
|
80
|
+
useInsertionEffect(() => {
|
|
81
|
+
callbackRef.current = callback;
|
|
82
|
+
});
|
|
83
|
+
return useCallback((...args) => callbackRef.current?.(...args), deps);
|
|
119
84
|
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/hooks/useDropZone.ts
|
|
120
87
|
/**
|
|
121
|
-
*
|
|
122
|
-
* whether anything came of it.
|
|
88
|
+
* Take files dropped onto an element.
|
|
123
89
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
90
|
+
* Any element can become a drop target this way — a waveform display, a track
|
|
91
|
+
* lane, a canvas — without wrapping it in anything.
|
|
92
|
+
*
|
|
93
|
+
* @returns a ref callback for the element, and whether a drag is over it
|
|
127
94
|
*/
|
|
128
|
-
function
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
95
|
+
function useDropZone({ accept, multiple, disabled, onDrop, onReject } = {}) {
|
|
96
|
+
const dropHandler = useCallbackRef(onDrop);
|
|
97
|
+
const rejectHandler = useCallbackRef(onReject);
|
|
98
|
+
const [state, setState] = useState({
|
|
99
|
+
over: false,
|
|
100
|
+
invalid: false
|
|
101
|
+
});
|
|
102
|
+
const [node, setNode] = useState(null);
|
|
103
|
+
const instanceRef = useRef(null);
|
|
104
|
+
const latest = useRef({
|
|
105
|
+
accept,
|
|
106
|
+
multiple,
|
|
107
|
+
disabled
|
|
108
|
+
});
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
if (!node) return;
|
|
111
|
+
const instance = createDropZone(node, {
|
|
112
|
+
...latest.current,
|
|
113
|
+
onDrop: (files, event) => dropHandler(files, event),
|
|
114
|
+
onReject: (files, event) => rejectHandler(files, event),
|
|
115
|
+
onStateChange: (next) => setState(next)
|
|
116
|
+
});
|
|
117
|
+
instanceRef.current = instance;
|
|
118
|
+
return () => {
|
|
119
|
+
instanceRef.current = null;
|
|
120
|
+
instance.destroy();
|
|
121
|
+
setState({
|
|
122
|
+
over: false,
|
|
123
|
+
invalid: false
|
|
124
|
+
});
|
|
125
|
+
};
|
|
126
|
+
}, [
|
|
127
|
+
node,
|
|
128
|
+
dropHandler,
|
|
129
|
+
rejectHandler
|
|
130
|
+
]);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
latest.current = {
|
|
133
|
+
accept,
|
|
134
|
+
multiple,
|
|
135
|
+
disabled
|
|
136
|
+
};
|
|
137
|
+
instanceRef.current?.update({
|
|
138
|
+
accept,
|
|
139
|
+
multiple,
|
|
140
|
+
disabled
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
return {
|
|
144
|
+
refCallback: setNode,
|
|
145
|
+
over: state.over,
|
|
146
|
+
invalid: state.invalid
|
|
133
147
|
};
|
|
134
|
-
for (const { modifiers } of entries(options)) for (const position of PROBES) {
|
|
135
|
-
const value = scale.denormalize(position, min, max);
|
|
136
|
-
const up = applyDelta(value, 1, options, range, modifiers);
|
|
137
|
-
const down = applyDelta(value, -1, options, range, modifiers);
|
|
138
|
-
if (up !== value || down !== value) outcome.moved = true;
|
|
139
|
-
if (!format) continue;
|
|
140
|
-
const shown = format(value);
|
|
141
|
-
if (format(up) !== shown || format(down) !== shown) outcome.visible = true;
|
|
142
|
-
}
|
|
143
|
-
return outcome;
|
|
144
148
|
}
|
|
145
149
|
/**
|
|
150
|
+
* An area that takes files dropped onto it.
|
|
151
|
+
*/
|
|
152
|
+
const DropZone = { Root: /* @__PURE__ */ forwardRef(({ accept, multiple = false, disabled = false, onDrop, onReject, children, ...props }, ref) => {
|
|
153
|
+
const { refCallback, over, invalid } = useDropZone({
|
|
154
|
+
accept,
|
|
155
|
+
multiple,
|
|
156
|
+
disabled,
|
|
157
|
+
onDrop,
|
|
158
|
+
onReject
|
|
159
|
+
});
|
|
160
|
+
const composedRef = useComposedRefs(ref, refCallback);
|
|
161
|
+
return /* @__PURE__ */ jsx("div", {
|
|
162
|
+
ref: composedRef,
|
|
163
|
+
"data-dragover": over ? "" : void 0,
|
|
164
|
+
"data-invalid": invalid ? "" : void 0,
|
|
165
|
+
"data-disabled": disabled ? "" : void 0,
|
|
166
|
+
...props,
|
|
167
|
+
children
|
|
168
|
+
});
|
|
169
|
+
}) };
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/components/FileInput/context.tsx
|
|
172
|
+
const FileInputContext = /* @__PURE__ */ createContext(null);
|
|
173
|
+
const FileInputProvider = FileInputContext.Provider;
|
|
174
|
+
function useFileInputContext(selector) {
|
|
175
|
+
const context = useContext(FileInputContext);
|
|
176
|
+
if (!context) throw new Error("Missing FileInputContext.Provider in the tree");
|
|
177
|
+
return selector ? selector(context) : context;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Picks files, and hands them over as `File`s.
|
|
181
|
+
*/
|
|
182
|
+
const FileInput = {
|
|
183
|
+
Root: /* @__PURE__ */ forwardRef(({ accept, multiple = false, disabled = false, onChange, onReject, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, ...props }, ref) => {
|
|
184
|
+
const inputId = useId();
|
|
185
|
+
const context = useMemo(() => ({
|
|
186
|
+
inputId,
|
|
187
|
+
disabled
|
|
188
|
+
}), [inputId, disabled]);
|
|
189
|
+
function handleChange(event) {
|
|
190
|
+
const { accepted, rejected } = partitionByAccept(Array.from(event.target.files ?? []), accept);
|
|
191
|
+
event.target.value = "";
|
|
192
|
+
if (accepted.length > 0) onChange?.(accepted);
|
|
193
|
+
if (rejected.length > 0) onReject?.(rejected);
|
|
194
|
+
}
|
|
195
|
+
return /* @__PURE__ */ jsx(FileInputProvider, {
|
|
196
|
+
value: context,
|
|
197
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
198
|
+
ref,
|
|
199
|
+
"data-disabled": disabled ? "" : void 0,
|
|
200
|
+
...props,
|
|
201
|
+
children: [/* @__PURE__ */ jsx("input", {
|
|
202
|
+
id: inputId,
|
|
203
|
+
type: "file",
|
|
204
|
+
accept,
|
|
205
|
+
multiple,
|
|
206
|
+
disabled,
|
|
207
|
+
style: visuallyHiddenStyle,
|
|
208
|
+
"aria-label": ariaLabel,
|
|
209
|
+
"aria-labelledby": ariaLabelledby,
|
|
210
|
+
"aria-describedby": ariaDescribedby,
|
|
211
|
+
onChange: handleChange
|
|
212
|
+
}), children]
|
|
213
|
+
})
|
|
214
|
+
});
|
|
215
|
+
}),
|
|
216
|
+
Trigger: /* @__PURE__ */ forwardRef(({ children, ...props }, ref) => {
|
|
217
|
+
const { inputId, disabled } = useFileInputContext();
|
|
218
|
+
return /* @__PURE__ */ jsx("label", {
|
|
219
|
+
ref,
|
|
220
|
+
htmlFor: inputId,
|
|
221
|
+
"data-disabled": disabled ? "" : void 0,
|
|
222
|
+
...props,
|
|
223
|
+
children
|
|
224
|
+
});
|
|
225
|
+
})
|
|
226
|
+
};
|
|
227
|
+
//#endregion
|
|
228
|
+
//#region src/hooks/_internal/useCheckSteps.ts
|
|
229
|
+
/**
|
|
146
230
|
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
147
|
-
* a change the user can see.
|
|
148
|
-
*
|
|
149
|
-
* Two settings that are each fine on their own can cancel out, and nothing
|
|
150
|
-
* fails when they do — the control simply sits there:
|
|
151
|
-
*
|
|
152
|
-
* - **`step` coarser than the amount.** `keyboard={['raw', 0.1]}` with
|
|
153
|
-
* `step={1}` rounds every press straight back to where it started
|
|
154
|
-
* - **the display coarser than the amount.** A `format` showing two decimals
|
|
155
|
-
* of a kHz value cannot show a press worth 1 Hz
|
|
156
|
-
*
|
|
157
|
-
* The second is only reported when the press is invisible at *every* point
|
|
158
|
-
* along the travel. A display that rounds is a deliberate choice and is
|
|
159
|
-
* normally right — it is being too coarse everywhere that makes it a mistake.
|
|
231
|
+
* a change the user can see. The probing is `checkSteps` in the core.
|
|
160
232
|
*
|
|
161
233
|
* @internal
|
|
162
234
|
*/
|
|
163
|
-
function useCheckSteps(
|
|
235
|
+
function useCheckSteps(options) {
|
|
236
|
+
const { component, axis, range, keyboard, wheel } = options;
|
|
164
237
|
const { min, max, step, scale } = range ?? {};
|
|
165
|
-
const where = axis ? `${component} (${axis})` : component;
|
|
166
238
|
useEffect(() => {
|
|
167
239
|
try {
|
|
168
240
|
if (process.env.NODE_ENV === "production") return;
|
|
169
|
-
|
|
170
|
-
for (const [name, options] of [["keyboard", keyboard], ["wheel", wheel]]) {
|
|
171
|
-
if (!options) continue;
|
|
172
|
-
const { moved, visible } = probe(options, range, format);
|
|
173
|
-
if (!moved) console.warn(`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` + (step !== void 0 ? ` Each press is smaller than \`step\` (${step}), so it rounds` : " Each press rounds") + " straight back to where it started.");
|
|
174
|
-
else if (format && !visible) console.warn(`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\` shows the same text before and after, everywhere in the range. The display is too coarse for it to be seen.`);
|
|
175
|
-
}
|
|
241
|
+
for (const warning of checkSteps(options)) console.warn(warning);
|
|
176
242
|
} catch {}
|
|
177
243
|
}, [
|
|
178
|
-
|
|
244
|
+
component,
|
|
245
|
+
axis,
|
|
179
246
|
min,
|
|
180
247
|
max,
|
|
181
248
|
step,
|
|
@@ -185,21 +252,6 @@ function useCheckSteps({ component, axis, range, keyboard, wheel, format }) {
|
|
|
185
252
|
]);
|
|
186
253
|
}
|
|
187
254
|
//#endregion
|
|
188
|
-
//#region src/hooks/_internal/useCallbackRef.ts
|
|
189
|
-
/**
|
|
190
|
-
* Internal
|
|
191
|
-
* @private
|
|
192
|
-
*/
|
|
193
|
-
function useCallbackRef(callback, deps = []) {
|
|
194
|
-
const callbackRef = useRef(() => {
|
|
195
|
-
throw new Error("Cannot call an event handler while rendering.");
|
|
196
|
-
});
|
|
197
|
-
useInsertionEffect(() => {
|
|
198
|
-
callbackRef.current = callback;
|
|
199
|
-
});
|
|
200
|
-
return useCallback((...args) => callbackRef.current?.(...args), deps);
|
|
201
|
-
}
|
|
202
|
-
//#endregion
|
|
203
255
|
//#region src/hooks/useDragValue.ts
|
|
204
256
|
/**
|
|
205
257
|
* Drive a value with a pointer drag.
|
|
@@ -298,49 +350,6 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
|
|
|
298
350
|
return setNode;
|
|
299
351
|
}
|
|
300
352
|
//#endregion
|
|
301
|
-
//#region src/input-event.ts
|
|
302
|
-
/**
|
|
303
|
-
* The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
|
|
304
|
-
* 1 per press in the units of the value, and 0.1 with shift held. That is one
|
|
305
|
-
* `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
|
|
306
|
-
* `useCheckSteps` warns about.
|
|
307
|
-
*
|
|
308
|
-
* A modifier entry is not snapped to `step`, which is what lets the finer
|
|
309
|
-
* amount move at all — see `applyDelta` in `@tremolo-ui/functions`.
|
|
310
|
-
*/
|
|
311
|
-
const DEFAULT_KEYBOARD_OPTIONS = {
|
|
312
|
-
default: ["raw", 1],
|
|
313
|
-
shift: ["raw", .1]
|
|
314
|
-
};
|
|
315
|
-
/**
|
|
316
|
-
* The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
|
|
317
|
-
*
|
|
318
|
-
* Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
|
|
319
|
-
* and fills `deltaX`, so no modifier is bound here.
|
|
320
|
-
*/
|
|
321
|
-
const DEFAULT_WHEEL_OPTIONS = ["raw", 1];
|
|
322
|
-
/**
|
|
323
|
-
* The drag sensitivity used by value controls by default. Shift makes the
|
|
324
|
-
* same movement cover a tenth of the range, matching the arrow keys.
|
|
325
|
-
*/
|
|
326
|
-
const DEFAULT_DRAG_SENSITIVITY = {
|
|
327
|
-
default: 1,
|
|
328
|
-
shift: .1
|
|
329
|
-
};
|
|
330
|
-
//#endregion
|
|
331
|
-
//#region src/components/_util/css-length.ts
|
|
332
|
-
/**
|
|
333
|
-
* A number as a CSS length, for a custom property.
|
|
334
|
-
*
|
|
335
|
-
* React appends `px` to a bare number for the properties it knows take a
|
|
336
|
-
* length, and a custom property is never one of them: `--size: 50` would come
|
|
337
|
-
* out as the invalid `50`. Anything already a string is passed through, so
|
|
338
|
-
* `'3rem'` and `'100%'` still work.
|
|
339
|
-
*/
|
|
340
|
-
function cssLength(value) {
|
|
341
|
-
return typeof value === "number" ? `${value}px` : value;
|
|
342
|
-
}
|
|
343
|
-
//#endregion
|
|
344
353
|
//#region src/components/_util/Placement.tsx
|
|
345
354
|
/**
|
|
346
355
|
* The subcomponent that established the current placement, if any.
|
|
@@ -382,49 +391,8 @@ function useCheckPlacement(child, parent) {
|
|
|
382
391
|
parent
|
|
383
392
|
]);
|
|
384
393
|
}
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
*
|
|
388
|
-
* 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
|
|
389
|
-
* それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
|
|
390
|
-
*/
|
|
391
|
-
function pointOnArc(angle, radius) {
|
|
392
|
-
return {
|
|
393
|
-
x: 50 + radius * Math.cos(radian(angle - 90)),
|
|
394
|
-
y: 50 + radius * Math.sin(radian(angle - 90))
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
/** 線の太さが viewBox からはみ出さないようにした半径 */
|
|
398
|
-
function arcRadius(strokeWidth) {
|
|
399
|
-
const width = typeof strokeWidth === "number" ? strokeWidth : Number.parseFloat(String(strokeWidth));
|
|
400
|
-
return Number.isFinite(width) ? 50 - width / 2 : 50;
|
|
401
|
-
}
|
|
402
|
-
/** Build an SVG path for an arc, splitting full turns into drawable segments. */
|
|
403
|
-
function arcPath(startAngle, endAngle, radius) {
|
|
404
|
-
const start = pointOnArc(startAngle, radius);
|
|
405
|
-
const sweep = endAngle - startAngle;
|
|
406
|
-
const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180));
|
|
407
|
-
const segmentSweep = sweep / segmentCount;
|
|
408
|
-
let path = `M ${start.x} ${start.y}`;
|
|
409
|
-
for (let i = 1; i <= segmentCount; i += 1) {
|
|
410
|
-
const end = pointOnArc(startAngle + segmentSweep * i, radius);
|
|
411
|
-
path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`;
|
|
412
|
-
}
|
|
413
|
-
return path;
|
|
414
|
-
}
|
|
415
|
-
/** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
|
|
416
|
-
function calcAngles({ value, min, max, scale, startValue, angleRange }) {
|
|
417
|
-
const p = scale.normalize(value, min, max);
|
|
418
|
-
const s = scale.normalize(startValue, min, max);
|
|
419
|
-
const r1 = -angleRange / 2;
|
|
420
|
-
return {
|
|
421
|
-
p,
|
|
422
|
-
r1,
|
|
423
|
-
r2: r1 + Math.min(p, s) * angleRange,
|
|
424
|
-
r3: r1 + Math.max(p, s) * angleRange,
|
|
425
|
-
r4: angleRange / 2
|
|
426
|
-
};
|
|
427
|
-
}
|
|
394
|
+
//#endregion
|
|
395
|
+
//#region src/components/Knob/context.tsx
|
|
428
396
|
const KnobContext = /* @__PURE__ */ createContext(null);
|
|
429
397
|
const KnobProvider = KnobContext.Provider;
|
|
430
398
|
function useKnobContext(selector) {
|
|
@@ -438,10 +406,10 @@ function ActiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...pr
|
|
|
438
406
|
useCheckPlacement("Knob.ActiveLine", "Knob.SVGRoot");
|
|
439
407
|
const r2 = useKnobContext((s) => s.r2);
|
|
440
408
|
const r3 = useKnobContext((s) => s.r3);
|
|
441
|
-
const radius =
|
|
409
|
+
const radius = knobArcRadius(strokeWidth);
|
|
442
410
|
return /* @__PURE__ */ jsx("path", {
|
|
443
411
|
className,
|
|
444
|
-
d:
|
|
412
|
+
d: knobArcPath(r2, r3, radius),
|
|
445
413
|
fill: "none",
|
|
446
414
|
stroke,
|
|
447
415
|
strokeWidth,
|
|
@@ -459,17 +427,17 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
459
427
|
const r2 = useKnobContext((s) => s.r2);
|
|
460
428
|
const r3 = useKnobContext((s) => s.r3);
|
|
461
429
|
const r4 = useKnobContext((s) => s.r4);
|
|
462
|
-
const radius =
|
|
430
|
+
const radius = knobArcRadius(strokeWidth);
|
|
463
431
|
return /* @__PURE__ */ jsxs(Fragment, { children: [startValue > min && /* @__PURE__ */ jsx("path", {
|
|
464
432
|
className,
|
|
465
|
-
d:
|
|
433
|
+
d: knobArcPath(r1, r2, radius),
|
|
466
434
|
fill: "none",
|
|
467
435
|
stroke,
|
|
468
436
|
strokeWidth,
|
|
469
437
|
...props
|
|
470
438
|
}), startValue < max && /* @__PURE__ */ jsx("path", {
|
|
471
439
|
className,
|
|
472
|
-
d:
|
|
440
|
+
d: knobArcPath(r3, r4, radius),
|
|
473
441
|
fill: "none",
|
|
474
442
|
stroke,
|
|
475
443
|
strokeWidth,
|
|
@@ -480,7 +448,7 @@ function InactiveLine({ stroke = "currentColor", strokeWidth = 6, className, ...
|
|
|
480
448
|
//#region src/components/Knob/SVGRoot.tsx
|
|
481
449
|
function SVGRoot({ children, style, ...props }) {
|
|
482
450
|
return /* @__PURE__ */ jsx("svg", {
|
|
483
|
-
viewBox: `0 0
|
|
451
|
+
viewBox: `0 0 ${KNOB_VIEWBOX_SIZE} ${KNOB_VIEWBOX_SIZE}`,
|
|
484
452
|
style: {
|
|
485
453
|
display: "block",
|
|
486
454
|
...style
|
|
@@ -510,7 +478,7 @@ function Thumb$2({ className, thumb = "currentColor", thumbLine = "currentColor"
|
|
|
510
478
|
}), /* @__PURE__ */ jsx("line", {
|
|
511
479
|
className: classes?.thumbLine,
|
|
512
480
|
x1: "50%",
|
|
513
|
-
y1: `${(
|
|
481
|
+
y1: `${(KNOB_VIEWBOX_SIZE - clamp(thumbSize, 0, 100)) / 2}%`,
|
|
514
482
|
x2: "50%",
|
|
515
483
|
y2: `${thumbLineLength}%`,
|
|
516
484
|
stroke: thumbLine,
|
|
@@ -560,16 +528,10 @@ const Knob = {
|
|
|
560
528
|
});
|
|
561
529
|
const handleKeyDown = useCallback((event) => {
|
|
562
530
|
if (!keyboard || !onChange || inactive) return;
|
|
563
|
-
const
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
"ArrowUp",
|
|
568
|
-
"ArrowDown"
|
|
569
|
-
].includes(key)) {
|
|
570
|
-
event.preventDefault();
|
|
571
|
-
onChange(applyDelta(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
|
|
572
|
-
}
|
|
531
|
+
const direction = arrowKeyDirection(event.key);
|
|
532
|
+
if (direction === null) return;
|
|
533
|
+
event.preventDefault();
|
|
534
|
+
onChange(applyDelta(value, direction, keyboard, range, event));
|
|
573
535
|
}, [
|
|
574
536
|
keyboard,
|
|
575
537
|
onChange,
|
|
@@ -595,9 +557,11 @@ const Knob = {
|
|
|
595
557
|
});
|
|
596
558
|
const wheelRefCallback = useWheel((event) => {
|
|
597
559
|
if (!wheel || inactive) return;
|
|
560
|
+
const direction = wheelDirection(event);
|
|
561
|
+
if (direction === null) return;
|
|
598
562
|
event.preventDefault();
|
|
599
|
-
if (!onChange
|
|
600
|
-
onChange(applyDelta(value,
|
|
563
|
+
if (!onChange) return;
|
|
564
|
+
onChange(applyDelta(value, direction, wheel, range, event));
|
|
601
565
|
}, WHEEL_OPTIONS$2);
|
|
602
566
|
const rootRefCallback = useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
|
|
603
567
|
const context = useMemo(() => {
|
|
@@ -612,7 +576,7 @@ const Knob = {
|
|
|
612
576
|
};
|
|
613
577
|
return {
|
|
614
578
|
...config,
|
|
615
|
-
...
|
|
579
|
+
...knobAngles(config)
|
|
616
580
|
};
|
|
617
581
|
}, [
|
|
618
582
|
value,
|
|
@@ -686,82 +650,36 @@ function useStepperContext() {
|
|
|
686
650
|
return useContext(StepperContext);
|
|
687
651
|
}
|
|
688
652
|
//#endregion
|
|
689
|
-
//#region src/hooks/
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
cleanupRef.current = cleanup;
|
|
701
|
-
return () => {
|
|
702
|
-
cleanup();
|
|
703
|
-
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
704
|
-
};
|
|
705
|
-
}, [
|
|
706
|
-
event,
|
|
707
|
-
target,
|
|
708
|
-
options,
|
|
709
|
-
listener
|
|
710
|
-
]);
|
|
711
|
-
return useCallback(() => cleanupRef.current(), []);
|
|
712
|
-
}
|
|
713
|
-
//#endregion
|
|
714
|
-
//#region src/hooks/useInterval.ts
|
|
715
|
-
function useInterval(callback, delay) {
|
|
716
|
-
const savedCallback = useRef(callback);
|
|
653
|
+
//#region src/hooks/useLongPress.ts
|
|
654
|
+
/**
|
|
655
|
+
* Repeat `callback` while a pointer is held down: once on the press, then
|
|
656
|
+
* every `interval` after `initialDelay`. The repeat itself is
|
|
657
|
+
* `createLongPress` in `@tremolo-ui/dom`.
|
|
658
|
+
*
|
|
659
|
+
* Returns the function that starts a press, for an `onPointerDown`.
|
|
660
|
+
*/
|
|
661
|
+
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
662
|
+
const onPress = useCallbackRef(callback);
|
|
663
|
+
const instanceRef = useRef(null);
|
|
717
664
|
useEffect(() => {
|
|
718
|
-
|
|
719
|
-
|
|
665
|
+
instanceRef.current?.update({
|
|
666
|
+
delay: initialDelay,
|
|
667
|
+
interval
|
|
668
|
+
});
|
|
669
|
+
});
|
|
720
670
|
useEffect(() => {
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
671
|
+
const instance = createLongPress({
|
|
672
|
+
onPress: () => onPress(),
|
|
673
|
+
delay: initialDelay,
|
|
674
|
+
interval
|
|
675
|
+
});
|
|
676
|
+
instanceRef.current = instance;
|
|
725
677
|
return () => {
|
|
726
|
-
|
|
678
|
+
instanceRef.current = null;
|
|
679
|
+
instance.destroy();
|
|
727
680
|
};
|
|
728
|
-
}, [
|
|
729
|
-
|
|
730
|
-
//#endregion
|
|
731
|
-
//#region src/hooks/useLongPress.ts
|
|
732
|
-
function useLongPress(callback, initialDelay = 500, interval = 40) {
|
|
733
|
-
const [pressed, setPressed] = useState(false);
|
|
734
|
-
const [delay, setDelay] = useState(initialDelay);
|
|
735
|
-
const activePointerId = useRef(null);
|
|
736
|
-
const isPressed = useRef(false);
|
|
737
|
-
useInterval(() => {
|
|
738
|
-
callback();
|
|
739
|
-
setDelay(interval);
|
|
740
|
-
}, pressed ? delay : null);
|
|
741
|
-
const stop = useCallback((pointerId) => {
|
|
742
|
-
if (!isPressed.current) return;
|
|
743
|
-
if (pointerId !== void 0 && activePointerId.current !== null && activePointerId.current !== pointerId) return;
|
|
744
|
-
isPressed.current = false;
|
|
745
|
-
activePointerId.current = null;
|
|
746
|
-
setPressed(false);
|
|
747
|
-
setDelay(initialDelay);
|
|
748
|
-
}, [initialDelay]);
|
|
749
|
-
useEventListener(globalThis.window, "pointerup", (event) => {
|
|
750
|
-
stop(event.pointerId);
|
|
751
|
-
});
|
|
752
|
-
useEventListener(globalThis.window, "pointercancel", (event) => {
|
|
753
|
-
stop(event.pointerId);
|
|
754
|
-
});
|
|
755
|
-
useEventListener(globalThis.window, "blur", () => {
|
|
756
|
-
stop();
|
|
757
|
-
});
|
|
758
|
-
return useCallback((event) => {
|
|
759
|
-
if (isPressed.current || event && event.button !== 0) return;
|
|
760
|
-
isPressed.current = true;
|
|
761
|
-
activePointerId.current = event?.pointerId ?? null;
|
|
762
|
-
callback();
|
|
763
|
-
setPressed(true);
|
|
764
|
-
}, [callback]);
|
|
681
|
+
}, []);
|
|
682
|
+
return useCallback((event) => instanceRef.current?.start(event), []);
|
|
765
683
|
}
|
|
766
684
|
//#endregion
|
|
767
685
|
//#region src/components/NumberInput/StepperButton.tsx
|
|
@@ -773,7 +691,7 @@ function StepperButton({ direction, variant, icon, className, style, children, o
|
|
|
773
691
|
const { step, disabled, readonly, atMin, atMax, nudge } = useNumberInputContext();
|
|
774
692
|
const stepper = useStepperContext();
|
|
775
693
|
const press = useLongPress(() => {
|
|
776
|
-
if (stepper?.
|
|
694
|
+
if (stepper?.moved()) return;
|
|
777
695
|
nudge(direction, ["raw", step]);
|
|
778
696
|
});
|
|
779
697
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -832,21 +750,6 @@ function IncrementStepper(props) {
|
|
|
832
750
|
}
|
|
833
751
|
//#endregion
|
|
834
752
|
//#region src/components/NumberInput/InputField.tsx
|
|
835
|
-
/** The leading number of the displayed text, whatever the format put around it. */
|
|
836
|
-
const NUMBER_PREFIX = /^\s*-?[\d.,]*/;
|
|
837
|
-
const numberEnd = (text) => text.match(NUMBER_PREFIX)?.[0].length ?? text.length;
|
|
838
|
-
/**
|
|
839
|
-
* The index the caret is measured against: the decimal point, or where one
|
|
840
|
-
* would go if the number has none.
|
|
841
|
-
*
|
|
842
|
-
* Measuring from an end instead would slide the caret across a digit whenever
|
|
843
|
-
* the number changed length — `9.9` to `10.0` gains a character in front, `10`
|
|
844
|
-
* to `9` loses one — which is exactly what stepping does.
|
|
845
|
-
*/
|
|
846
|
-
const decimalAnchor = (text) => {
|
|
847
|
-
const dot = text.indexOf(".");
|
|
848
|
-
return dot === -1 ? numberEnd(text) : dot;
|
|
849
|
-
};
|
|
850
753
|
/**
|
|
851
754
|
* The text field of a `NumberInput`, and the only place the value can be typed.
|
|
852
755
|
*
|
|
@@ -856,7 +759,7 @@ const decimalAnchor = (text) => {
|
|
|
856
759
|
* How it behaves on focus, Enter and the arrow keys is set on `Root`
|
|
857
760
|
* (`selectOnFocus`, `unformatOnFocus`, `keepCaretOnStep`, `blurOnEnter`).
|
|
858
761
|
*/
|
|
859
|
-
const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
762
|
+
const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, style, onChange, onFocus, onBlur, onKeyDown, ...props }, forwardedRef) {
|
|
860
763
|
const { value, min, max, step, disabled, readonly, keyboard, selectOnFocus, unformatOnFocus, keepCaretOnStep, blurOnEnter, text, editing, outOfRange, setDraft, commitDraft, nudge, inputRef } = useNumberInputContext();
|
|
861
764
|
const [focused, setFocused] = useState(false);
|
|
862
765
|
const shown = unformatOnFocus && focused && !editing ? String(value) : text;
|
|
@@ -865,7 +768,10 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
865
768
|
const input = inputRef.current;
|
|
866
769
|
if (!input) return;
|
|
867
770
|
if (selectOnFocus === "all") input.setSelectionRange(0, input.value.length);
|
|
868
|
-
else
|
|
771
|
+
else {
|
|
772
|
+
const span = numberSpan(input.value);
|
|
773
|
+
if (span) input.setSelectionRange(span.start, span.end);
|
|
774
|
+
}
|
|
869
775
|
}, [
|
|
870
776
|
focused,
|
|
871
777
|
selectOnFocus,
|
|
@@ -882,8 +788,7 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
882
788
|
caretAfterStep.current = null;
|
|
883
789
|
const input = inputRef.current;
|
|
884
790
|
if (!input || input.value === pending.from) return;
|
|
885
|
-
const
|
|
886
|
-
const caret = Math.max(0, Math.min(place, numberEnd(input.value)));
|
|
791
|
+
const caret = caretAtDecimalOffset(input.value, pending.offset);
|
|
887
792
|
input.setSelectionRange(caret, caret);
|
|
888
793
|
});
|
|
889
794
|
const composedRef = useComposedRefs(forwardedRef, inputRef);
|
|
@@ -910,6 +815,7 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
910
815
|
onChange: (event) => {
|
|
911
816
|
caretAfterStep.current = null;
|
|
912
817
|
setDraft(event.currentTarget.value);
|
|
818
|
+
onChange?.(event);
|
|
913
819
|
},
|
|
914
820
|
onFocus: (event) => {
|
|
915
821
|
setFocused(true);
|
|
@@ -929,7 +835,7 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
929
835
|
event.preventDefault();
|
|
930
836
|
const input = event.currentTarget;
|
|
931
837
|
if (keepCaretOnStep && input.selectionStart !== null) caretAfterStep.current = {
|
|
932
|
-
offset: input.
|
|
838
|
+
offset: caretDecimalOffset(input.value, input.selectionStart),
|
|
933
839
|
from: input.value
|
|
934
840
|
};
|
|
935
841
|
nudge(key === "ArrowUp" ? 1 : -1, keyboard, event);
|
|
@@ -940,117 +846,54 @@ const InputField = /* @__PURE__ */ forwardRef(function InputField({ className, s
|
|
|
940
846
|
});
|
|
941
847
|
});
|
|
942
848
|
//#endregion
|
|
943
|
-
//#region src/hooks/useDrag.ts
|
|
944
|
-
/**
|
|
945
|
-
* Track a pointer drag on an element.
|
|
946
|
-
*
|
|
947
|
-
* @returns a ref callback to attach to the element being dragged
|
|
948
|
-
*/
|
|
949
|
-
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
950
|
-
const dragHandler = useCallbackRef(onDrag);
|
|
951
|
-
const dragStartHandler = useCallbackRef(onDragStart);
|
|
952
|
-
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
953
|
-
const [node, setNode] = useState(null);
|
|
954
|
-
useEffect(() => {
|
|
955
|
-
if (!node) return;
|
|
956
|
-
const instance = createDrag(node, {
|
|
957
|
-
threshold,
|
|
958
|
-
cursor,
|
|
959
|
-
pointerLock,
|
|
960
|
-
onDragStart: (state) => dragStartHandler(state),
|
|
961
|
-
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
962
|
-
onDragEnd: (state) => dragEndHandler(state)
|
|
963
|
-
});
|
|
964
|
-
return () => instance.destroy();
|
|
965
|
-
}, [
|
|
966
|
-
node,
|
|
967
|
-
threshold,
|
|
968
|
-
cursor,
|
|
969
|
-
pointerLock,
|
|
970
|
-
dragHandler,
|
|
971
|
-
dragStartHandler,
|
|
972
|
-
dragEndHandler
|
|
973
|
-
]);
|
|
974
|
-
return setNode;
|
|
975
|
-
}
|
|
976
|
-
//#endregion
|
|
977
849
|
//#region src/components/NumberInput/Stepper.tsx
|
|
978
850
|
/**
|
|
979
851
|
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
980
|
-
* up and down moves the value one `step` every `drag` pixels.
|
|
852
|
+
* up and down moves the value one `step` every `drag` pixels. The counting is
|
|
853
|
+
* `createStepperDrag` in the core.
|
|
981
854
|
*
|
|
982
855
|
* The drag lives here rather than on `InputField` because `createDrag` turns
|
|
983
856
|
* off text selection on whatever element it is attached to.
|
|
984
857
|
*/
|
|
985
858
|
const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style, children, ...props }, forwardedRef) {
|
|
986
|
-
const { value,
|
|
987
|
-
const
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
/**
|
|
1009
|
-
* Which modifier the drag is currently counting at, and where the previous
|
|
1010
|
-
* event was — a key produces no pointer event of its own, so a change is
|
|
1011
|
-
* only seen on the next move and has to be dated back to the one before it.
|
|
1012
|
-
*/
|
|
1013
|
-
const factorRef = useRef(1);
|
|
1014
|
-
const previousYRef = useRef(0);
|
|
1015
|
-
const dragRefCallback = useDrag({
|
|
1016
|
-
threshold: 1,
|
|
1017
|
-
cursor: inactive ? void 0 : "ns-resize",
|
|
1018
|
-
pointerLock: inactive ? false : pointerLock,
|
|
1019
|
-
onDragStart: (state) => {
|
|
1020
|
-
originRef.current = null;
|
|
1021
|
-
draggingRef.current = false;
|
|
1022
|
-
factorRef.current = selectModifier(dragSensitivity, state.event).value;
|
|
1023
|
-
},
|
|
1024
|
-
onDrag: (_x, y, _dx, _dy, state) => {
|
|
1025
|
-
if (inactive || drag === null) return;
|
|
1026
|
-
if (!originRef.current) {
|
|
1027
|
-
originRef.current = {
|
|
1028
|
-
y,
|
|
1029
|
-
value
|
|
1030
|
-
};
|
|
1031
|
-
previousYRef.current = y;
|
|
1032
|
-
return;
|
|
1033
|
-
}
|
|
1034
|
-
const factor = selectModifier(dragSensitivity, state.event).value;
|
|
1035
|
-
if (factor !== factorRef.current) {
|
|
1036
|
-
originRef.current = {
|
|
1037
|
-
y: previousYRef.current,
|
|
1038
|
-
value
|
|
1039
|
-
};
|
|
1040
|
-
factorRef.current = factor;
|
|
1041
|
-
}
|
|
1042
|
-
previousYRef.current = y;
|
|
1043
|
-
const steps = Math.round(-(y - originRef.current.y) / drag);
|
|
1044
|
-
if (steps === 0) return;
|
|
1045
|
-
draggingRef.current = true;
|
|
1046
|
-
changeValue(applyDelta(originRef.current.value, steps, dragOptions, rawRange, state.event));
|
|
1047
|
-
},
|
|
1048
|
-
onDragEnd: () => {
|
|
1049
|
-
draggingRef.current = false;
|
|
1050
|
-
}
|
|
859
|
+
const { value, disabled, readonly, drag, dragSensitivity, pointerLock, rawRange, changeValue } = useNumberInputContext();
|
|
860
|
+
const enabled = drag !== null && !disabled && !readonly;
|
|
861
|
+
const [node, setNode] = useState(null);
|
|
862
|
+
const instanceRef = useRef(null);
|
|
863
|
+
const latest = useRef({
|
|
864
|
+
value,
|
|
865
|
+
changeValue
|
|
866
|
+
});
|
|
867
|
+
const options = {
|
|
868
|
+
range: rawRange,
|
|
869
|
+
pixels: drag ?? 1,
|
|
870
|
+
sensitivity: dragSensitivity,
|
|
871
|
+
pointerLock
|
|
872
|
+
};
|
|
873
|
+
const optionsRef = useRef(options);
|
|
874
|
+
useEffect(() => {
|
|
875
|
+
latest.current = {
|
|
876
|
+
value,
|
|
877
|
+
changeValue
|
|
878
|
+
};
|
|
879
|
+
optionsRef.current = options;
|
|
880
|
+
instanceRef.current?.update(options);
|
|
1051
881
|
});
|
|
1052
|
-
|
|
1053
|
-
|
|
882
|
+
useEffect(() => {
|
|
883
|
+
if (!node || !enabled) return;
|
|
884
|
+
const instance = createStepperDrag(node, {
|
|
885
|
+
...optionsRef.current,
|
|
886
|
+
getValue: () => latest.current.value,
|
|
887
|
+
onChange: (next) => latest.current.changeValue(next)
|
|
888
|
+
});
|
|
889
|
+
instanceRef.current = instance;
|
|
890
|
+
return () => {
|
|
891
|
+
instanceRef.current = null;
|
|
892
|
+
instance.destroy();
|
|
893
|
+
};
|
|
894
|
+
}, [node, enabled]);
|
|
895
|
+
const composedRef = useComposedRefs(forwardedRef, setNode);
|
|
896
|
+
const context = useMemo(() => ({ moved: () => instanceRef.current?.moved() ?? false }), []);
|
|
1054
897
|
return /* @__PURE__ */ jsx(StepperProvider, {
|
|
1055
898
|
value: context,
|
|
1056
899
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -1064,17 +907,7 @@ const Stepper = /* @__PURE__ */ forwardRef(function Stepper({ className, style,
|
|
|
1064
907
|
});
|
|
1065
908
|
//#endregion
|
|
1066
909
|
//#region src/components/NumberInput/index.tsx
|
|
1067
|
-
/**
|
|
1068
|
-
* A number at the start of the text, and nothing read after it. A half-typed
|
|
1069
|
-
* entry still yields the number in front of it, but text with no number is
|
|
1070
|
-
* `NaN` rather than 0, so that "unreadable" and "the user typed 0" stay apart.
|
|
1071
|
-
*/
|
|
1072
|
-
const LEADING_NUMBER = /^\s*([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)/;
|
|
1073
910
|
const defaultFormat = (value) => String(value);
|
|
1074
|
-
const defaultParse = (text) => {
|
|
1075
|
-
const match = text.match(LEADING_NUMBER);
|
|
1076
|
-
return match ? Number(match[1]) : NaN;
|
|
1077
|
-
};
|
|
1078
911
|
/**
|
|
1079
912
|
* Input with some useful functions for entering numerical values.
|
|
1080
913
|
*/
|
|
@@ -1088,24 +921,13 @@ const NumberInput = {
|
|
|
1088
921
|
const [draft, setDraft] = useState(null);
|
|
1089
922
|
const inactive = disabled || readonly;
|
|
1090
923
|
const format = formatProp ?? defaultFormat;
|
|
1091
|
-
const parse = parseProp ??
|
|
1092
|
-
const range = useMemo(() => ({
|
|
1093
|
-
min: (clampValue ? min : void 0) ?? Number.MIN_SAFE_INTEGER,
|
|
1094
|
-
max: (clampValue ? max : void 0) ?? Number.MAX_SAFE_INTEGER,
|
|
1095
|
-
step,
|
|
1096
|
-
scale
|
|
1097
|
-
}), [
|
|
1098
|
-
clampValue,
|
|
924
|
+
const parse = parseProp ?? parseNumberText;
|
|
925
|
+
const { normalized: range, raw: rawRange } = useMemo(() => numberInputRanges({
|
|
1099
926
|
min,
|
|
1100
927
|
max,
|
|
1101
928
|
step,
|
|
1102
|
-
scale
|
|
1103
|
-
|
|
1104
|
-
const rawRange = useMemo(() => ({
|
|
1105
|
-
min: (clampValue ? min : void 0) ?? -Number.MAX_VALUE,
|
|
1106
|
-
max: (clampValue ? max : void 0) ?? Number.MAX_VALUE,
|
|
1107
|
-
step,
|
|
1108
|
-
scale
|
|
929
|
+
scale,
|
|
930
|
+
clampValue
|
|
1109
931
|
}), [
|
|
1110
932
|
clampValue,
|
|
1111
933
|
min,
|
|
@@ -1113,6 +935,10 @@ const NumberInput = {
|
|
|
1113
935
|
step,
|
|
1114
936
|
scale
|
|
1115
937
|
]);
|
|
938
|
+
const ranges = useMemo(() => ({
|
|
939
|
+
normalized: range,
|
|
940
|
+
raw: rawRange
|
|
941
|
+
}), [range, rawRange]);
|
|
1116
942
|
useCheckSteps({
|
|
1117
943
|
component: "NumberInput",
|
|
1118
944
|
range: useMemo(() => min !== void 0 && max !== void 0 && min < max ? {
|
|
@@ -1132,7 +958,13 @@ const NumberInput = {
|
|
|
1132
958
|
});
|
|
1133
959
|
const text = draft ?? format(value);
|
|
1134
960
|
const editing = draft !== null;
|
|
1135
|
-
const
|
|
961
|
+
const bounds = numberInputBounds(value, {
|
|
962
|
+
min,
|
|
963
|
+
max,
|
|
964
|
+
clampValue
|
|
965
|
+
});
|
|
966
|
+
const { atMin, atMax } = bounds;
|
|
967
|
+
const outOfRange = !editing && bounds.outOfRange;
|
|
1136
968
|
const handleDraft = useCallback((next) => {
|
|
1137
969
|
if (inactive) return;
|
|
1138
970
|
setDraft(next);
|
|
@@ -1154,15 +986,13 @@ const NumberInput = {
|
|
|
1154
986
|
]);
|
|
1155
987
|
const commitDraft = useCallback(() => {
|
|
1156
988
|
if (draft === null || inactive) return;
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
if (clampValue && max !== void 0) committed = Math.min(committed, max);
|
|
1165
|
-
changeValue(committed);
|
|
989
|
+
const committed = commitNumberInputText(draft, parse, {
|
|
990
|
+
min,
|
|
991
|
+
max,
|
|
992
|
+
clampValue
|
|
993
|
+
});
|
|
994
|
+
if (committed === null) setDraft(null);
|
|
995
|
+
else changeValue(committed);
|
|
1166
996
|
}, [
|
|
1167
997
|
draft,
|
|
1168
998
|
inactive,
|
|
@@ -1173,18 +1003,17 @@ const NumberInput = {
|
|
|
1173
1003
|
changeValue
|
|
1174
1004
|
]);
|
|
1175
1005
|
const nudge = useCallback((direction, option, modifiers) => {
|
|
1176
|
-
|
|
1177
|
-
changeValue(applyDelta(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
|
|
1006
|
+
changeValue(nudgeNumberInput(value, direction, option, ranges, modifiers));
|
|
1178
1007
|
}, [
|
|
1179
1008
|
changeValue,
|
|
1180
1009
|
value,
|
|
1181
|
-
|
|
1182
|
-
range
|
|
1010
|
+
ranges
|
|
1183
1011
|
]);
|
|
1184
1012
|
const wheelRefCallback = useWheel((event) => {
|
|
1185
|
-
|
|
1013
|
+
const direction = wheelDirection(event);
|
|
1014
|
+
if (!wheel || inactive || direction === null) return;
|
|
1186
1015
|
event.preventDefault();
|
|
1187
|
-
nudge(
|
|
1016
|
+
nudge(direction, wheel, event);
|
|
1188
1017
|
}, { requireFocus: true });
|
|
1189
1018
|
const context = useMemo(() => ({
|
|
1190
1019
|
value,
|
|
@@ -1207,8 +1036,8 @@ const NumberInput = {
|
|
|
1207
1036
|
unformatOnFocus,
|
|
1208
1037
|
keepCaretOnStep,
|
|
1209
1038
|
blurOnEnter,
|
|
1210
|
-
atMin
|
|
1211
|
-
atMax
|
|
1039
|
+
atMin,
|
|
1040
|
+
atMax,
|
|
1212
1041
|
drag,
|
|
1213
1042
|
format,
|
|
1214
1043
|
parse,
|
|
@@ -1232,6 +1061,8 @@ const NumberInput = {
|
|
|
1232
1061
|
text,
|
|
1233
1062
|
editing,
|
|
1234
1063
|
outOfRange,
|
|
1064
|
+
atMin,
|
|
1065
|
+
atMax,
|
|
1235
1066
|
drag,
|
|
1236
1067
|
dragSensitivity,
|
|
1237
1068
|
pointerLock,
|
|
@@ -1274,78 +1105,15 @@ const NumberInput = {
|
|
|
1274
1105
|
IncrementStepper,
|
|
1275
1106
|
DecrementStepper
|
|
1276
1107
|
};
|
|
1277
|
-
//#endregion
|
|
1278
|
-
//#region src/components/Piano/keyboard-shortcuts.ts
|
|
1279
|
-
/**
|
|
1280
|
-
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
1281
|
-
*/
|
|
1282
|
-
const SHORTCUTS = {
|
|
1283
|
-
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
1284
|
-
HOME_ROW: { keys: [
|
|
1285
|
-
"a",
|
|
1286
|
-
"w",
|
|
1287
|
-
"s",
|
|
1288
|
-
"e",
|
|
1289
|
-
"d",
|
|
1290
|
-
"f",
|
|
1291
|
-
"t",
|
|
1292
|
-
"g",
|
|
1293
|
-
"y",
|
|
1294
|
-
"h",
|
|
1295
|
-
"u",
|
|
1296
|
-
"j",
|
|
1297
|
-
"k",
|
|
1298
|
-
"o",
|
|
1299
|
-
"l",
|
|
1300
|
-
"p",
|
|
1301
|
-
";"
|
|
1302
|
-
] },
|
|
1303
|
-
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
1304
|
-
HOME_ROW_NATURAL: { keys: [
|
|
1305
|
-
"a",
|
|
1306
|
-
"",
|
|
1307
|
-
"s",
|
|
1308
|
-
"",
|
|
1309
|
-
"d",
|
|
1310
|
-
"f",
|
|
1311
|
-
"",
|
|
1312
|
-
"g",
|
|
1313
|
-
"",
|
|
1314
|
-
"h",
|
|
1315
|
-
"",
|
|
1316
|
-
"j",
|
|
1317
|
-
"k",
|
|
1318
|
-
"",
|
|
1319
|
-
"l",
|
|
1320
|
-
"",
|
|
1321
|
-
";"
|
|
1322
|
-
] }
|
|
1323
|
-
};
|
|
1324
|
-
//#endregion
|
|
1325
|
-
//#region src/components/Piano/index.tsx
|
|
1326
|
-
function isEditableTarget(target) {
|
|
1327
|
-
if (!(target instanceof HTMLElement)) return false;
|
|
1328
|
-
if (target.matches("input, textarea, select")) return true;
|
|
1329
|
-
for (let element = target; element;) {
|
|
1330
|
-
const contentEditable = element.getAttribute("contenteditable");
|
|
1331
|
-
if (contentEditable !== null) return contentEditable.toLowerCase() !== "false";
|
|
1332
|
-
element = element.parentElement;
|
|
1333
|
-
}
|
|
1334
|
-
return false;
|
|
1335
|
-
}
|
|
1336
|
-
function shortcutKey(event) {
|
|
1337
|
-
return event.code || event.key;
|
|
1338
|
-
}
|
|
1339
1108
|
/**
|
|
1340
1109
|
* Customizable piano component.
|
|
1341
1110
|
*/
|
|
1342
|
-
const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6,
|
|
1111
|
+
const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, keyboardShortcutsScope = "root", resizable = false, whiteKeyWidth = 40, keyGap = 1, blackKeyWidthRatio = .65, blackKeyHeightRatio = .6, style, className, label, keyProps, classes, onPlayNote, onStopNote, ...props }, forwardedRef) {
|
|
1343
1112
|
const [node, setNode] = useState(null);
|
|
1344
1113
|
const [activeNotes, setActiveNotes] = useState([]);
|
|
1345
1114
|
/** Set while `resizable` is on, measured from the parent. */
|
|
1346
1115
|
const [resizedKeyWidth, setResizedKeyWidth] = useState(whiteKeyWidth);
|
|
1347
1116
|
const notes = useMemo(() => getNoteRangeArray(noteRange), [noteRange]);
|
|
1348
|
-
const whiteKeyCount = useMemo(() => notes.filter(isWhiteKey).length, [notes]);
|
|
1349
1117
|
const layout = useMemo(() => ({
|
|
1350
1118
|
noteRange,
|
|
1351
1119
|
whiteKeyWidth: resizable ? resizedKeyWidth : whiteKeyWidth,
|
|
@@ -1365,21 +1133,20 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
|
|
|
1365
1133
|
layout,
|
|
1366
1134
|
glissando,
|
|
1367
1135
|
midiMax,
|
|
1136
|
+
keyboardShortcuts,
|
|
1137
|
+
keyboardShortcutsScope,
|
|
1368
1138
|
onPlayNote,
|
|
1369
1139
|
onStopNote
|
|
1370
1140
|
});
|
|
1371
1141
|
const instanceRef = useRef(null);
|
|
1372
|
-
const shortcutNotes = useRef(/* @__PURE__ */ new Map());
|
|
1373
|
-
const releaseShortcutNotes = useCallback(() => {
|
|
1374
|
-
for (const { note, source } of shortcutNotes.current.values()) instanceRef.current?.noteOff(note, { source });
|
|
1375
|
-
shortcutNotes.current.clear();
|
|
1376
|
-
}, []);
|
|
1377
1142
|
useEffect(() => {
|
|
1378
1143
|
if (!node) return;
|
|
1379
1144
|
const instance = createPianoInput(node, {
|
|
1380
1145
|
layout: latest.current.layout,
|
|
1381
1146
|
glissando: latest.current.glissando,
|
|
1382
1147
|
midiMax: latest.current.midiMax,
|
|
1148
|
+
keyboardShortcuts: latest.current.keyboardShortcuts,
|
|
1149
|
+
keyboardShortcutsScope: latest.current.keyboardShortcutsScope,
|
|
1383
1150
|
onPlayNote: (note, velocity) => latest.current.onPlayNote?.(note, velocity),
|
|
1384
1151
|
onStopNote: (note) => latest.current.onStopNote?.(note),
|
|
1385
1152
|
onActiveNotesChange: setActiveNotes
|
|
@@ -1395,86 +1162,44 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
|
|
|
1395
1162
|
layout,
|
|
1396
1163
|
glissando,
|
|
1397
1164
|
midiMax,
|
|
1165
|
+
keyboardShortcuts,
|
|
1166
|
+
keyboardShortcutsScope,
|
|
1398
1167
|
onPlayNote,
|
|
1399
1168
|
onStopNote
|
|
1400
1169
|
};
|
|
1401
1170
|
instanceRef.current?.update({
|
|
1402
1171
|
layout,
|
|
1403
1172
|
glissando,
|
|
1404
|
-
midiMax
|
|
1173
|
+
midiMax,
|
|
1174
|
+
keyboardShortcuts,
|
|
1175
|
+
keyboardShortcutsScope
|
|
1405
1176
|
});
|
|
1406
1177
|
});
|
|
1178
|
+
const { first, last } = noteRange;
|
|
1407
1179
|
useEffect(() => {
|
|
1408
1180
|
if (!resizable || !node) return;
|
|
1409
1181
|
const parent = node.parentElement;
|
|
1410
1182
|
if (!parent) throw new Error("doesn't have a parent element.");
|
|
1411
1183
|
const resizeObserver = new ResizeObserver(() => {
|
|
1412
|
-
setResizedKeyWidth(node.clientWidth
|
|
1184
|
+
setResizedKeyWidth(fitWhiteKeyWidth(node.clientWidth, {
|
|
1185
|
+
noteRange: {
|
|
1186
|
+
first,
|
|
1187
|
+
last
|
|
1188
|
+
},
|
|
1189
|
+
keyGap,
|
|
1190
|
+
blackKeyWidthRatio
|
|
1191
|
+
}));
|
|
1413
1192
|
});
|
|
1414
1193
|
resizeObserver.observe(parent);
|
|
1415
1194
|
return () => resizeObserver.disconnect();
|
|
1416
1195
|
}, [
|
|
1417
1196
|
resizable,
|
|
1418
1197
|
node,
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
const hasShortcuts = shortcutKeys !== void 0;
|
|
1424
|
-
const shortcutMapping = shortcutKeys?.join("\0");
|
|
1425
|
-
useEffect(() => releaseShortcutNotes, [
|
|
1426
|
-
releaseShortcutNotes,
|
|
1427
|
-
shortcutMapping,
|
|
1428
|
-
keyboardShortcutsScope,
|
|
1429
|
-
noteRange.first,
|
|
1430
|
-
noteRange.last
|
|
1431
|
-
]);
|
|
1432
|
-
/** The note a shortcut key plays, or null when it has none. */
|
|
1433
|
-
function shortcutNote(key) {
|
|
1434
|
-
if (!shortcutKeys || key === "") return null;
|
|
1435
|
-
const index = shortcutKeys.indexOf(key);
|
|
1436
|
-
const note = noteRange.first + index;
|
|
1437
|
-
return index === -1 || note > noteRange.last ? null : note;
|
|
1438
|
-
}
|
|
1439
|
-
const shortcutTarget = useMemo(() => () => {
|
|
1440
|
-
if (!hasShortcuts) return null;
|
|
1441
|
-
return keyboardShortcutsScope === "window" ? globalThis.window : node;
|
|
1442
|
-
}, [
|
|
1443
|
-
hasShortcuts,
|
|
1444
|
-
keyboardShortcutsScope,
|
|
1445
|
-
node
|
|
1446
|
-
]);
|
|
1447
|
-
const focusOutTarget = useMemo(() => () => hasShortcuts && keyboardShortcutsScope === "root" ? node : null, [
|
|
1448
|
-
hasShortcuts,
|
|
1449
|
-
keyboardShortcutsScope,
|
|
1450
|
-
node
|
|
1198
|
+
first,
|
|
1199
|
+
last,
|
|
1200
|
+
keyGap,
|
|
1201
|
+
blackKeyWidthRatio
|
|
1451
1202
|
]);
|
|
1452
|
-
const windowBlurTarget = useMemo(() => () => hasShortcuts ? globalThis.window : null, [hasShortcuts]);
|
|
1453
|
-
useEventListener(shortcutTarget, "keydown", (e) => {
|
|
1454
|
-
const key = shortcutKey(e);
|
|
1455
|
-
if (e.repeat || shortcutNotes.current.has(key)) return;
|
|
1456
|
-
if (isEditableTarget(e.target)) return;
|
|
1457
|
-
const note = shortcutNote(e.key);
|
|
1458
|
-
if (note === null) return;
|
|
1459
|
-
const source = `keyboard:${key}`;
|
|
1460
|
-
shortcutNotes.current.set(key, {
|
|
1461
|
-
note,
|
|
1462
|
-
source
|
|
1463
|
-
});
|
|
1464
|
-
instanceRef.current?.noteOn(note, { source });
|
|
1465
|
-
});
|
|
1466
|
-
useEventListener(shortcutTarget, "keyup", (e) => {
|
|
1467
|
-
const key = shortcutKey(e);
|
|
1468
|
-
const shortcut = shortcutNotes.current.get(key);
|
|
1469
|
-
if (!shortcut) return;
|
|
1470
|
-
shortcutNotes.current.delete(key);
|
|
1471
|
-
instanceRef.current?.noteOff(shortcut.note, { source: shortcut.source });
|
|
1472
|
-
});
|
|
1473
|
-
useEventListener(focusOutTarget, "focusout", (e) => {
|
|
1474
|
-
if (e.relatedTarget instanceof Node && node?.contains(e.relatedTarget)) return;
|
|
1475
|
-
releaseShortcutNotes();
|
|
1476
|
-
});
|
|
1477
|
-
useEventListener(windowBlurTarget, "blur", releaseShortcutNotes);
|
|
1478
1203
|
useImperativeHandle(forwardedRef, () => ({
|
|
1479
1204
|
playNote: (note, velocity) => instanceRef.current?.noteOn(note, {
|
|
1480
1205
|
source: "api",
|
|
@@ -1490,7 +1215,6 @@ const Piano = { Root: /* @__PURE__ */ forwardRef(function Root({ noteRange, glis
|
|
|
1490
1215
|
tabIndex: 0,
|
|
1491
1216
|
style: {
|
|
1492
1217
|
width: resizable ? "100%" : pianoWidth(layout),
|
|
1493
|
-
"--height": cssLength(height),
|
|
1494
1218
|
position: "relative",
|
|
1495
1219
|
...style
|
|
1496
1220
|
},
|
|
@@ -1556,43 +1280,58 @@ function usePointsEditorContext(selector) {
|
|
|
1556
1280
|
return selector ? selector(context) : context;
|
|
1557
1281
|
}
|
|
1558
1282
|
//#endregion
|
|
1283
|
+
//#region src/components/PointsEditor/Container.tsx
|
|
1284
|
+
const Container = /* @__PURE__ */ forwardRef(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1285
|
+
const { containerRef, disabled, selectable, editor } = usePointsEditorContext();
|
|
1286
|
+
const { refCallback: dragRefCallback } = useDragValue({
|
|
1287
|
+
axis: POINT_AXIS,
|
|
1288
|
+
baseElementRef: containerRef,
|
|
1289
|
+
shouldStart: (event) => !disabled && !editor.isPointElement(event.target),
|
|
1290
|
+
onDragStart: ([x, y], state) => {
|
|
1291
|
+
editor.beginSelectionBox({
|
|
1292
|
+
x,
|
|
1293
|
+
y
|
|
1294
|
+
}, state.event);
|
|
1295
|
+
},
|
|
1296
|
+
onChange: ([x, y]) => editor.moveSelectionBox({
|
|
1297
|
+
x,
|
|
1298
|
+
y
|
|
1299
|
+
}),
|
|
1300
|
+
onDragEnd: () => editor.endSelectionBox()
|
|
1301
|
+
});
|
|
1302
|
+
useWheel((event) => {
|
|
1303
|
+
const move = wheelMove(event);
|
|
1304
|
+
if (!move) return;
|
|
1305
|
+
const axis = move.axis === 0 ? "x" : "y";
|
|
1306
|
+
if (editor.nudgeFocusedPoint(axis, move.direction, event)) event.preventDefault();
|
|
1307
|
+
}, { target: containerRef });
|
|
1308
|
+
const composedRef = useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1309
|
+
return /* @__PURE__ */ jsx("div", {
|
|
1310
|
+
ref: composedRef,
|
|
1311
|
+
className,
|
|
1312
|
+
style: {
|
|
1313
|
+
position: "absolute",
|
|
1314
|
+
inset: 0,
|
|
1315
|
+
zIndex: 10,
|
|
1316
|
+
...style
|
|
1317
|
+
},
|
|
1318
|
+
...props,
|
|
1319
|
+
children: /* @__PURE__ */ jsx(Placement, {
|
|
1320
|
+
name: "PointsEditor.Container",
|
|
1321
|
+
children
|
|
1322
|
+
})
|
|
1323
|
+
});
|
|
1324
|
+
});
|
|
1325
|
+
//#endregion
|
|
1559
1326
|
//#region src/components/_util/VisuallyHiddenRangeInput.tsx
|
|
1560
|
-
const hiddenStyle = {
|
|
1561
|
-
position: "absolute",
|
|
1562
|
-
width: 1,
|
|
1563
|
-
height: 1,
|
|
1564
|
-
padding: 0,
|
|
1565
|
-
margin: -1,
|
|
1566
|
-
overflow: "hidden",
|
|
1567
|
-
clip: "rect(0, 0, 0, 0)",
|
|
1568
|
-
clipPath: "inset(50%)",
|
|
1569
|
-
whiteSpace: "nowrap",
|
|
1570
|
-
border: 0,
|
|
1571
|
-
pointerEvents: "none"
|
|
1572
|
-
};
|
|
1573
1327
|
const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef((props, ref) => /* @__PURE__ */ jsx("input", {
|
|
1574
1328
|
...props,
|
|
1575
1329
|
ref,
|
|
1576
1330
|
type: "range",
|
|
1577
|
-
style:
|
|
1331
|
+
style: visuallyHiddenStyle
|
|
1578
1332
|
}));
|
|
1579
1333
|
//#endregion
|
|
1580
1334
|
//#region src/components/PointsEditor/Point.tsx
|
|
1581
|
-
function clampPoint(point, min, max) {
|
|
1582
|
-
const { x, y } = point;
|
|
1583
|
-
return {
|
|
1584
|
-
x: clamp(x, min?.x ?? 0, max?.x ?? 1),
|
|
1585
|
-
y: clamp(y, min?.y ?? 0, max?.y ?? 1)
|
|
1586
|
-
};
|
|
1587
|
-
}
|
|
1588
|
-
/**
|
|
1589
|
-
* A point is placed by its position within the container, so its value is a
|
|
1590
|
-
* position: 0..1 on each axis, with y growing downwards.
|
|
1591
|
-
*/
|
|
1592
|
-
const AXIS = {
|
|
1593
|
-
min: 0,
|
|
1594
|
-
max: 1
|
|
1595
|
-
};
|
|
1596
1335
|
/**
|
|
1597
1336
|
* One setting for both axes, or one per axis. The point holds a range input
|
|
1598
1337
|
* for each, so anything that names or describes it comes in a pair.
|
|
@@ -1600,8 +1339,8 @@ const AXIS = {
|
|
|
1600
1339
|
function perAxis(value, axis) {
|
|
1601
1340
|
return typeof value === "string" ? value : value?.[axis];
|
|
1602
1341
|
}
|
|
1603
|
-
function Point({ value, children, id: idProp, min, max,
|
|
1604
|
-
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection,
|
|
1342
|
+
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 }) {
|
|
1343
|
+
const { containerRef, externalStyles, disabled: rootDisabled, readonly: rootReadonly, wheel: rootWheel, keyboard: rootKeyboard, dragSensitivity, selection, editor } = usePointsEditorContext();
|
|
1605
1344
|
const generatedId = useId();
|
|
1606
1345
|
const id = idProp ?? generatedId;
|
|
1607
1346
|
const selected = selection.includes(id);
|
|
@@ -1634,11 +1373,11 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1634
1373
|
wheel
|
|
1635
1374
|
};
|
|
1636
1375
|
});
|
|
1637
|
-
useEffect(() => registerPoint(id, registration), [id,
|
|
1376
|
+
useEffect(() => editor.registerPoint(id, () => registration.current), [id, editor]);
|
|
1638
1377
|
/** Where the pointer was when the drag started, to measure the move from. */
|
|
1639
1378
|
const pointerOrigin = useRef(null);
|
|
1640
1379
|
const { refCallback: dragRefCallback, dragging } = useDragValue({
|
|
1641
|
-
axis:
|
|
1380
|
+
axis: POINT_AXIS,
|
|
1642
1381
|
baseElementRef: containerRef,
|
|
1643
1382
|
sensitivity: (state) => selectModifier(dragSensitivity, state.event).value,
|
|
1644
1383
|
cursor: inactive ? void 0 : externalStyles.cursor,
|
|
@@ -1646,13 +1385,13 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1646
1385
|
onChange: ([x, y]) => {
|
|
1647
1386
|
const origin = pointerOrigin.current;
|
|
1648
1387
|
if (!origin) return;
|
|
1649
|
-
movePointDrag({
|
|
1388
|
+
editor.movePointDrag({
|
|
1650
1389
|
x: x - origin.x,
|
|
1651
1390
|
y: y - origin.y
|
|
1652
1391
|
});
|
|
1653
1392
|
},
|
|
1654
1393
|
onDragStart: ([x, y], state) => {
|
|
1655
|
-
beginPointDrag(id, state.event);
|
|
1394
|
+
editor.beginPointDrag(id, state.event);
|
|
1656
1395
|
pointerOrigin.current = {
|
|
1657
1396
|
x,
|
|
1658
1397
|
y
|
|
@@ -1667,30 +1406,13 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1667
1406
|
onDragEnd?.(clampPoint(value, min, max));
|
|
1668
1407
|
}
|
|
1669
1408
|
});
|
|
1670
|
-
const nudge = useCallback((axis, direction, option, modifiers) => {
|
|
1671
|
-
const next = applyDelta(value[axis], direction, option, AXIS, modifiers);
|
|
1672
|
-
nudgeSelection(id, {
|
|
1673
|
-
x: axis === "x" ? next - value.x : 0,
|
|
1674
|
-
y: axis === "y" ? next - value.y : 0
|
|
1675
|
-
});
|
|
1676
|
-
}, [
|
|
1677
|
-
value,
|
|
1678
|
-
id,
|
|
1679
|
-
nudgeSelection
|
|
1680
|
-
]);
|
|
1681
1409
|
const refCallback = useComposedRefs(dragRefCallback, setElement);
|
|
1682
1410
|
const handleKeyDown = (event) => {
|
|
1683
|
-
const
|
|
1684
|
-
if (!
|
|
1685
|
-
"ArrowRight",
|
|
1686
|
-
"ArrowLeft",
|
|
1687
|
-
"ArrowUp",
|
|
1688
|
-
"ArrowDown"
|
|
1689
|
-
].includes(key)) return;
|
|
1690
|
-
const axis = key === "ArrowRight" || key === "ArrowLeft" ? "x" : "y";
|
|
1411
|
+
const move = arrowKeyMove(event.key);
|
|
1412
|
+
if (!move) return;
|
|
1691
1413
|
event.preventDefault();
|
|
1692
1414
|
if (!onChange || inactive || !keyboard) return;
|
|
1693
|
-
|
|
1415
|
+
editor.nudgePoint(id, move.axis === 0 ? "x" : "y", move.direction, keyboard, event);
|
|
1694
1416
|
};
|
|
1695
1417
|
const current = clampPoint(value, min, max);
|
|
1696
1418
|
return /* @__PURE__ */ jsxs("div", {
|
|
@@ -1703,8 +1425,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1703
1425
|
"data-selected": selected ? "" : void 0,
|
|
1704
1426
|
style: {
|
|
1705
1427
|
"--color": color,
|
|
1706
|
-
"--width": cssLength(size ?? width),
|
|
1707
|
-
"--height": cssLength(size ?? height),
|
|
1708
1428
|
position: "absolute",
|
|
1709
1429
|
translate: "var(--translate, -50% -50%)",
|
|
1710
1430
|
...style,
|
|
@@ -1739,7 +1459,7 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1739
1459
|
return;
|
|
1740
1460
|
}
|
|
1741
1461
|
const delta = event.currentTarget.valueAsNumber - value[axis];
|
|
1742
|
-
nudgeSelection(id, {
|
|
1462
|
+
editor.nudgeSelection(id, {
|
|
1743
1463
|
x: axis === "x" ? delta : 0,
|
|
1744
1464
|
y: axis === "y" ? delta : 0
|
|
1745
1465
|
});
|
|
@@ -1748,50 +1468,6 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
|
|
|
1748
1468
|
});
|
|
1749
1469
|
}
|
|
1750
1470
|
//#endregion
|
|
1751
|
-
//#region src/components/PointsEditor/Container.tsx
|
|
1752
|
-
const Container = /* @__PURE__ */ forwardRef(function Container({ children, className, style, ...props }, forwardedRef) {
|
|
1753
|
-
const { containerRef, disabled, selectable, isPointElement, nudgeFocusedPoint, beginSelectionBox, moveSelectionBox, endSelectionBox } = usePointsEditorContext();
|
|
1754
|
-
const { refCallback: dragRefCallback } = useDragValue({
|
|
1755
|
-
axis: AXIS,
|
|
1756
|
-
baseElementRef: containerRef,
|
|
1757
|
-
shouldStart: (event) => !disabled && !isPointElement(event.target),
|
|
1758
|
-
onDragStart: ([x, y], state) => {
|
|
1759
|
-
beginSelectionBox({
|
|
1760
|
-
x,
|
|
1761
|
-
y
|
|
1762
|
-
}, state.event);
|
|
1763
|
-
},
|
|
1764
|
-
onChange: ([x, y]) => moveSelectionBox({
|
|
1765
|
-
x,
|
|
1766
|
-
y
|
|
1767
|
-
}),
|
|
1768
|
-
onDragEnd: endSelectionBox
|
|
1769
|
-
});
|
|
1770
|
-
useWheel((event) => {
|
|
1771
|
-
const horizontal = event.deltaX !== 0;
|
|
1772
|
-
const delta = horizontal ? event.deltaX : event.deltaY;
|
|
1773
|
-
if (delta === 0) return;
|
|
1774
|
-
const axis = horizontal || event.shiftKey ? "x" : "y";
|
|
1775
|
-
if (nudgeFocusedPoint(axis, delta < 0 ? -1 : 1, event)) event.preventDefault();
|
|
1776
|
-
}, { target: containerRef });
|
|
1777
|
-
const composedRef = useComposedRefs(forwardedRef, containerRef, selectable && !disabled ? dragRefCallback : void 0);
|
|
1778
|
-
return /* @__PURE__ */ jsx("div", {
|
|
1779
|
-
ref: composedRef,
|
|
1780
|
-
className,
|
|
1781
|
-
style: {
|
|
1782
|
-
position: "absolute",
|
|
1783
|
-
inset: 0,
|
|
1784
|
-
zIndex: 10,
|
|
1785
|
-
...style
|
|
1786
|
-
},
|
|
1787
|
-
...props,
|
|
1788
|
-
children: /* @__PURE__ */ jsx(Placement, {
|
|
1789
|
-
name: "PointsEditor.Container",
|
|
1790
|
-
children
|
|
1791
|
-
})
|
|
1792
|
-
});
|
|
1793
|
-
});
|
|
1794
|
-
//#endregion
|
|
1795
1471
|
//#region src/components/PointsEditor/SelectionBox.tsx
|
|
1796
1472
|
/**
|
|
1797
1473
|
* The box a drag on empty space draws, and what it covers is selected.
|
|
@@ -1824,181 +1500,28 @@ function SelectionBox({ children, className, style, ...props }) {
|
|
|
1824
1500
|
/** One array for every editor with selection turned off, so memos hold still. */
|
|
1825
1501
|
const EMPTY = [];
|
|
1826
1502
|
/**
|
|
1827
|
-
* How far a point may move before something in the selection leaves its range.
|
|
1828
|
-
*
|
|
1829
|
-
* Clamping each point on its own would break the shape of the selection: the
|
|
1830
|
-
* one that reached the edge would stop while the rest carried on. One amount
|
|
1831
|
-
* for all of them means the whole selection stops together.
|
|
1832
|
-
*/
|
|
1833
|
-
function allowedDelta(delta, entries) {
|
|
1834
|
-
let loX = -Infinity;
|
|
1835
|
-
let hiX = Infinity;
|
|
1836
|
-
let loY = -Infinity;
|
|
1837
|
-
let hiY = Infinity;
|
|
1838
|
-
for (const { start, registration } of entries) {
|
|
1839
|
-
loX = Math.max(loX, (registration.min?.x ?? 0) - start.x);
|
|
1840
|
-
hiX = Math.min(hiX, (registration.max?.x ?? 1) - start.x);
|
|
1841
|
-
loY = Math.max(loY, (registration.min?.y ?? 0) - start.y);
|
|
1842
|
-
hiY = Math.min(hiY, (registration.max?.y ?? 1) - start.y);
|
|
1843
|
-
}
|
|
1844
|
-
return {
|
|
1845
|
-
x: hiX < loX ? 0 : clamp(delta.x, loX, hiX),
|
|
1846
|
-
y: hiY < loY ? 0 : clamp(delta.y, loY, hiY)
|
|
1847
|
-
};
|
|
1848
|
-
}
|
|
1849
|
-
/**
|
|
1850
|
-
* A point moves over 0..1 in both axes, so a nudge of 0.01 crosses the editor
|
|
1851
|
-
* in a hundred steps whatever its pixel size.
|
|
1852
|
-
*/
|
|
1853
|
-
const DEFAULT_WHEEL = ["normalized", .01];
|
|
1854
|
-
/**
|
|
1855
|
-
* Shift is the fine-adjustment key everywhere else, so it is bound here too
|
|
1856
|
-
* — but only on the keyboard. On the wheel it already means the x axis, and
|
|
1857
|
-
* browsers hand shift+wheel over as horizontal scrolling anyway.
|
|
1858
|
-
*/
|
|
1859
|
-
const DEFAULT_KEYBOARD = {
|
|
1860
|
-
default: ["normalized", .01],
|
|
1861
|
-
shift: ["normalized", .001]
|
|
1862
|
-
};
|
|
1863
|
-
/**
|
|
1864
1503
|
* Multiple Point Controller
|
|
1865
1504
|
*/
|
|
1866
1505
|
const PointsEditor = {
|
|
1867
|
-
Root: /* @__PURE__ */ forwardRef(({
|
|
1506
|
+
Root: /* @__PURE__ */ forwardRef(({ disabled = false, readonly = false, wheel = POINTS_EDITOR_DEFAULT_WHEEL, keyboard = POINTS_EDITOR_DEFAULT_KEYBOARD, dragSensitivity = DEFAULT_DRAG_SENSITIVITY, selectable = false, selection: selectionProp, defaultSelection, onSelectionChange, externalStyles, style, className, children, ...props }, forwardedRef) => {
|
|
1868
1507
|
const containerRef = useRef(null);
|
|
1869
1508
|
const { cursor = "grabbing" } = externalStyles ?? {};
|
|
1870
1509
|
const controlled = selectionProp !== void 0;
|
|
1871
1510
|
const [ownSelection, setOwnSelection] = useState(defaultSelection ?? []);
|
|
1872
1511
|
const selection = selectable ? selectionProp ?? ownSelection : EMPTY;
|
|
1873
|
-
const selectionRef = useRef(selection);
|
|
1874
|
-
const changeHandlerRef = useRef(onSelectionChange);
|
|
1875
|
-
useEffect(() => {
|
|
1876
|
-
selectionRef.current = selection;
|
|
1877
|
-
changeHandlerRef.current = onSelectionChange;
|
|
1878
|
-
});
|
|
1879
|
-
const changeSelection = useCallback((next) => {
|
|
1880
|
-
selectionRef.current = next;
|
|
1881
|
-
if (!controlled) setOwnSelection(next);
|
|
1882
|
-
changeHandlerRef.current?.(next);
|
|
1883
|
-
}, [controlled]);
|
|
1884
|
-
/**
|
|
1885
|
-
* Every mounted point, by id. A registration is a ref rather than a value:
|
|
1886
|
-
* the value inside changes on every frame of a drag, and a registry keyed
|
|
1887
|
-
* on it would be rebuilt just as often.
|
|
1888
|
-
*/
|
|
1889
|
-
const points = useRef(/* @__PURE__ */ new Map());
|
|
1890
|
-
const registerPoint = useCallback((id, entry) => {
|
|
1891
|
-
points.current.set(id, entry);
|
|
1892
|
-
return () => {
|
|
1893
|
-
points.current.delete(id);
|
|
1894
|
-
};
|
|
1895
|
-
}, []);
|
|
1896
|
-
/** What the current drag picked up, and where those points started. */
|
|
1897
|
-
const dragRef = useRef([]);
|
|
1898
|
-
const snapshot = useCallback((ids) => {
|
|
1899
|
-
return ids.flatMap((id) => {
|
|
1900
|
-
const registration = points.current.get(id)?.current;
|
|
1901
|
-
return registration ? [{
|
|
1902
|
-
id,
|
|
1903
|
-
start: { ...registration.value }
|
|
1904
|
-
}] : [];
|
|
1905
|
-
});
|
|
1906
|
-
}, []);
|
|
1907
|
-
const applyDeltaTo = useCallback((entries, delta) => {
|
|
1908
|
-
const withRegistration = entries.flatMap((entry) => {
|
|
1909
|
-
const registration = points.current.get(entry.id)?.current;
|
|
1910
|
-
return registration ? [{
|
|
1911
|
-
...entry,
|
|
1912
|
-
registration
|
|
1913
|
-
}] : [];
|
|
1914
|
-
});
|
|
1915
|
-
const allowed = allowedDelta(delta, withRegistration);
|
|
1916
|
-
for (const { start, registration } of withRegistration) {
|
|
1917
|
-
if (registration.readonly) continue;
|
|
1918
|
-
registration.onChange?.({
|
|
1919
|
-
x: toPrecision(start.x + allowed.x),
|
|
1920
|
-
y: toPrecision(start.y + allowed.y)
|
|
1921
|
-
});
|
|
1922
|
-
}
|
|
1923
|
-
}, []);
|
|
1924
|
-
const beginPointDrag = useCallback((id, modifiers) => {
|
|
1925
|
-
if (!selectable) {
|
|
1926
|
-
dragRef.current = snapshot([id]);
|
|
1927
|
-
return;
|
|
1928
|
-
}
|
|
1929
|
-
const current = selectionRef.current;
|
|
1930
|
-
const additive = modifiers.ctrlKey || modifiers.metaKey;
|
|
1931
|
-
let next;
|
|
1932
|
-
if (additive) next = current.includes(id) ? current.filter((x) => x !== id) : [...current, id];
|
|
1933
|
-
else if (current.includes(id)) next = current;
|
|
1934
|
-
else next = [id];
|
|
1935
|
-
changeSelection([...next]);
|
|
1936
|
-
dragRef.current = next.includes(id) ? snapshot(next) : [];
|
|
1937
|
-
}, [
|
|
1938
|
-
selectable,
|
|
1939
|
-
changeSelection,
|
|
1940
|
-
snapshot
|
|
1941
|
-
]);
|
|
1942
|
-
const movePointDrag = useCallback((delta) => applyDeltaTo(dragRef.current, delta), [applyDeltaTo]);
|
|
1943
|
-
const nudgeSelection = useCallback((id, delta) => {
|
|
1944
|
-
const ids = selectionRef.current.includes(id) ? selectionRef.current : [id];
|
|
1945
|
-
applyDeltaTo(snapshot(ids), delta);
|
|
1946
|
-
}, [applyDeltaTo, snapshot]);
|
|
1947
|
-
const isPointElement = useCallback((element) => {
|
|
1948
|
-
if (!element) return false;
|
|
1949
|
-
for (const [, entry] of points.current) if (entry.current.element?.contains(element)) return true;
|
|
1950
|
-
return false;
|
|
1951
|
-
}, []);
|
|
1952
|
-
const nudgeFocusedPoint = useCallback((axis, direction, modifiers) => {
|
|
1953
|
-
const container = containerRef.current;
|
|
1954
|
-
const active = container?.ownerDocument.activeElement;
|
|
1955
|
-
if (!container || !active || !container.contains(active)) return false;
|
|
1956
|
-
for (const [id, entry] of points.current) {
|
|
1957
|
-
const { element, wheel, readonly, onChange, value } = entry.current;
|
|
1958
|
-
if (!element?.contains(active)) continue;
|
|
1959
|
-
if (!wheel || readonly || !onChange) return false;
|
|
1960
|
-
const next = applyDelta(value[axis], direction, wheel, AXIS, modifiers);
|
|
1961
|
-
nudgeSelection(id, {
|
|
1962
|
-
x: axis === "x" ? next - value.x : 0,
|
|
1963
|
-
y: axis === "y" ? next - value.y : 0
|
|
1964
|
-
});
|
|
1965
|
-
return true;
|
|
1966
|
-
}
|
|
1967
|
-
return false;
|
|
1968
|
-
}, [nudgeSelection]);
|
|
1969
1512
|
const [selectionBox, setSelectionBox] = useState(null);
|
|
1970
|
-
const
|
|
1971
|
-
selectionBoxRef.current ??= createSelectionBox({
|
|
1972
|
-
*items() {
|
|
1973
|
-
for (const [id, entry] of points.current) {
|
|
1974
|
-
const { x, y } = entry.current.value;
|
|
1975
|
-
yield [id, [x, y]];
|
|
1976
|
-
}
|
|
1977
|
-
},
|
|
1978
|
-
onBoxChange: setSelectionBox
|
|
1979
|
-
});
|
|
1513
|
+
const [editor] = useState(() => createPointsEditor({ onSelectionBoxChange: setSelectionBox }));
|
|
1980
1514
|
useEffect(() => {
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
if (!selectable) return;
|
|
1989
|
-
selectionBoxRef.current?.begin([at.x, at.y], {
|
|
1990
|
-
additive: modifiers.ctrlKey || modifiers.metaKey,
|
|
1991
|
-
selection: selectionRef.current
|
|
1515
|
+
editor.update({
|
|
1516
|
+
selectable,
|
|
1517
|
+
selection,
|
|
1518
|
+
onSelectionChange: (next) => {
|
|
1519
|
+
if (!controlled) setOwnSelection(next);
|
|
1520
|
+
onSelectionChange?.(next);
|
|
1521
|
+
}
|
|
1992
1522
|
});
|
|
1993
|
-
}
|
|
1994
|
-
|
|
1995
|
-
selectionBoxRef.current?.move([to.x, to.y]);
|
|
1996
|
-
}, []);
|
|
1997
|
-
const endSelectionBox = useCallback(() => {
|
|
1998
|
-
if (!selectionBoxRef.current?.end()) return;
|
|
1999
|
-
const [first] = selectionRef.current;
|
|
2000
|
-
if (first) points.current.get(first)?.current.element?.focus();
|
|
2001
|
-
}, []);
|
|
1523
|
+
});
|
|
1524
|
+
useEffect(() => () => editor.destroy(), [editor]);
|
|
2002
1525
|
const context = useMemo(() => ({
|
|
2003
1526
|
disabled,
|
|
2004
1527
|
readonly,
|
|
@@ -2009,16 +1532,8 @@ const PointsEditor = {
|
|
|
2009
1532
|
containerRef,
|
|
2010
1533
|
selectable,
|
|
2011
1534
|
selection,
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
beginPointDrag,
|
|
2015
|
-
movePointDrag,
|
|
2016
|
-
nudgeSelection,
|
|
2017
|
-
nudgeFocusedPoint,
|
|
2018
|
-
selectionBox,
|
|
2019
|
-
beginSelectionBox,
|
|
2020
|
-
moveSelectionBox,
|
|
2021
|
-
endSelectionBox
|
|
1535
|
+
editor,
|
|
1536
|
+
selectionBox
|
|
2022
1537
|
}), [
|
|
2023
1538
|
disabled,
|
|
2024
1539
|
readonly,
|
|
@@ -2028,16 +1543,8 @@ const PointsEditor = {
|
|
|
2028
1543
|
cursor,
|
|
2029
1544
|
selectable,
|
|
2030
1545
|
selection,
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
beginPointDrag,
|
|
2034
|
-
movePointDrag,
|
|
2035
|
-
nudgeSelection,
|
|
2036
|
-
nudgeFocusedPoint,
|
|
2037
|
-
selectionBox,
|
|
2038
|
-
beginSelectionBox,
|
|
2039
|
-
moveSelectionBox,
|
|
2040
|
-
endSelectionBox
|
|
1546
|
+
editor,
|
|
1547
|
+
selectionBox
|
|
2041
1548
|
]);
|
|
2042
1549
|
return /* @__PURE__ */ jsx(PointsEditorProvider, {
|
|
2043
1550
|
value: context,
|
|
@@ -2047,8 +1554,6 @@ const PointsEditor = {
|
|
|
2047
1554
|
"data-disabled": disabled ? "" : void 0,
|
|
2048
1555
|
"data-readonly": readonly ? "" : void 0,
|
|
2049
1556
|
style: {
|
|
2050
|
-
"--width": cssLength(width),
|
|
2051
|
-
"--height": cssLength(height),
|
|
2052
1557
|
position: "relative",
|
|
2053
1558
|
...style
|
|
2054
1559
|
},
|
|
@@ -2073,36 +1578,30 @@ function useSliderContext(selector) {
|
|
|
2073
1578
|
}
|
|
2074
1579
|
//#endregion
|
|
2075
1580
|
//#region src/components/Slider/MarksOption.tsx
|
|
2076
|
-
function MarksOption({ value, mark = true, label, thickness, length, gap,
|
|
1581
|
+
function MarksOption({ value, mark = true, label, thickness, length, gap, classes, styles, className, style, ...props }) {
|
|
2077
1582
|
useCheckPlacement("Slider.MarksOption", "Slider.Marks");
|
|
2078
1583
|
const min = useSliderContext((s) => s.min);
|
|
2079
1584
|
const max = useSliderContext((s) => s.max);
|
|
2080
1585
|
const scale = useSliderContext((s) => s.scale);
|
|
2081
1586
|
const vertical = useSliderContext((s) => s.vertical);
|
|
2082
1587
|
const reverse = useSliderContext((s) => s.reverse);
|
|
2083
|
-
const
|
|
2084
|
-
const percent = scale.normalize(value, min, max) * 100;
|
|
2085
|
-
return toFixed(xor(vertical, reverse) ? 100 - percent : percent);
|
|
2086
|
-
}, [
|
|
2087
|
-
vertical,
|
|
2088
|
-
reverse,
|
|
2089
|
-
max,
|
|
1588
|
+
const percent = valuePercent(value, {
|
|
2090
1589
|
min,
|
|
1590
|
+
max,
|
|
2091
1591
|
scale
|
|
2092
|
-
|
|
1592
|
+
}, vertical !== reverse);
|
|
2093
1593
|
return /* @__PURE__ */ jsxs("div", {
|
|
2094
1594
|
className,
|
|
2095
1595
|
style: {
|
|
2096
1596
|
"--thickness": cssLength(thickness),
|
|
2097
1597
|
"--length": cssLength(length),
|
|
2098
1598
|
"--gap": cssLength(gap),
|
|
2099
|
-
"--label-width": cssLength(labelWidth),
|
|
2100
1599
|
position: "absolute",
|
|
2101
1600
|
translate: vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
|
|
2102
1601
|
zIndex: 10,
|
|
2103
1602
|
...style,
|
|
2104
|
-
left: !vertical ? `${
|
|
2105
|
-
top: vertical ? `${
|
|
1603
|
+
left: !vertical ? `${percent}%` : void 0,
|
|
1604
|
+
top: vertical ? `${percent}%` : void 0
|
|
2106
1605
|
},
|
|
2107
1606
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2108
1607
|
...props,
|
|
@@ -2118,23 +1617,6 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, labelW
|
|
|
2118
1617
|
});
|
|
2119
1618
|
}
|
|
2120
1619
|
//#endregion
|
|
2121
|
-
//#region src/components/Slider/type.ts
|
|
2122
|
-
function generateOptionsList(options, min, max, step) {
|
|
2123
|
-
const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
|
|
2124
|
-
const optionsList = [];
|
|
2125
|
-
const interval = per === "step" ? step : per;
|
|
2126
|
-
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
|
|
2127
|
-
for (let i = 0; i < count; i++) {
|
|
2128
|
-
const value = toFixed(interval * (Math.ceil(min / interval) + i), decimalPart(interval)?.length);
|
|
2129
|
-
optionsList.push({
|
|
2130
|
-
value,
|
|
2131
|
-
mark,
|
|
2132
|
-
label
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
return optionsList;
|
|
2136
|
-
}
|
|
2137
|
-
//#endregion
|
|
2138
1620
|
//#region src/components/Slider/Marks.tsx
|
|
2139
1621
|
function Marks({ gap, options, children, className, style, ...props }) {
|
|
2140
1622
|
const min = useSliderContext((s) => s.min);
|
|
@@ -2142,8 +1624,8 @@ function Marks({ gap, options, children, className, style, ...props }) {
|
|
|
2142
1624
|
const step = useSliderContext((s) => s.step);
|
|
2143
1625
|
const vertical = useSliderContext((s) => s.vertical);
|
|
2144
1626
|
const reverse = useSliderContext((s) => s.reverse);
|
|
2145
|
-
const optionsList = options ?
|
|
2146
|
-
if (
|
|
1627
|
+
const optionsList = options ? sliderMarks(options, min, max, step) : [];
|
|
1628
|
+
if (vertical !== reverse) optionsList.reverse();
|
|
2147
1629
|
return /* @__PURE__ */ jsx("div", {
|
|
2148
1630
|
className,
|
|
2149
1631
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
@@ -2228,7 +1710,7 @@ const Track = /* @__PURE__ */ forwardRef(function Track({ length, thickness, act
|
|
|
2228
1710
|
className,
|
|
2229
1711
|
"data-disabled": disabled ? "" : void 0,
|
|
2230
1712
|
"data-orientation": vertical ? "vertical" : "horizontal",
|
|
2231
|
-
"data-flipped":
|
|
1713
|
+
"data-flipped": vertical !== reverse ? "" : void 0,
|
|
2232
1714
|
style: {
|
|
2233
1715
|
"--active": active,
|
|
2234
1716
|
"--inactive": inactive,
|
|
@@ -2265,10 +1747,12 @@ const Slider = {
|
|
|
2265
1747
|
..._externalStyles
|
|
2266
1748
|
};
|
|
2267
1749
|
const inactive = disabled || readonly;
|
|
2268
|
-
const
|
|
2269
|
-
const
|
|
2270
|
-
|
|
2271
|
-
|
|
1750
|
+
const displayReversed = vertical !== reverse;
|
|
1751
|
+
const percent = valuePercent(value, {
|
|
1752
|
+
min,
|
|
1753
|
+
max,
|
|
1754
|
+
scale
|
|
1755
|
+
}, displayReversed);
|
|
2272
1756
|
const axis = useMemo(() => ({
|
|
2273
1757
|
min,
|
|
2274
1758
|
max,
|
|
@@ -2289,18 +1773,11 @@ const Slider = {
|
|
|
2289
1773
|
wheel
|
|
2290
1774
|
});
|
|
2291
1775
|
const handleKeyDown = useCallback((event) => {
|
|
2292
|
-
const
|
|
2293
|
-
if (
|
|
2294
|
-
"ArrowRight",
|
|
2295
|
-
"ArrowLeft",
|
|
2296
|
-
"ArrowUp",
|
|
2297
|
-
"ArrowDown"
|
|
2298
|
-
].includes(key)) return;
|
|
1776
|
+
const direction = arrowKeyDirection(event.key);
|
|
1777
|
+
if (direction === null) return;
|
|
2299
1778
|
event.preventDefault();
|
|
2300
1779
|
if (!keyboard || !onChange || inactive) return;
|
|
2301
|
-
|
|
2302
|
-
if (reverse) direction *= -1;
|
|
2303
|
-
onChange(applyDelta(value, direction, keyboard, axis, event));
|
|
1780
|
+
onChange(applyDelta(value, reverse ? -direction : direction, keyboard, axis, event));
|
|
2304
1781
|
}, [
|
|
2305
1782
|
keyboard,
|
|
2306
1783
|
onChange,
|
|
@@ -2333,15 +1810,10 @@ const Slider = {
|
|
|
2333
1810
|
});
|
|
2334
1811
|
const wheelRefCallback = useWheel((event) => {
|
|
2335
1812
|
if (!wheel || !onChange || inactive) return;
|
|
1813
|
+
const direction = wheelDirection(event, { horizontal: !vertical });
|
|
1814
|
+
if (direction === null) return;
|
|
2336
1815
|
event.preventDefault();
|
|
2337
|
-
|
|
2338
|
-
if (!vertical && event.deltaX !== 0) direction = event.deltaX > 0 ? 1 : -1;
|
|
2339
|
-
else {
|
|
2340
|
-
if (event.deltaY === 0) return;
|
|
2341
|
-
direction = event.deltaY > 0 ? -1 : 1;
|
|
2342
|
-
}
|
|
2343
|
-
if (reverse) direction *= -1;
|
|
2344
|
-
onChange(applyDelta(value, direction, wheel, axis, event));
|
|
1816
|
+
onChange(applyDelta(value, reverse ? -direction : direction, wheel, axis, event));
|
|
2345
1817
|
}, WHEEL_OPTIONS$1);
|
|
2346
1818
|
const rootRefCallback = useComposedRefs(dragRefCallback, wheelRefCallback);
|
|
2347
1819
|
const context = useMemo(() => ({
|
|
@@ -2426,16 +1898,13 @@ function useXYPadContext(selector) {
|
|
|
2426
1898
|
}
|
|
2427
1899
|
//#endregion
|
|
2428
1900
|
//#region src/components/XYPad/Area.tsx
|
|
2429
|
-
const Area = /* @__PURE__ */ forwardRef(function Area({
|
|
1901
|
+
const Area = /* @__PURE__ */ forwardRef(function Area({ children, className, style, ...props }, forwardedRef) {
|
|
2430
1902
|
const { areaRef } = useXYPadContext();
|
|
2431
1903
|
const composedRef = useComposedRefs(forwardedRef, areaRef);
|
|
2432
1904
|
return /* @__PURE__ */ jsx("div", {
|
|
2433
1905
|
ref: composedRef,
|
|
2434
1906
|
className,
|
|
2435
1907
|
style: {
|
|
2436
|
-
"--color": color,
|
|
2437
|
-
"--width": cssLength(width),
|
|
2438
|
-
"--height": cssLength(height),
|
|
2439
1908
|
position: "relative",
|
|
2440
1909
|
...style
|
|
2441
1910
|
},
|
|
@@ -2534,10 +2003,11 @@ const XYPad = {
|
|
|
2534
2003
|
const step = useMemo(() => toXY(_step), [_step]);
|
|
2535
2004
|
const scale = useMemo(() => toXY(_scale), [_scale]);
|
|
2536
2005
|
const reverse = useMemo(() => toXY(_reverse), [_reverse]);
|
|
2537
|
-
const percent = useMemo(() => {
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2006
|
+
const percent = useMemo(() => [0, 1].map((i) => valuePercent(value[i], {
|
|
2007
|
+
min: min[i],
|
|
2008
|
+
max: max[i],
|
|
2009
|
+
scale: scale[i]
|
|
2010
|
+
}, reverse[i])), [
|
|
2541
2011
|
value,
|
|
2542
2012
|
min,
|
|
2543
2013
|
max,
|
|
@@ -2579,20 +2049,12 @@ const XYPad = {
|
|
|
2579
2049
|
withAxis
|
|
2580
2050
|
]);
|
|
2581
2051
|
const handleKeyDown = useCallback((event) => {
|
|
2582
|
-
const
|
|
2583
|
-
if (!
|
|
2584
|
-
"ArrowRight",
|
|
2585
|
-
"ArrowLeft",
|
|
2586
|
-
"ArrowUp",
|
|
2587
|
-
"ArrowDown"
|
|
2588
|
-
].includes(key)) return;
|
|
2589
|
-
const i = key === "ArrowRight" || key === "ArrowLeft" ? 0 : 1;
|
|
2052
|
+
const move = arrowKeyMove(event.key);
|
|
2053
|
+
if (!move) return;
|
|
2590
2054
|
event.preventDefault();
|
|
2591
2055
|
if (!onChange || inactive || !keyboard) return;
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
if (reverse[i]) direction *= -1;
|
|
2595
|
-
onChange(nudge(i, direction, keyboard, event));
|
|
2056
|
+
const { axis: i, direction } = move;
|
|
2057
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event));
|
|
2596
2058
|
}, [
|
|
2597
2059
|
onChange,
|
|
2598
2060
|
inactive,
|
|
@@ -2623,15 +2085,11 @@ const XYPad = {
|
|
|
2623
2085
|
});
|
|
2624
2086
|
const wheelRefCallback = useWheel((event) => {
|
|
2625
2087
|
if (!onChange || inactive || !wheel) return;
|
|
2626
|
-
const
|
|
2627
|
-
|
|
2628
|
-
if (delta === 0) return;
|
|
2629
|
-
const i = horizontal || event.shiftKey ? 0 : 1;
|
|
2088
|
+
const move = wheelMove(event);
|
|
2089
|
+
if (!move) return;
|
|
2630
2090
|
event.preventDefault();
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
if (reverse[i]) direction *= -1;
|
|
2634
|
-
onChange(nudge(i, direction, wheel, event));
|
|
2091
|
+
const { axis: i, direction } = move;
|
|
2092
|
+
onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event));
|
|
2635
2093
|
}, WHEEL_OPTIONS);
|
|
2636
2094
|
const rootRefCallback = useComposedRefs(rootRef, dragRefCallback, wheelRefCallback);
|
|
2637
2095
|
const context = useMemo(() => ({
|
|
@@ -2716,6 +2174,82 @@ function useAnimationFrame(callback = () => {}, deps = []) {
|
|
|
2716
2174
|
}, [runCallback, ...deps]);
|
|
2717
2175
|
}
|
|
2718
2176
|
//#endregion
|
|
2177
|
+
//#region src/hooks/useDrag.ts
|
|
2178
|
+
/**
|
|
2179
|
+
* Track a pointer drag on an element.
|
|
2180
|
+
*
|
|
2181
|
+
* @returns a ref callback to attach to the element being dragged
|
|
2182
|
+
*/
|
|
2183
|
+
function useDrag({ threshold = 1, cursor, pointerLock, onDrag, onDragStart, onDragEnd }) {
|
|
2184
|
+
const dragHandler = useCallbackRef(onDrag);
|
|
2185
|
+
const dragStartHandler = useCallbackRef(onDragStart);
|
|
2186
|
+
const dragEndHandler = useCallbackRef(onDragEnd);
|
|
2187
|
+
const [node, setNode] = useState(null);
|
|
2188
|
+
useEffect(() => {
|
|
2189
|
+
if (!node) return;
|
|
2190
|
+
const instance = createDrag(node, {
|
|
2191
|
+
threshold,
|
|
2192
|
+
cursor,
|
|
2193
|
+
pointerLock,
|
|
2194
|
+
onDragStart: (state) => dragStartHandler(state),
|
|
2195
|
+
onDrag: (state) => dragHandler(state.x, state.y, state.deltaX, state.deltaY, state),
|
|
2196
|
+
onDragEnd: (state) => dragEndHandler(state)
|
|
2197
|
+
});
|
|
2198
|
+
return () => instance.destroy();
|
|
2199
|
+
}, [
|
|
2200
|
+
node,
|
|
2201
|
+
threshold,
|
|
2202
|
+
cursor,
|
|
2203
|
+
pointerLock,
|
|
2204
|
+
dragHandler,
|
|
2205
|
+
dragStartHandler,
|
|
2206
|
+
dragEndHandler
|
|
2207
|
+
]);
|
|
2208
|
+
return setNode;
|
|
2209
|
+
}
|
|
2210
|
+
//#endregion
|
|
2211
|
+
//#region src/hooks/useEventListener.ts
|
|
2212
|
+
function useEventListener(target, event, handler, options) {
|
|
2213
|
+
const listener = useCallbackRef(handler);
|
|
2214
|
+
const cleanupRef = useRef(() => {});
|
|
2215
|
+
useEffect(() => {
|
|
2216
|
+
const node = typeof target === "function" ? target() : target ?? document;
|
|
2217
|
+
if (!node) return;
|
|
2218
|
+
node.addEventListener(event, listener, options);
|
|
2219
|
+
const cleanup = () => {
|
|
2220
|
+
node.removeEventListener(event, listener, options);
|
|
2221
|
+
};
|
|
2222
|
+
cleanupRef.current = cleanup;
|
|
2223
|
+
return () => {
|
|
2224
|
+
cleanup();
|
|
2225
|
+
if (cleanupRef.current === cleanup) cleanupRef.current = () => {};
|
|
2226
|
+
};
|
|
2227
|
+
}, [
|
|
2228
|
+
event,
|
|
2229
|
+
target,
|
|
2230
|
+
options,
|
|
2231
|
+
listener
|
|
2232
|
+
]);
|
|
2233
|
+
return useCallback(() => cleanupRef.current(), []);
|
|
2234
|
+
}
|
|
2235
|
+
//#endregion
|
|
2236
|
+
//#region src/hooks/useInterval.ts
|
|
2237
|
+
function useInterval(callback, delay) {
|
|
2238
|
+
const savedCallback = useRef(callback);
|
|
2239
|
+
useEffect(() => {
|
|
2240
|
+
savedCallback.current = callback;
|
|
2241
|
+
}, [callback]);
|
|
2242
|
+
useEffect(() => {
|
|
2243
|
+
if (delay === null) return;
|
|
2244
|
+
const id = setInterval(() => {
|
|
2245
|
+
savedCallback.current();
|
|
2246
|
+
}, delay);
|
|
2247
|
+
return () => {
|
|
2248
|
+
clearInterval(id);
|
|
2249
|
+
};
|
|
2250
|
+
}, [delay]);
|
|
2251
|
+
}
|
|
2252
|
+
//#endregion
|
|
2719
2253
|
//#region src/hooks/useMIDIAccess.ts
|
|
2720
2254
|
const INITIAL_STATE = {
|
|
2721
2255
|
midiAccess: null,
|
|
@@ -2852,6 +2386,6 @@ function useMIDIMessage(midiAccess, onMIDIMessage) {
|
|
|
2852
2386
|
});
|
|
2853
2387
|
}
|
|
2854
2388
|
//#endregion
|
|
2855
|
-
export { AnimationCanvas,
|
|
2389
|
+
export { AnimationCanvas, DropZone, FileInput, Knob, NumberInput, Piano, PointsEditor, Slider, XYPad, useAnimationFrame, useDrag, useDragValue, useDropZone, useEventListener, useFileInputContext, useInterval, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
|
|
2856
2390
|
|
|
2857
2391
|
//# sourceMappingURL=index.js.map
|