@tremolo-ui/react 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/index.cjs +252 -58
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +550 -133
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +550 -133
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +218 -39
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/src/components/DropZone/index.tsx +100 -0
  11. package/src/components/FileInput/Trigger.tsx +39 -0
  12. package/src/components/FileInput/context.tsx +24 -0
  13. package/src/components/FileInput/index.tsx +144 -0
  14. package/src/components/Knob/index.tsx +8 -5
  15. package/src/components/NumberInput/DecrementStepper.tsx +2 -13
  16. package/src/components/NumberInput/IncrementStepper.tsx +2 -14
  17. package/src/components/NumberInput/InputField.tsx +5 -11
  18. package/src/components/NumberInput/Stepper.tsx +4 -19
  19. package/src/components/NumberInput/StepperButton.tsx +13 -8
  20. package/src/components/NumberInput/context.tsx +6 -7
  21. package/src/components/NumberInput/index.tsx +4 -13
  22. package/src/components/Piano/index.tsx +10 -13
  23. package/src/components/PointsEditor/Background.tsx +2 -8
  24. package/src/components/PointsEditor/Container.tsx +2 -8
  25. package/src/components/PointsEditor/Point.tsx +7 -5
  26. package/src/components/PointsEditor/SelectionBox.tsx +2 -8
  27. package/src/components/PointsEditor/context.tsx +6 -6
  28. package/src/components/PointsEditor/index.tsx +9 -11
  29. package/src/components/Slider/Marks.tsx +6 -9
  30. package/src/components/Slider/MarksOption.tsx +7 -2
  31. package/src/components/Slider/Thumb.tsx +9 -15
  32. package/src/components/Slider/Track.tsx +6 -13
  33. package/src/components/Slider/index.tsx +5 -12
  34. package/src/components/Slider/type.ts +4 -2
  35. package/src/components/XYPad/Area.tsx +5 -10
  36. package/src/components/XYPad/Thumb.tsx +5 -11
  37. package/src/components/XYPad/index.tsx +3 -5
  38. package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
  39. package/src/components/_util/decimal-digits.ts +23 -0
  40. package/src/components/_util/visually-hidden.ts +25 -0
  41. package/src/css-variables.ts +23 -0
  42. package/src/hooks/_internal/useCheckSteps.ts +2 -3
  43. package/src/hooks/useDrag.ts +13 -4
  44. package/src/hooks/useDragValue.ts +17 -2
  45. package/src/hooks/useDropZone.ts +78 -0
  46. package/src/hooks/useWheel.ts +4 -1
  47. package/src/index.ts +12 -9
  48. package/src/input-event.ts +2 -2
package/dist/index.cjs CHANGED
@@ -92,6 +92,203 @@ const AnimationCanvas = /* @__PURE__ */ (0, react.forwardRef)(function Animation
92
92
  });
93
93
  });
94
94
  //#endregion
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);
108
+ }
109
+ //#endregion
110
+ //#region src/hooks/useDropZone.ts
111
+ /**
112
+ * Take files dropped onto an element.
113
+ *
114
+ * Any element can become a drop target this way — a waveform display, a track
115
+ * lane, a canvas — without wrapping it in anything.
116
+ *
117
+ * @returns a ref callback for the element, and whether a drag is over it
118
+ */
119
+ function useDropZone({ accept, multiple, disabled, onDrop, onReject } = {}) {
120
+ const dropHandler = useCallbackRef(onDrop);
121
+ const rejectHandler = useCallbackRef(onReject);
122
+ const [state, setState] = (0, react.useState)({
123
+ over: false,
124
+ invalid: false
125
+ });
126
+ const [node, setNode] = (0, react.useState)(null);
127
+ const instanceRef = (0, react.useRef)(null);
128
+ const latest = (0, react.useRef)({
129
+ accept,
130
+ multiple,
131
+ disabled
132
+ });
133
+ (0, react.useEffect)(() => {
134
+ if (!node) return;
135
+ const instance = (0, _tremolo_ui_dom.createDropZone)(node, {
136
+ ...latest.current,
137
+ onDrop: (files, event) => dropHandler(files, event),
138
+ onReject: (files, event) => rejectHandler(files, event),
139
+ onStateChange: (next) => setState(next)
140
+ });
141
+ instanceRef.current = instance;
142
+ return () => {
143
+ instanceRef.current = null;
144
+ instance.destroy();
145
+ setState({
146
+ over: false,
147
+ invalid: false
148
+ });
149
+ };
150
+ }, [
151
+ node,
152
+ dropHandler,
153
+ rejectHandler
154
+ ]);
155
+ (0, react.useEffect)(() => {
156
+ latest.current = {
157
+ accept,
158
+ multiple,
159
+ disabled
160
+ };
161
+ instanceRef.current?.update({
162
+ accept,
163
+ multiple,
164
+ disabled
165
+ });
166
+ });
167
+ return {
168
+ refCallback: setNode,
169
+ over: state.over,
170
+ invalid: state.invalid
171
+ };
172
+ }
173
+ /**
174
+ * An area that takes files dropped onto it.
175
+ */
176
+ const DropZone = { Root: /* @__PURE__ */ (0, react.forwardRef)(({ accept, multiple = false, disabled = false, onDrop, onReject, children, ...props }, ref) => {
177
+ const { refCallback, over, invalid } = useDropZone({
178
+ accept,
179
+ multiple,
180
+ disabled,
181
+ onDrop,
182
+ onReject
183
+ });
184
+ const composedRef = require_compose_refs.useComposedRefs(ref, refCallback);
185
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
186
+ ref: composedRef,
187
+ "data-dragover": over ? "" : void 0,
188
+ "data-invalid": invalid ? "" : void 0,
189
+ "data-disabled": disabled ? "" : void 0,
190
+ ...props,
191
+ children
192
+ });
193
+ }) };
194
+ //#endregion
195
+ //#region src/components/_util/visually-hidden.ts
196
+ /**
197
+ * Take an element out of sight while leaving it in the accessibility tree and
198
+ * in the tab order.
199
+ *
200
+ * `display: none` and `visibility: hidden` would remove it from both, and the
201
+ * native control underneath a headless component is what carries the ARIA and
202
+ * the keyboard behaviour. `pointer-events: none` is safe because nothing is
203
+ * ever clicked here directly: a `<label>` forwards its click, and a drag is
204
+ * handled by the part that is visible.
205
+ */
206
+ const visuallyHiddenStyle = {
207
+ position: "absolute",
208
+ width: 1,
209
+ height: 1,
210
+ padding: 0,
211
+ margin: -1,
212
+ overflow: "hidden",
213
+ clip: "rect(0, 0, 0, 0)",
214
+ clipPath: "inset(50%)",
215
+ whiteSpace: "nowrap",
216
+ border: 0,
217
+ pointerEvents: "none"
218
+ };
219
+ //#endregion
220
+ //#region src/components/FileInput/context.tsx
221
+ const FileInputContext = /* @__PURE__ */ (0, react.createContext)(null);
222
+ const FileInputProvider = FileInputContext.Provider;
223
+ function useFileInputContext(selector) {
224
+ const context = (0, react.useContext)(FileInputContext);
225
+ if (!context) throw new Error("Missing FileInputContext.Provider in the tree");
226
+ return selector ? selector(context) : context;
227
+ }
228
+ //#endregion
229
+ //#region src/components/FileInput/Trigger.tsx
230
+ /**
231
+ * Opens the file picker, and names the input for a screen reader.
232
+ *
233
+ * A `<label>` rather than a `<button>`: the browser forwards the click to the
234
+ * input on its own, the input stays the one thing in the tab order, and its
235
+ * accessible name comes from this text without an `aria-label` to keep in
236
+ * step. A disabled input ignores the click, so nothing has to be intercepted.
237
+ */
238
+ const Trigger = /* @__PURE__ */ (0, react.forwardRef)(({ children, ...props }, ref) => {
239
+ const { inputId, disabled } = useFileInputContext();
240
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
241
+ ref,
242
+ htmlFor: inputId,
243
+ "data-disabled": disabled ? "" : void 0,
244
+ ...props,
245
+ children
246
+ });
247
+ });
248
+ /**
249
+ * Picks files, and hands them over as `File`s.
250
+ */
251
+ const FileInput = {
252
+ Root: /* @__PURE__ */ (0, react.forwardRef)(({ accept, multiple = false, disabled = false, onChange, onReject, children, "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, ...props }, ref) => {
253
+ const inputId = (0, react.useId)();
254
+ const context = (0, react.useMemo)(() => ({
255
+ inputId,
256
+ disabled
257
+ }), [inputId, disabled]);
258
+ function handleChange(event) {
259
+ const picked = Array.from(event.target.files ?? []);
260
+ event.target.value = "";
261
+ const accepted = [];
262
+ const rejected = [];
263
+ for (const file of picked) if ((0, _tremolo_ui_dom.matchesAccept)(file, accept)) accepted.push(file);
264
+ else rejected.push(file);
265
+ if (rejected.length > 0) onReject?.(rejected);
266
+ if (accepted.length > 0) onChange?.(accepted);
267
+ }
268
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FileInputProvider, {
269
+ value: context,
270
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
271
+ ref,
272
+ "data-disabled": disabled ? "" : void 0,
273
+ ...props,
274
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
275
+ id: inputId,
276
+ type: "file",
277
+ accept,
278
+ multiple,
279
+ disabled,
280
+ style: visuallyHiddenStyle,
281
+ "aria-label": ariaLabel,
282
+ "aria-labelledby": ariaLabelledby,
283
+ "aria-describedby": ariaDescribedby,
284
+ onChange: handleChange
285
+ }), children]
286
+ })
287
+ });
288
+ }),
289
+ Trigger
290
+ };
291
+ //#endregion
95
292
  //#region src/hooks/_internal/useCheckSteps.ts
96
293
  /** Positions probed across the travel. The ends are left out so that the
97
294
  * clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
@@ -157,8 +354,8 @@ function probe(options, range, format) {
157
354
  };
158
355
  for (const { modifiers } of entries(options)) for (const position of PROBES) {
159
356
  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);
357
+ const up = (0, _tremolo_ui_dom.applyDelta)(value, 1, options, range, modifiers);
358
+ const down = (0, _tremolo_ui_dom.applyDelta)(value, -1, options, range, modifiers);
162
359
  if (up !== value || down !== value) outcome.moved = true;
163
360
  if (!format) continue;
164
361
  const shown = format(value);
@@ -209,21 +406,6 @@ function useCheckSteps({ component, axis, range, keyboard, wheel, format }) {
209
406
  ]);
210
407
  }
211
408
  //#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
409
  //#region src/hooks/useDragValue.ts
228
410
  /**
229
411
  * Drive a value with a pointer drag.
@@ -330,7 +512,7 @@ function useWheel(onWheel, { requireFocus = false, target } = {}) {
330
512
  * `useCheckSteps` warns about.
331
513
  *
332
514
  * 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`.
515
+ * amount move at all — see `applyDelta` in `@tremolo-ui/dom`.
334
516
  */
335
517
  const DEFAULT_KEYBOARD_OPTIONS = {
336
518
  default: ["raw", 1],
@@ -592,7 +774,7 @@ const Knob = {
592
774
  "ArrowDown"
593
775
  ].includes(key)) {
594
776
  event.preventDefault();
595
- onChange((0, _tremolo_ui_functions.applyDelta)(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
777
+ onChange((0, _tremolo_ui_dom.applyDelta)(value, key === "ArrowRight" || key === "ArrowUp" ? 1 : -1, keyboard, range, event));
596
778
  }
597
779
  }, [
598
780
  keyboard,
@@ -607,7 +789,7 @@ const Knob = {
607
789
  reverse: true
608
790
  }], [range]),
609
791
  getValue: () => [value, value],
610
- sensitivity: (state) => (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value,
792
+ sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
611
793
  threshold: 1,
612
794
  cursor: inactive ? void 0 : externalStyles.cursor,
613
795
  pointerLock: inactive ? false : pointerLock,
@@ -621,7 +803,7 @@ const Knob = {
621
803
  if (!wheel || inactive) return;
622
804
  event.preventDefault();
623
805
  if (!onChange || event.deltaY === 0) return;
624
- onChange((0, _tremolo_ui_functions.applyDelta)(value, -Math.sign(event.deltaY), wheel, range, event));
806
+ onChange((0, _tremolo_ui_dom.applyDelta)(value, -Math.sign(event.deltaY), wheel, range, event));
625
807
  }, WHEEL_OPTIONS$2);
626
808
  const rootRefCallback = require_compose_refs.useComposedRefs(elmRef, dragRefCallback, wheelRefCallback);
627
809
  const context = (0, react.useMemo)(() => {
@@ -1016,7 +1198,7 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
1016
1198
  * out of the pipeline for a modifier entry — naming one is a request to move
1017
1199
  * off the grid.
1018
1200
  */
1019
- const dragOptions = (0, react.useMemo)(() => (0, _tremolo_ui_functions.mapModifier)(dragSensitivity, (factor) => ["raw", step * factor]), [dragSensitivity, step]);
1201
+ const dragOptions = (0, react.useMemo)(() => (0, _tremolo_ui_dom.mapModifier)(dragSensitivity, (factor) => ["raw", step * factor]), [dragSensitivity, step]);
1020
1202
  /**
1021
1203
  * Set once the drag has actually moved the value, so that the press-and-hold
1022
1204
  * repeat of a stepper stands down and leaves the value to the drag. A ref,
@@ -1043,7 +1225,7 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
1043
1225
  onDragStart: (state) => {
1044
1226
  originRef.current = null;
1045
1227
  draggingRef.current = false;
1046
- factorRef.current = (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value;
1228
+ factorRef.current = (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value;
1047
1229
  },
1048
1230
  onDrag: (_x, y, _dx, _dy, state) => {
1049
1231
  if (inactive || drag === null) return;
@@ -1055,7 +1237,7 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
1055
1237
  previousYRef.current = y;
1056
1238
  return;
1057
1239
  }
1058
- const factor = (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value;
1240
+ const factor = (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value;
1059
1241
  if (factor !== factorRef.current) {
1060
1242
  originRef.current = {
1061
1243
  y: previousYRef.current,
@@ -1067,7 +1249,7 @@ const Stepper = /* @__PURE__ */ (0, react.forwardRef)(function Stepper({ classNa
1067
1249
  const steps = Math.round(-(y - originRef.current.y) / drag);
1068
1250
  if (steps === 0) return;
1069
1251
  draggingRef.current = true;
1070
- changeValue((0, _tremolo_ui_functions.applyDelta)(originRef.current.value, steps, dragOptions, rawRange, state.event));
1252
+ changeValue((0, _tremolo_ui_dom.applyDelta)(originRef.current.value, steps, dragOptions, rawRange, state.event));
1071
1253
  },
1072
1254
  onDragEnd: () => {
1073
1255
  draggingRef.current = false;
@@ -1197,8 +1379,8 @@ const NumberInput = {
1197
1379
  changeValue
1198
1380
  ]);
1199
1381
  const nudge = (0, react.useCallback)((direction, option, modifiers) => {
1200
- const [mode] = (0, _tremolo_ui_functions.selectInputEvent)(option, modifiers).option;
1201
- changeValue((0, _tremolo_ui_functions.applyDelta)(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
1382
+ const [mode] = (0, _tremolo_ui_dom.selectModifier)(option, modifiers).value;
1383
+ changeValue((0, _tremolo_ui_dom.applyDelta)(value, direction, option, mode === "raw" ? rawRange : range, modifiers));
1202
1384
  }, [
1203
1385
  changeValue,
1204
1386
  value,
@@ -1368,7 +1550,7 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1368
1550
  const [activeNotes, setActiveNotes] = (0, react.useState)([]);
1369
1551
  /** Set while `resizable` is on, measured from the parent. */
1370
1552
  const [resizedKeyWidth, setResizedKeyWidth] = (0, react.useState)(whiteKeyWidth);
1371
- const notes = (0, react.useMemo)(() => (0, _tremolo_ui_functions.getNoteRangeArray)(noteRange), [noteRange]);
1553
+ const notes = (0, react.useMemo)(() => (0, _tremolo_ui_dom.getNoteRangeArray)(noteRange), [noteRange]);
1372
1554
  const whiteKeyCount = (0, react.useMemo)(() => notes.filter(_tremolo_ui_functions.isWhiteKey).length, [notes]);
1373
1555
  const layout = (0, react.useMemo)(() => ({
1374
1556
  noteRange,
@@ -1513,7 +1695,7 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1513
1695
  role: "group",
1514
1696
  tabIndex: 0,
1515
1697
  style: {
1516
- width: resizable ? "100%" : (0, _tremolo_ui_functions.pianoWidth)(layout),
1698
+ width: resizable ? "100%" : (0, _tremolo_ui_dom.pianoWidth)(layout),
1517
1699
  "--height": cssLength(height),
1518
1700
  position: "relative",
1519
1701
  ...style
@@ -1540,8 +1722,8 @@ const Piano = { Root: /* @__PURE__ */ (0, react.forwardRef)(function Root({ note
1540
1722
  position: "absolute",
1541
1723
  zIndex: keyType === "white" ? 1 : 2,
1542
1724
  ...keyStyle,
1543
- left: (0, _tremolo_ui_functions.notePosition)(note, layout),
1544
- width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_functions.blackKeyWidth)(layout),
1725
+ left: (0, _tremolo_ui_dom.notePosition)(note, layout),
1726
+ width: keyType === "white" ? layout.whiteKeyWidth : (0, _tremolo_ui_dom.blackKeyWidth)(layout),
1545
1727
  height: keyType === "white" ? "100%" : `${blackKeyHeightRatio * 100}%`
1546
1728
  },
1547
1729
  children: content !== "" && content !== null && content !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -1581,24 +1763,11 @@ function usePointsEditorContext(selector) {
1581
1763
  }
1582
1764
  //#endregion
1583
1765
  //#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
1766
  const VisuallyHiddenRangeInput = /* @__PURE__ */ (0, react.forwardRef)((props, ref) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
1598
1767
  ...props,
1599
1768
  ref,
1600
1769
  type: "range",
1601
- style: hiddenStyle
1770
+ style: visuallyHiddenStyle
1602
1771
  }));
1603
1772
  //#endregion
1604
1773
  //#region src/components/PointsEditor/Point.tsx
@@ -1664,7 +1833,7 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
1664
1833
  const { refCallback: dragRefCallback, dragging } = useDragValue({
1665
1834
  axis: AXIS,
1666
1835
  baseElementRef: containerRef,
1667
- sensitivity: (state) => (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value,
1836
+ sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
1668
1837
  cursor: inactive ? void 0 : externalStyles.cursor,
1669
1838
  shouldStart: () => !disabled,
1670
1839
  onChange: ([x, y]) => {
@@ -1692,7 +1861,7 @@ function Point({ value, children, id: idProp, min, max, size, width, height, col
1692
1861
  }
1693
1862
  });
1694
1863
  const nudge = (0, react.useCallback)((axis, direction, option, modifiers) => {
1695
- const next = (0, _tremolo_ui_functions.applyDelta)(value[axis], direction, option, AXIS, modifiers);
1864
+ const next = (0, _tremolo_ui_dom.applyDelta)(value[axis], direction, option, AXIS, modifiers);
1696
1865
  nudgeSelection(id, {
1697
1866
  x: axis === "x" ? next - value.x : 0,
1698
1867
  y: axis === "y" ? next - value.y : 0
@@ -1981,7 +2150,7 @@ const PointsEditor = {
1981
2150
  const { element, wheel, readonly, onChange, value } = entry.current;
1982
2151
  if (!element?.contains(active)) continue;
1983
2152
  if (!wheel || readonly || !onChange) return false;
1984
- const next = (0, _tremolo_ui_functions.applyDelta)(value[axis], direction, wheel, AXIS, modifiers);
2153
+ const next = (0, _tremolo_ui_dom.applyDelta)(value[axis], direction, wheel, AXIS, modifiers);
1985
2154
  nudgeSelection(id, {
1986
2155
  x: axis === "x" ? next - value.x : 0,
1987
2156
  y: axis === "y" ? next - value.y : 0
@@ -2106,7 +2275,7 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, labelW
2106
2275
  const reverse = useSliderContext((s) => s.reverse);
2107
2276
  const calcPercent = (0, react.useCallback)((value) => {
2108
2277
  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);
2278
+ return (0, _tremolo_ui_functions.toFixed)(vertical !== reverse ? 100 - percent : percent);
2110
2279
  }, [
2111
2280
  vertical,
2112
2281
  reverse,
@@ -2142,6 +2311,27 @@ function MarksOption({ value, mark = true, label, thickness, length, gap, labelW
2142
2311
  });
2143
2312
  }
2144
2313
  //#endregion
2314
+ //#region src/components/_util/decimal-digits.ts
2315
+ /**
2316
+ * How many digits a number carries after the decimal point.
2317
+ *
2318
+ * `Slider.Marks` builds its labels by multiplying an interval, which leaves
2319
+ * binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
2320
+ * count is what tells `toFixed` how far to round that back.
2321
+ *
2322
+ * The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
2323
+ * which has no decimal point at all, so reading the text after the point
2324
+ * reports no digits and the interval rounds away to whole numbers.
2325
+ */
2326
+ function decimalDigits(x) {
2327
+ if (!Number.isFinite(x)) return 0;
2328
+ const text = String(x);
2329
+ const e = text.indexOf("e");
2330
+ if (e === -1) return text.split(".")[1]?.length ?? 0;
2331
+ const fraction = text.slice(0, e).split(".")[1]?.length ?? 0;
2332
+ return Math.max(0, fraction - Number(text.slice(e + 1)));
2333
+ }
2334
+ //#endregion
2145
2335
  //#region src/components/Slider/type.ts
2146
2336
  function generateOptionsList(options, min, max, step) {
2147
2337
  const { per, mark = true, label = true } = typeof options === "object" ? options : { per: options };
@@ -2149,7 +2339,7 @@ function generateOptionsList(options, min, max, step) {
2149
2339
  const interval = per === "step" ? step : per;
2150
2340
  const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1;
2151
2341
  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);
2342
+ const value = (0, _tremolo_ui_functions.toFixed)(interval * (Math.ceil(min / interval) + i), decimalDigits(interval));
2153
2343
  optionsList.push({
2154
2344
  value,
2155
2345
  mark,
@@ -2167,7 +2357,7 @@ function Marks({ gap, options, children, className, style, ...props }) {
2167
2357
  const vertical = useSliderContext((s) => s.vertical);
2168
2358
  const reverse = useSliderContext((s) => s.reverse);
2169
2359
  const optionsList = options ? generateOptionsList(options, min, max, step) : [];
2170
- if ((0, _tremolo_ui_functions.xor)(vertical, reverse)) optionsList.reverse();
2360
+ if (vertical !== reverse) optionsList.reverse();
2171
2361
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2172
2362
  className,
2173
2363
  "data-orientation": vertical ? "vertical" : "horizontal",
@@ -2252,7 +2442,7 @@ const Track = /* @__PURE__ */ (0, react.forwardRef)(function Track({ length, thi
2252
2442
  className,
2253
2443
  "data-disabled": disabled ? "" : void 0,
2254
2444
  "data-orientation": vertical ? "vertical" : "horizontal",
2255
- "data-flipped": (0, _tremolo_ui_functions.xor)(vertical, reverse) ? "" : void 0,
2445
+ "data-flipped": vertical !== reverse ? "" : void 0,
2256
2446
  style: {
2257
2447
  "--active": active,
2258
2448
  "--inactive": inactive,
@@ -2291,7 +2481,7 @@ const Slider = {
2291
2481
  const inactive = disabled || readonly;
2292
2482
  const p = (0, _tremolo_ui_functions.toFixed)(scale.normalize(value, min, max) * 100);
2293
2483
  const rev = (0, _tremolo_ui_functions.toFixed)(100 - p);
2294
- const displayReversed = (0, react.useMemo)(() => (0, _tremolo_ui_functions.xor)(vertical, reverse), [vertical, reverse]);
2484
+ const displayReversed = vertical !== reverse;
2295
2485
  const percent = displayReversed ? rev : p;
2296
2486
  const axis = (0, react.useMemo)(() => ({
2297
2487
  min,
@@ -2324,7 +2514,7 @@ const Slider = {
2324
2514
  if (!keyboard || !onChange || inactive) return;
2325
2515
  let direction = key === "ArrowRight" || key === "ArrowUp" ? 1 : -1;
2326
2516
  if (reverse) direction *= -1;
2327
- onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, keyboard, axis, event));
2517
+ onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, keyboard, axis, event));
2328
2518
  }, [
2329
2519
  keyboard,
2330
2520
  onChange,
@@ -2339,7 +2529,7 @@ const Slider = {
2339
2529
  baseElementRef: trackRef,
2340
2530
  cursor: inactive ? void 0 : externalStyles.cursor,
2341
2531
  shouldStart: () => !inactive,
2342
- sensitivity: (state) => (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value,
2532
+ sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
2343
2533
  updateOnPointerDown: true,
2344
2534
  onChange: (v) => {
2345
2535
  if (inactive) return;
@@ -2365,7 +2555,7 @@ const Slider = {
2365
2555
  direction = event.deltaY > 0 ? -1 : 1;
2366
2556
  }
2367
2557
  if (reverse) direction *= -1;
2368
- onChange((0, _tremolo_ui_functions.applyDelta)(value, direction, wheel, axis, event));
2558
+ onChange((0, _tremolo_ui_dom.applyDelta)(value, direction, wheel, axis, event));
2369
2559
  }, WHEEL_OPTIONS$1);
2370
2560
  const rootRefCallback = require_compose_refs.useComposedRefs(dragRefCallback, wheelRefCallback);
2371
2561
  const context = (0, react.useMemo)(() => ({
@@ -2597,7 +2787,7 @@ const XYPad = {
2597
2787
  });
2598
2788
  const withAxis = (0, react.useCallback)((axis, next) => axis === 0 ? [next, value[1]] : [value[0], next], [value]);
2599
2789
  /** @param i 0 = x, 1 = y */
2600
- const nudge = (0, react.useCallback)((i, direction, option, modifiers) => withAxis(i, (0, _tremolo_ui_functions.applyDelta)(value[i], direction, option, axis[i], modifiers)), [
2790
+ const nudge = (0, react.useCallback)((i, direction, option, modifiers) => withAxis(i, (0, _tremolo_ui_dom.applyDelta)(value[i], direction, option, axis[i], modifiers)), [
2601
2791
  value,
2602
2792
  axis,
2603
2793
  withAxis
@@ -2627,7 +2817,7 @@ const XYPad = {
2627
2817
  const { refCallback: dragRefCallback } = useDragValue({
2628
2818
  axis,
2629
2819
  baseElementRef: areaRef,
2630
- sensitivity: (state) => (0, _tremolo_ui_functions.selectModifier)(dragSensitivity, state.event).value,
2820
+ sensitivity: (state) => (0, _tremolo_ui_dom.selectModifier)(dragSensitivity, state.event).value,
2631
2821
  updateOnPointerDown: true,
2632
2822
  cursor: inactive ? void 0 : externalStyles.cursor,
2633
2823
  shouldStart: () => !inactive,
@@ -2880,6 +3070,8 @@ exports.AnimationCanvas = AnimationCanvas;
2880
3070
  exports.DEFAULT_DRAG_SENSITIVITY = DEFAULT_DRAG_SENSITIVITY;
2881
3071
  exports.DEFAULT_KEYBOARD_OPTIONS = DEFAULT_KEYBOARD_OPTIONS;
2882
3072
  exports.DEFAULT_WHEEL_OPTIONS = DEFAULT_WHEEL_OPTIONS;
3073
+ exports.DropZone = DropZone;
3074
+ exports.FileInput = FileInput;
2883
3075
  exports.Knob = Knob;
2884
3076
  Object.defineProperty(exports, "NOT_SUPPORTED", {
2885
3077
  enumerable: true,
@@ -2910,7 +3102,9 @@ exports.clampPoint = clampPoint;
2910
3102
  exports.useAnimationFrame = useAnimationFrame;
2911
3103
  exports.useDrag = useDrag;
2912
3104
  exports.useDragValue = useDragValue;
3105
+ exports.useDropZone = useDropZone;
2913
3106
  exports.useEventListener = useEventListener;
3107
+ exports.useFileInputContext = useFileInputContext;
2914
3108
  exports.useInterval = useInterval;
2915
3109
  exports.useKnobContext = useKnobContext;
2916
3110
  exports.useLongPress = useLongPress;