@tremolo-ui/react 0.1.5 → 0.2.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 (40) hide show
  1. package/dist/index.cjs +325 -239
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +149 -64
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +229 -199
  6. package/dist/index.d.cts.map +1 -1
  7. package/dist/index.d.ts +229 -199
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +314 -223
  10. package/dist/index.js.map +1 -1
  11. package/package.json +2 -2
  12. package/src/components/Knob/index.css +50 -25
  13. package/src/components/Knob/index.tsx +37 -32
  14. package/src/components/NumberInput/index.css +31 -15
  15. package/src/components/NumberInput/index.tsx +22 -14
  16. package/src/components/Piano/context.tsx +1 -1
  17. package/src/components/Piano/index.css +17 -8
  18. package/src/components/Piano/index.tsx +259 -220
  19. package/src/components/Piano/key.tsx +3 -3
  20. package/src/components/Slider/Thumb.tsx +1 -1
  21. package/src/components/Slider/Track.tsx +3 -3
  22. package/src/components/Slider/index.css +33 -8
  23. package/src/components/Slider/index.tsx +25 -25
  24. package/src/components/XYPad/Area.tsx +4 -3
  25. package/src/components/XYPad/Thumb.tsx +8 -4
  26. package/src/components/XYPad/index.css +18 -4
  27. package/src/components/XYPad/index.tsx +25 -19
  28. package/src/hooks/useAnimationFrame.ts +3 -0
  29. package/src/hooks/useCallbackRef.ts +2 -2
  30. package/src/hooks/useDrag.ts +2 -1
  31. package/src/hooks/useDragWithElement.ts +2 -1
  32. package/src/hooks/useEventListener.ts +3 -0
  33. package/src/hooks/useInterval.ts +3 -0
  34. package/src/hooks/useLongPress.ts +3 -0
  35. package/src/hooks/useMIDIAccess.ts +40 -0
  36. package/src/hooks/useMIDIInput.ts +50 -0
  37. package/src/hooks/useMIDIMessage.ts +24 -0
  38. package/src/hooks/useRefCallbackEvent.ts +4 -0
  39. package/src/index.ts +30 -35
  40. package/src/styles/global.css +0 -4
package/dist/index.cjs CHANGED
@@ -21,11 +21,13 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
21
21
  }) : target, mod));
22
22
 
23
23
  //#endregion
24
- const clsx = __toESM(require("clsx"));
25
- const react = __toESM(require("react"));
26
- const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
27
- const __tremolo_ui_functions = __toESM(require("@tremolo-ui/functions"));
28
- const zustand = __toESM(require("zustand"));
24
+ let clsx = require("clsx");
25
+ clsx = __toESM(clsx);
26
+ let react = require("react");
27
+ react = __toESM(react);
28
+ let react_jsx_runtime = require("react/jsx-runtime");
29
+ let __tremolo_ui_functions = require("@tremolo-ui/functions");
30
+ let zustand = require("zustand");
29
31
 
30
32
  //#region src/components/AnimationCanvas/canvas.ts
31
33
  const drawingState = [
@@ -167,8 +169,8 @@ function AnimationCanvas({ draw, init, animate = true, options, width: _width =
167
169
  //#endregion
168
170
  //#region src/hooks/useCallbackRef.ts
169
171
  /**
170
- * This hook is user-land implementation of the experimental `useEffectEvent` hook.
171
- * React docs: https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
172
+ * Internal
173
+ * @private
172
174
  */
173
175
  function useCallbackRef(callback, deps = []) {
174
176
  const callbackRef = (0, react.useRef)(() => {
@@ -199,8 +201,7 @@ function useEventListener(target, event, handler, options) {
199
201
  handler
200
202
  ]);
201
203
  return () => {
202
- const node = typeof target === "function" ? target() : target ?? document;
203
- node?.removeEventListener(event, listener, options);
204
+ (typeof target === "function" ? target() : target ?? document)?.removeEventListener(event, listener, options);
204
205
  };
205
206
  }
206
207
 
@@ -225,6 +226,7 @@ function useRefCallbackEvent(event, handler, options, deps = []) {
225
226
  //#endregion
226
227
  //#region src/hooks/useDrag.ts
227
228
  /**
229
+ * @category hooks
228
230
  * @returns [refCallback, pointerDownHandler]
229
231
  */
230
232
  function useDrag({ threshold = 1, onDrag, onDragStart, onDragEnd }) {
@@ -291,9 +293,7 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
291
293
  const isRelativeSize = typeof (size ?? width) == "string" || typeof height == "string";
292
294
  const onDrag = (0, react.useCallback)((_x, y) => {
293
295
  if (!onChange) return;
294
- const v = (0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew);
295
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
296
- onChange(v2);
296
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)((0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew), step), min, max));
297
297
  }, [
298
298
  max,
299
299
  min,
@@ -313,10 +313,8 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
313
313
  event.preventDefault();
314
314
  const x = key == "ArrowRight" || key == "ArrowUp" ? keyboard[1] : -keyboard[1];
315
315
  let v;
316
- if (keyboard[0] == "normalized") {
317
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
318
- v = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
319
- } else v = value + x;
316
+ if (keyboard[0] == "normalized") v = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
317
+ else v = value + x;
320
318
  onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max));
321
319
  }
322
320
  }, [
@@ -343,10 +341,8 @@ const AnimationKnob = (0, react.forwardRef)(({ value, min, max, defaultValue = m
343
341
  event.preventDefault();
344
342
  const x = event.deltaY > 0 ? -wheel[1] : wheel[1];
345
343
  let v;
346
- if (wheel[0] == "normalized") {
347
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
348
- v = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
349
- } else v = value + x;
344
+ if (wheel[0] == "normalized") v = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
345
+ else v = value + x;
350
346
  if (onChange) onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max));
351
347
  }, { passive: false }, [
352
348
  wheel,
@@ -458,20 +454,12 @@ function DragObserver(props) {
458
454
  *
459
455
  * @category Knob
460
456
  */
461
- const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size = 50, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, activeLine, inactiveLine, thumb, thumbLine, onChange, onKeyDown, onPointerDown, onDoubleClick, className, classes,...props }, ref) => {
457
+ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defaultValue = min, startValue = min, size = 50, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], enableDoubleClickDefault = true, disabled = false, readonly = false, activeLine, inactiveLine, thumb, thumbLine, thumbSize = 84, thumbLineWeight = 6, thumbLineLength = 35, lineWeight = 6, angleRange = 270, onChange, onKeyDown, onPointerDown, onDoubleClick, className, classes,...props }, forwardedRef) => {
462
458
  const valueRef = (0, react.useRef)(0);
463
459
  const elmRef = (0, react.useRef)(null);
464
- const padding = 8;
465
- const thumbLineWeight = 6;
466
- const thumbLineLength = 35;
467
- const lineWeight = size * .06;
468
- const p = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
469
- const s = (0, __tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
470
460
  const onDrag = (0, react.useCallback)((_x, y) => {
471
461
  if (!onChange || readonly) return;
472
- const v = (0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew);
473
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
474
- onChange(v2);
462
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)((0, __tremolo_ui_functions.rawValue)(valueRef.current - y / 100, min, max, skew), step), min, max));
475
463
  }, [
476
464
  max,
477
465
  min,
@@ -482,10 +470,8 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
482
470
  ]);
483
471
  const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
484
472
  let newValue;
485
- if (eventType == "normalized") {
486
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
487
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
488
- } else newValue = value + x;
473
+ if (eventType == "normalized") newValue = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
474
+ else newValue = value + x;
489
475
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
490
476
  }, [
491
477
  max,
@@ -535,7 +521,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
535
521
  readonly,
536
522
  updateValueByEvent
537
523
  ]);
538
- (0, react.useImperativeHandle)(ref, () => {
524
+ (0, react.useImperativeHandle)(forwardedRef, () => {
539
525
  return {
540
526
  focus() {
541
527
  elmRef.current?.focus();
@@ -545,11 +531,15 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
545
531
  }
546
532
  };
547
533
  }, []);
548
- const center = size / 2;
549
- const r1 = -135;
550
- const r2 = -135 + Math.min(p, s) * 270;
551
- const r3 = -135 + Math.max(p, s) * 270;
552
- const r4 = 135;
534
+ const viewBoxSize = 100;
535
+ const center = viewBoxSize / 2;
536
+ const padding = (viewBoxSize - (0, __tremolo_ui_functions.clamp)(thumbSize, 0, 100)) / 2;
537
+ const p = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
538
+ const s = (0, __tremolo_ui_functions.normalizeValue)(startValue, min, max, skew);
539
+ const r1 = -angleRange / 2;
540
+ const r2 = r1 + Math.min(p, s) * angleRange;
541
+ const r3 = r1 + Math.max(p, s) * angleRange;
542
+ const r4 = angleRange / 2;
553
543
  const x1 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r1 - 90));
554
544
  const y1 = center + center * Math.sin((0, __tremolo_ui_functions.radian)(r1 - 90));
555
545
  const x2 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r2 - 90));
@@ -559,12 +549,13 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
559
549
  const x4 = center + center * Math.cos((0, __tremolo_ui_functions.radian)(r4 - 90));
560
550
  const y4 = center + center * Math.sin((0, __tremolo_ui_functions.radian)(r4 - 90));
561
551
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
562
- className: (0, clsx.default)("tremolo-knob", className),
563
552
  ref: (div) => {
564
553
  elmRef.current = div;
565
554
  wheelRefCallback(div);
566
555
  touchMoveRefCallback(div);
567
556
  },
557
+ className: (0, clsx.default)("tremolo-knob", className),
558
+ viewBox: `0 0 ${viewBoxSize} ${viewBoxSize}`,
568
559
  width: size,
569
560
  height: size,
570
561
  tabIndex: 0,
@@ -595,13 +586,6 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
595
586
  stroke: inactiveLine || "currentColor",
596
587
  strokeWidth: lineWeight
597
588
  }),
598
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
599
- className: (0, clsx.default)("tremolo-knob-active-line", classes?.activeLine),
600
- d: `M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`,
601
- fill: "none",
602
- stroke: activeLine || "currentColor",
603
- strokeWidth: lineWeight
604
- }),
605
589
  startValue < max && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
606
590
  className: (0, clsx.default)("tremolo-knob-inactive-line", classes?.inactiveLine),
607
591
  d: `M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`,
@@ -609,12 +593,19 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
609
593
  stroke: inactiveLine || "currentColor",
610
594
  strokeWidth: lineWeight
611
595
  }),
596
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
597
+ className: (0, clsx.default)("tremolo-knob-active-line", classes?.activeLine),
598
+ d: `M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`,
599
+ fill: "none",
600
+ stroke: activeLine || "currentColor",
601
+ strokeWidth: lineWeight
602
+ }),
612
603
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
613
604
  className: (0, clsx.default)("tremolo-knob-thumb", classes?.thumb),
614
605
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
615
606
  cx: "50%",
616
607
  cy: "50%",
617
- r: `${50 - padding}%`,
608
+ r: `${thumbSize / 2}%`,
618
609
  fill: thumb || "currentColor"
619
610
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("line", {
620
611
  className: (0, clsx.default)("tremolo-knob-thumb-line", classes?.thumbLine),
@@ -625,7 +616,7 @@ const Knob = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, defau
625
616
  stroke: thumbLine || "currentColor",
626
617
  strokeWidth: `${thumbLineWeight}%`,
627
618
  style: {
628
- transform: `rotate(${-135 + p * 270}deg)`,
619
+ transform: `rotate(${r1 + p * angleRange}deg)`,
629
620
  transformOrigin: "50% 50%"
630
621
  }
631
622
  })]
@@ -698,8 +689,7 @@ const createNumberInputStore = (initProps) => {
698
689
  valueAsNumber: parseValue(String(initProps?.value || ""), initProps?.units, initProps?.digit).rawValue,
699
690
  increment: () => set((state) => {
700
691
  if (state.readonly) return {};
701
- const current = parseValue(state.value, state.units, state.digit).rawValue;
702
- let next = current + (state.step ?? 1);
692
+ let next = parseValue(state.value, state.units, state.digit).rawValue + (state.step ?? 1);
703
693
  if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
704
694
  const v = parseValue(String(next), state.units, state.digit).formatValue;
705
695
  state?.onChange?.(next, v);
@@ -710,8 +700,7 @@ const createNumberInputStore = (initProps) => {
710
700
  }),
711
701
  decrement: () => set((state) => {
712
702
  if (state.readonly) return {};
713
- const current = parseValue(state.value, state.units, state.digit).rawValue;
714
- let next = current - (state.step ?? 1);
703
+ let next = parseValue(state.value, state.units, state.digit).rawValue - (state.step ?? 1);
715
704
  if (state.keepWithinRange) next = safeClamp(next, state.min, state.max);
716
705
  const v = parseValue(String(next), state.units, state.digit).formatValue;
717
706
  state?.onChange?.(next, v);
@@ -756,6 +745,111 @@ function useNumberInputContext(selector) {
756
745
  return (0, zustand.useStore)(store, selector);
757
746
  }
758
747
 
748
+ //#endregion
749
+ //#region src/hooks/useInterval.ts
750
+ /**
751
+ * @category hooks
752
+ */
753
+ function useInterval(callback, delay) {
754
+ const savedCallback = (0, react.useRef)(callback);
755
+ (0, react.useEffect)(() => {
756
+ savedCallback.current = callback;
757
+ }, [callback]);
758
+ (0, react.useEffect)(() => {
759
+ if (delay === null) return;
760
+ const id = setInterval(() => {
761
+ savedCallback.current();
762
+ }, delay);
763
+ return () => {
764
+ clearInterval(id);
765
+ };
766
+ }, [delay]);
767
+ }
768
+
769
+ //#endregion
770
+ //#region src/hooks/useLongPress.ts
771
+ /**
772
+ * @category hooks
773
+ */
774
+ function useLongPress(callback, initialDelay = 500, interval = 40) {
775
+ const [pressed, setPressed] = (0, react.useState)(false);
776
+ const [delay, setDelay] = (0, react.useState)(initialDelay);
777
+ useInterval(() => {
778
+ callback();
779
+ setDelay(interval);
780
+ }, pressed ? delay : null);
781
+ useEventListener(globalThis.window, "pointerup", () => {
782
+ setPressed(false);
783
+ setDelay(initialDelay);
784
+ });
785
+ return (0, react.useCallback)(() => {
786
+ callback();
787
+ setPressed(true);
788
+ }, [callback]);
789
+ }
790
+
791
+ //#endregion
792
+ //#region src/components/NumberInput/DecrementStepper.tsx
793
+ /** @category NumberInput */
794
+ function DecrementStepper({ size = 12, children, className,...props }) {
795
+ const min = useNumberInputContext((s) => s.min);
796
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
797
+ const readonly = useNumberInputContext((s) => s.readonly);
798
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
799
+ const press = useLongPress(useNumberInputContext((s) => s.decrement));
800
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
801
+ className: (0, clsx.default)("tremolo-number-input-decrement-stepper", className),
802
+ role: "button",
803
+ tabIndex: -1,
804
+ "aria-disabled": keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER),
805
+ "aria-readonly": readonly,
806
+ onPointerDown: press,
807
+ ...props,
808
+ children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
809
+ width: size,
810
+ height: size,
811
+ viewBox: "0 0 24 24",
812
+ fill: "none",
813
+ stroke: "currentColor",
814
+ strokeWidth: "2",
815
+ strokeLinecap: "round",
816
+ strokeLinejoin: "round",
817
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "6 9 12 15 18 9" })
818
+ })
819
+ });
820
+ }
821
+
822
+ //#endregion
823
+ //#region src/components/NumberInput/IncrementStepper.tsx
824
+ /** @category NumberInput */
825
+ function IncrementStepper({ size = 12, children, className,...props }) {
826
+ const max = useNumberInputContext((s) => s.max);
827
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
828
+ const readonly = useNumberInputContext((s) => s.readonly);
829
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
830
+ const press = useLongPress(useNumberInputContext((s) => s.increment));
831
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
832
+ className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
833
+ role: "button",
834
+ tabIndex: -1,
835
+ "aria-disabled": keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER),
836
+ "aria-readonly": readonly,
837
+ onPointerDown: press,
838
+ ...props,
839
+ children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
840
+ width: size,
841
+ height: size,
842
+ viewBox: "0 0 24 24",
843
+ fill: "none",
844
+ stroke: "currentColor",
845
+ strokeWidth: "2",
846
+ strokeLinecap: "round",
847
+ strokeLinejoin: "round",
848
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "18 15 12 9 6 15" })
849
+ })
850
+ });
851
+ }
852
+
759
853
  //#endregion
760
854
  //#region src/components/NumberInput/InternalInput.tsx
761
855
  const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFocus, blurOnEnter, clampValueOnBlur, wheel, keyboard, className, onChange, onFocus, onBlur, onKeyDown, children: _children,...props }, ref) => {
@@ -783,8 +877,7 @@ const InternalInput = (0, react.forwardRef)(({ disabled, readonly, selectWithFoc
783
877
  let newValue;
784
878
  if (eventType == "normalized") {
785
879
  if (!min || !max) throw new Error("required parameter: min, max");
786
- const n = (0, __tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max);
787
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max);
880
+ newValue = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(valueAsNumber, min, max) + x, min, max);
788
881
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
789
882
  } else {
790
883
  newValue = valueAsNumber + x;
@@ -898,114 +991,9 @@ function Stepper({ dynamic = true, children, className,...props }) {
898
991
  });
899
992
  }
900
993
 
901
- //#endregion
902
- //#region src/hooks/useInterval.ts
903
- function useInterval(callback, delay) {
904
- const savedCallback = (0, react.useRef)(callback);
905
- (0, react.useEffect)(() => {
906
- savedCallback.current = callback;
907
- }, [callback]);
908
- (0, react.useEffect)(() => {
909
- if (delay === null) return;
910
- const id = setInterval(() => {
911
- savedCallback.current();
912
- }, delay);
913
- return () => {
914
- clearInterval(id);
915
- };
916
- }, [delay]);
917
- }
918
-
919
- //#endregion
920
- //#region src/hooks/useLongPress.ts
921
- function useLongPress(callback, initialDelay = 500, interval = 40) {
922
- const [pressed, setPressed] = (0, react.useState)(false);
923
- const [delay, setDelay] = (0, react.useState)(initialDelay);
924
- useInterval(() => {
925
- callback();
926
- setDelay(interval);
927
- }, pressed ? delay : null);
928
- useEventListener(globalThis.window, "pointerup", () => {
929
- setPressed(false);
930
- setDelay(initialDelay);
931
- });
932
- return (0, react.useCallback)(() => {
933
- callback();
934
- setPressed(true);
935
- }, [callback]);
936
- }
937
-
938
- //#endregion
939
- //#region src/components/NumberInput/IncrementStepper.tsx
940
- /** @category NumberInput */
941
- function IncrementStepper({ size = 12, children, className,...props }) {
942
- const max = useNumberInputContext((s) => s.max);
943
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
944
- const readonly = useNumberInputContext((s) => s.readonly);
945
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
946
- const increment = useNumberInputContext((s) => s.increment);
947
- const press = useLongPress(increment);
948
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
949
- className: (0, clsx.default)("tremolo-number-input-increment-stepper", className),
950
- role: "button",
951
- tabIndex: -1,
952
- "aria-disabled": keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER),
953
- "aria-readonly": readonly,
954
- onPointerDown: press,
955
- ...props,
956
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
957
- width: size,
958
- height: size,
959
- viewBox: "0 0 24 24",
960
- fill: "none",
961
- stroke: "currentColor",
962
- strokeWidth: "2",
963
- strokeLinecap: "round",
964
- strokeLinejoin: "round",
965
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "18 15 12 9 6 15" })
966
- })
967
- });
968
- }
969
-
970
- //#endregion
971
- //#region src/components/NumberInput/DecrementStepper.tsx
972
- /** @category NumberInput */
973
- function DecrementStepper({ size = 12, children, className,...props }) {
974
- const min = useNumberInputContext((s) => s.min);
975
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber);
976
- const readonly = useNumberInputContext((s) => s.readonly);
977
- const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange);
978
- const decrement = useNumberInputContext((s) => s.decrement);
979
- const press = useLongPress(decrement);
980
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
981
- className: (0, clsx.default)("tremolo-number-input-decrement-stepper", className),
982
- role: "button",
983
- tabIndex: -1,
984
- "aria-disabled": keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER),
985
- "aria-readonly": readonly,
986
- onPointerDown: press,
987
- ...props,
988
- children: children || /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
989
- width: size,
990
- height: size,
991
- viewBox: "0 0 24 24",
992
- fill: "none",
993
- stroke: "currentColor",
994
- strokeWidth: "2",
995
- strokeLinecap: "round",
996
- strokeLinejoin: "round",
997
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("polyline", { points: "6 9 12 15 18 9" })
998
- })
999
- });
1000
- }
1001
-
1002
994
  //#endregion
1003
995
  //#region src/components/NumberInput/index.tsx
1004
- /**
1005
- * Input with some useful functions for entering numerical values.
1006
- * @category NumberInput
1007
- */
1008
- const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, readonly = false, disabled = false, digit, variant = "outline", selectWithFocus = "none", blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ["raw", 1], keyboard = ["raw", 1], activeColor, wrapperClassName, className, onChange, onFocus, onBlur, children,...props }, ref) => {
996
+ const NumberInputImpl = (0, react.forwardRef)(({ value, min, max, step = 1, units, readonly = false, disabled = false, digit, variant = "outline", selectWithFocus = "none", blurOnEnter = true, keepWithinRange = true, clampValueOnBlur = true, wheel = ["raw", 1], keyboard = ["raw", 1], activeColor, wrapperClassName, className, onChange, onFocus, onBlur, children,...props }, forwardedRef) => {
1009
997
  const colors = { "--active-color": activeColor };
1010
998
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberInputProvider, {
1011
999
  value: String(value),
@@ -1024,7 +1012,7 @@ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, r
1024
1012
  "data-stepper": !!children,
1025
1013
  "data-variant": variant,
1026
1014
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(InternalInput, {
1027
- ref,
1015
+ ref: forwardedRef,
1028
1016
  readonly,
1029
1017
  disabled,
1030
1018
  selectWithFocus,
@@ -1041,10 +1029,20 @@ const NumberInput = (0, react.forwardRef)(({ value, min, max, step = 1, units, r
1041
1029
  })
1042
1030
  });
1043
1031
  });
1032
+ /**
1033
+ * Input with some useful functions for entering numerical values.
1034
+ * @category NumberInput
1035
+ */
1036
+ const NumberInput = Object.assign(NumberInputImpl, {
1037
+ Stepper,
1038
+ IncrementStepper,
1039
+ DecrementStepper
1040
+ });
1044
1041
 
1045
1042
  //#endregion
1046
1043
  //#region src/hooks/useDragWithElement.ts
1047
1044
  /**
1045
+ * @category hooks
1048
1046
  * @returns [refCallback, pointerDownHandler]
1049
1047
  */
1050
1048
  function useDragWithElement({ baseElementRef, onDrag, onDragStart, onDragEnd }) {
@@ -1124,9 +1122,7 @@ function usePianoContext(selector) {
1124
1122
  //#region src/components/Piano/KeyLabel.tsx
1125
1123
  /** @category Piano */
1126
1124
  function KeyLabel({ label, className, wrapperClassName, wrapperStyle, __note, __label,...props }) {
1127
- const noteRange = usePianoContext((s) => s.noteRange);
1128
- const noteRangeArray = getNoteRangeArray(noteRange);
1129
- const index = noteRangeArray.indexOf(__note);
1125
+ const index = getNoteRangeArray(usePianoContext((s) => s.noteRange)).indexOf(__note);
1130
1126
  const content = label ? label(__note, index) : __label && __label(__note, index);
1131
1127
  return (content != void 0 || content != null) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1132
1128
  className: (0, clsx.default)("tremolo-piano-key-label-wrapper", wrapperClassName),
@@ -1152,8 +1148,7 @@ const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg,
1152
1148
  const onPlayNote = usePianoContext((s) => s.onPlayNote);
1153
1149
  const onStopNote = usePianoContext((s) => s.onStopNote);
1154
1150
  const label = usePianoContext((s) => s.label);
1155
- const notePosition = usePianoContext((s) => s.notePosition);
1156
- const position = notePosition(noteNumber);
1151
+ const position = usePianoContext((s) => s.notePosition)(noteNumber);
1157
1152
  const disabled = noteNumber > midiMax;
1158
1153
  const colors = {
1159
1154
  "--bg": bg,
@@ -1163,10 +1158,10 @@ const KeyImpl = (0, react.forwardRef)(({ keyType, noteNumber, width, height, bg,
1163
1158
  };
1164
1159
  (0, react.useImperativeHandle)(ref, () => {
1165
1160
  return {
1166
- play() {
1161
+ play(velocity) {
1167
1162
  if (disabled) return;
1168
1163
  setPlayed(true);
1169
- if (onPlayNote) onPlayNote(noteNumber);
1164
+ if (onPlayNote) onPlayNote(noteNumber, velocity);
1170
1165
  },
1171
1166
  stop() {
1172
1167
  setPlayed(false);
@@ -1257,21 +1252,14 @@ const SHORTCUTS = { HOME_ROW: { keys: [
1257
1252
  //#endregion
1258
1253
  //#region src/components/Piano/index.tsx
1259
1254
  /**
1260
- * Piano component
1261
- *
1262
- * TODO:
1263
- * - add scale highlight
1255
+ * [noteRange.first, noteRange.first + 1 ..., noteRange.last]
1256
+ * @category Piano
1264
1257
  */
1265
- /** @category Piano */
1266
1258
  function getNoteRangeArray(noteRange) {
1267
1259
  return Array.from({ length: noteRange.last - noteRange.first + 1 }, (_, i) => i + noteRange.first);
1268
1260
  }
1269
1261
  const blackPerWhiteWidth = defaultBlackKeyWidth / defaultWhiteKeyWidth;
1270
- /**
1271
- * Customizable piano component.
1272
- * @category Piano
1273
- */
1274
- function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? "100%" : 160, whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth, style, className, onPlayNote, onStopNote, label, children, onPointerDown,...props }) {
1262
+ const PianoImpl = (0, react.forwardRef)(({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts, fill = false, height = fill ? "100%" : 160, whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth, style, className, onPlayNote, onStopNote, label, children, onPointerDown,...props }, forwardedRef) => {
1275
1263
  const [whiteNoteWidth, setWhiteNoteWidth] = (0, react.useState)(_whiteNoteWidth);
1276
1264
  const keyRefs = (0, react.useRef)([]);
1277
1265
  for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) keyRefs.current[i] = (0, react.createRef)();
@@ -1342,9 +1330,7 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1342
1330
  ]);
1343
1331
  const onDrag = (0, react.useCallback)((perX, perY) => {
1344
1332
  if (!pianoRef.current) return;
1345
- const x = perX * staticWidth;
1346
- const y = perY * pianoRef.current.clientHeight;
1347
- const note = getHitKeyIndex(x, y);
1333
+ const note = getHitKeyIndex(perX * staticWidth, perY * pianoRef.current.clientHeight);
1348
1334
  const index = noteRangeArray.indexOf(note);
1349
1335
  if (index == -1) return;
1350
1336
  if (hitKeyIndex.current != index) {
@@ -1398,6 +1384,25 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1398
1384
  const index = keyboardShortcuts.keys.indexOf(e.key);
1399
1385
  if (index != -1) keyRefs.current[index]?.current?.stop();
1400
1386
  });
1387
+ (0, react.useImperativeHandle)(forwardedRef, () => {
1388
+ return {
1389
+ playNote(note, velocity) {
1390
+ const index = noteRangeArray.indexOf(note);
1391
+ if (index != -1) {
1392
+ if (!keyRefs.current[index]?.current?.played()) keyRefs.current[index]?.current?.play(velocity);
1393
+ } else onPlayNote?.(note, velocity);
1394
+ },
1395
+ stopNote(note) {
1396
+ const index = noteRangeArray.indexOf(note);
1397
+ if (index != -1) keyRefs.current[index]?.current?.stop();
1398
+ else onStopNote?.(note);
1399
+ }
1400
+ };
1401
+ }, [
1402
+ noteRangeArray,
1403
+ onPlayNote,
1404
+ onStopNote
1405
+ ]);
1401
1406
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PianoProvider, {
1402
1407
  notePosition,
1403
1408
  noteRange,
@@ -1434,7 +1439,16 @@ function Piano({ noteRange, glissando = true, midiMax = 127, keyboardShortcuts,
1434
1439
  }, note))
1435
1440
  })
1436
1441
  });
1437
- }
1442
+ });
1443
+ /**
1444
+ * Customizable piano component.
1445
+ * @category Piano
1446
+ */
1447
+ const Piano = Object.assign(PianoImpl, {
1448
+ WhiteKey,
1449
+ BlackKey,
1450
+ KeyLabel
1451
+ });
1438
1452
 
1439
1453
  //#endregion
1440
1454
  //#region src/components/Slider/context.tsx
@@ -1571,7 +1585,7 @@ function Scale({ gap = 6, options, children, className, style,...props }) {
1571
1585
  /** @category Slider */
1572
1586
  const defaultThumbSize = 22;
1573
1587
  /** @category Slider */
1574
- const SliderThumb = (0, react.forwardRef)(({ size, width = defaultThumbSize, height = defaultThumbSize, color, children, className, style, __percent }, ref) => {
1588
+ const Thumb$1 = (0, react.forwardRef)(({ size, width = defaultThumbSize, height = defaultThumbSize, color, children, className, style, __percent }, ref) => {
1575
1589
  const wrapperRef = (0, react.useRef)(null);
1576
1590
  const vertical = useSliderContext((s) => s.vertical);
1577
1591
  const disabled = useSliderContext((s) => s.disabled);
@@ -1613,7 +1627,7 @@ const SliderThumb = (0, react.forwardRef)(({ size, width = defaultThumbSize, hei
1613
1627
  const defaultLength = 140;
1614
1628
  const defaultThickness = 10;
1615
1629
  /** @category Slider */
1616
- function SliderTrack({ length = defaultLength, thickness = defaultThickness, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0,...props }) {
1630
+ function Track({ length = defaultLength, thickness = defaultThickness, active, inactive, children, className, style, defaultStyle = true, __thumb, __percent = 0,...props }) {
1617
1631
  const vertical = useSliderContext((s) => s.vertical);
1618
1632
  const reverse = useSliderContext((s) => s.reverse);
1619
1633
  const disabled = useSliderContext((s) => s.disabled);
@@ -1628,7 +1642,7 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1628
1642
  "data-vertical": vertical,
1629
1643
  style: !defaultStyle ? style : {
1630
1644
  ...colors,
1631
- background: (0, __tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive, #eee) ${__percent}%, var(--active, #7998ec) ${__percent}%)` : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`,
1645
+ background: (0, __tremolo_ui_functions.xor)(vertical, reverse) ? `linear-gradient(to ${direction}, var(--inactive) ${__percent}%, var(--active) ${__percent}%)` : `linear-gradient(to ${direction}, var(--active) ${__percent}%, var(--inactive) ${__percent}%)`,
1632
1646
  borderRadius: (0, __tremolo_ui_functions.styleHelper)(thickness, "/", 2),
1633
1647
  width: !vertical ? length : thickness,
1634
1648
  height: vertical ? length : thickness,
@@ -1641,11 +1655,7 @@ function SliderTrack({ length = defaultLength, thickness = defaultThickness, act
1641
1655
 
1642
1656
  //#endregion
1643
1657
  //#region src/components/Slider/index.tsx
1644
- /**
1645
- * Customizable slider
1646
- * @category Slider
1647
- */
1648
- const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, vertical = false, reverse = false, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown,...props }, ref) => {
1658
+ const SliderImpl = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, vertical = false, reverse = false, bodyNoSelect = true, wheel = ["raw", 1], keyboard = ["raw", 1], disabled = false, readonly = false, onChange, onDragStart, onDragEnd, className, style, children, onFocus, onBlur, onPointerDown, onKeyDown,...props }, forwardedRef) => {
1649
1659
  const trackElementRef = (0, react.useRef)(null);
1650
1660
  const thumbRef = (0, react.useRef)(null);
1651
1661
  const p = (0, __tremolo_ui_functions.toFixed)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) * 100);
@@ -1656,8 +1666,8 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1656
1666
  let thumbProps = {};
1657
1667
  let scaleComponent = void 0;
1658
1668
  if (children != void 0) react.default.Children.forEach(children, (child) => {
1659
- if (react.default.isValidElement(child)) if (child.type == SliderThumb) thumbProps = child.props;
1660
- else if (child.type == SliderTrack) trackProps = child.props;
1669
+ if (react.default.isValidElement(child)) if (child.type == Thumb$1) thumbProps = child.props;
1670
+ else if (child.type == Track) trackProps = child.props;
1661
1671
  else if (child.type == Scale) scaleComponent = child;
1662
1672
  else throw new Error("only <SliderThumb> or <SliderTrack>");
1663
1673
  else throw new Error("children is an invalid element.");
@@ -1665,9 +1675,7 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1665
1675
  const onDrag = (0, react.useCallback)((nx, ny) => {
1666
1676
  if (!onChange || readonly) return;
1667
1677
  const n = vertical ? ny : nx;
1668
- const v = (0, __tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew);
1669
- const v2 = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, step), min, max);
1670
- onChange(v2);
1678
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)((0, __tremolo_ui_functions.rawValue)(displayReversed ? 1 - n : n, min, max, skew), step), min, max));
1671
1679
  }, [
1672
1680
  displayReversed,
1673
1681
  max,
@@ -1680,10 +1688,8 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1680
1688
  ]);
1681
1689
  const updateValueByEvent = (0, react.useCallback)((eventType, x) => {
1682
1690
  let newValue;
1683
- if (eventType == "normalized") {
1684
- const n = (0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew);
1685
- newValue = (0, __tremolo_ui_functions.rawValue)(n + x, min, max, skew);
1686
- } else newValue = value + x;
1691
+ if (eventType == "normalized") newValue = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(value, min, max, skew) + x, min, max, skew);
1692
+ else newValue = value + x;
1687
1693
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(newValue, step), min, max);
1688
1694
  }, [
1689
1695
  max,
@@ -1746,7 +1752,7 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1746
1752
  onChange,
1747
1753
  updateValueByEvent
1748
1754
  ]);
1749
- (0, react.useImperativeHandle)(ref, () => {
1755
+ (0, react.useImperativeHandle)(forwardedRef, () => {
1750
1756
  return {
1751
1757
  focus() {
1752
1758
  thumbRef.current?.focus();
@@ -1810,9 +1816,9 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1810
1816
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1811
1817
  className: "tremolo-slider-track-wrapper",
1812
1818
  ref: trackElementRef,
1813
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderTrack, {
1819
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Track, {
1814
1820
  __percent: percent,
1815
- __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SliderThumb, {
1821
+ __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb$1, {
1816
1822
  ref: thumbRef,
1817
1823
  __percent: percent,
1818
1824
  ...thumbProps
@@ -1824,17 +1830,26 @@ const Slider = (0, react.forwardRef)(({ value, min, max, step = 1, skew = 1, ver
1824
1830
  })
1825
1831
  });
1826
1832
  });
1833
+ /**
1834
+ * Customizable slider
1835
+ * @category Slider
1836
+ */
1837
+ const Slider = Object.assign(SliderImpl, {
1838
+ Thumb: Thumb$1,
1839
+ Track,
1840
+ Scale,
1841
+ ScaleOption
1842
+ });
1827
1843
 
1828
1844
  //#endregion
1829
1845
  //#region src/components/WheelObserver/index.tsx
1830
1846
  /** @category WheelObserver */
1831
1847
  function WheelObserver(props) {
1832
1848
  const { children, onWheel, as: Component = "div",...attributes } = props;
1833
- const wheelRefCallback = useRefCallbackEvent("wheel", (event) => {
1834
- if (onWheel) onWheel(event);
1835
- }, { passive: false }, [onWheel]);
1836
1849
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Component, {
1837
- ref: wheelRefCallback,
1850
+ ref: useRefCallbackEvent("wheel", (event) => {
1851
+ if (onWheel) onWheel(event);
1852
+ }, { passive: false }, [onWheel]),
1838
1853
  ...attributes,
1839
1854
  children
1840
1855
  });
@@ -1843,7 +1858,7 @@ function WheelObserver(props) {
1843
1858
  //#endregion
1844
1859
  //#region src/components/XYPad/Area.tsx
1845
1860
  /** @category XYPad */
1846
- function XYPadArea({ width = 120, height = 120, color, children, className, style, __thumb,...props }) {
1861
+ function Area({ width = 120, height = 120, color, children, className, style, __thumb,...props }) {
1847
1862
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1848
1863
  className: (0, clsx.default)("tremolo-xy-pad-area", className),
1849
1864
  style: {
@@ -1860,7 +1875,7 @@ function XYPadArea({ width = 120, height = 120, color, children, className, styl
1860
1875
  //#endregion
1861
1876
  //#region src/components/XYPad/Thumb.tsx
1862
1877
  /** @category XYPad */
1863
- const XYPadThumb = (0, react.forwardRef)(({ size, width = 22, height = 22, color, children, wrapperClassName, wrapperStyle, className, style, __disabled, __css,...props }, ref) => {
1878
+ const Thumb = (0, react.forwardRef)(({ size, width = 22, height = 22, color, children, wrapperClassName, wrapperStyle, className, style, __disabled, __readonly, __css,...props }, ref) => {
1864
1879
  const wrapperRef = (0, react.useRef)(null);
1865
1880
  (0, react.useImperativeHandle)(ref, () => {
1866
1881
  return {
@@ -1884,6 +1899,7 @@ const XYPadThumb = (0, react.forwardRef)(({ size, width = 22, height = 22, color
1884
1899
  className: (0, clsx.default)("tremolo-xy-pad-thumb", className),
1885
1900
  tabIndex: 0,
1886
1901
  "aria-disabled": __disabled,
1902
+ "aria-readonly": __readonly,
1887
1903
  style: {
1888
1904
  "--color": color,
1889
1905
  width: size ?? width,
@@ -1903,11 +1919,7 @@ const defaultValueOptions = {
1903
1919
  wheel: ["raw", 1],
1904
1920
  keyboard: ["raw", 1]
1905
1921
  };
1906
- /**
1907
- * Simple XYPad
1908
- * @category XYPad
1909
- */
1910
- const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children,...props }, ref) => {
1922
+ const XYPadImpl = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSelect = true, disabled = false, readonly = false, onChange, onDragStart, onDragEnd, onPointerDown, onKeyDown, onFocus, onBlur, children,...props }, forwardedRef) => {
1911
1923
  const x = (0, react.useMemo)(() => {
1912
1924
  return {
1913
1925
  ...defaultValueOptions,
@@ -1929,8 +1941,8 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1929
1941
  let areaProps = {};
1930
1942
  let thumbProps = {};
1931
1943
  if (children != void 0) react.default.Children.forEach(children, (child) => {
1932
- if (react.default.isValidElement(child)) if (child.type == XYPadThumb) thumbProps = child.props;
1933
- else if (child.type == XYPadArea) areaProps = child.props;
1944
+ if (react.default.isValidElement(child)) if (child.type == Thumb) thumbProps = child.props;
1945
+ else if (child.type == Area) areaProps = child.props;
1934
1946
  else throw new Error("only <XYPadThumb> or <XYPadArea>");
1935
1947
  else throw new Error("children is an invalid element.");
1936
1948
  });
@@ -1938,9 +1950,7 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1938
1950
  if (!onChange || readonly) return;
1939
1951
  const vx = (0, __tremolo_ui_functions.rawValue)(x.reverse ? 1 - nx$1 : nx$1, x.min, x.max, x.skew);
1940
1952
  const vy = (0, __tremolo_ui_functions.rawValue)(y.reverse ? 1 - ny$1 : ny$1, y.min, y.max, y.skew);
1941
- const newX = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max);
1942
- const newY = (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max);
1943
- onChange(newX, newY);
1953
+ onChange((0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vx, x.step), x.min, x.max), (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(vy, y.step), y.min, y.max));
1944
1954
  }, [
1945
1955
  onChange,
1946
1956
  readonly,
@@ -1957,10 +1967,8 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
1957
1967
  ]);
1958
1968
  const updateValueByEvent = (0, react.useCallback)((eventType, target, x$1) => {
1959
1969
  let v;
1960
- if (eventType == "normalized") {
1961
- const n = (0, __tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew);
1962
- v = (0, __tremolo_ui_functions.rawValue)(n + x$1, target.min, target.max, target.skew);
1963
- } else v = target.value + x$1;
1970
+ if (eventType == "normalized") v = (0, __tremolo_ui_functions.rawValue)((0, __tremolo_ui_functions.normalizeValue)(target.value, target.min, target.max, target.skew) + x$1, target.min, target.max, target.skew);
1971
+ else v = target.value + x$1;
1964
1972
  return (0, __tremolo_ui_functions.clamp)((0, __tremolo_ui_functions.stepValue)(v, target.step), target.min, target.max);
1965
1973
  }, []);
1966
1974
  const handleKeyDown = (0, react.useCallback)((event) => {
@@ -2027,7 +2035,7 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
2027
2035
  readonly,
2028
2036
  updateValueByEvent
2029
2037
  ]);
2030
- (0, react.useImperativeHandle)(ref, () => {
2038
+ (0, react.useImperativeHandle)(forwardedRef, () => {
2031
2039
  return {
2032
2040
  focus() {
2033
2041
  thumbRef.current?.focus();
@@ -2072,10 +2080,11 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
2072
2080
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2073
2081
  className: "tremolo-xy-pad-area-wrapper",
2074
2082
  ref: areaElementRef,
2075
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadArea, {
2076
- __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(XYPadThumb, {
2083
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Area, {
2084
+ __thumb: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Thumb, {
2077
2085
  ref: thumbRef,
2078
2086
  __disabled: disabled,
2087
+ __readonly: readonly,
2079
2088
  __css: {
2080
2089
  top: `${percentY}%`,
2081
2090
  left: `${percentX}%`
@@ -2087,9 +2096,20 @@ const XYPad = (0, react.forwardRef)(({ x: _x, y: _y, className, style, bodyNoSel
2087
2096
  })
2088
2097
  });
2089
2098
  });
2099
+ /**
2100
+ * Simple XYPad
2101
+ * @category XYPad
2102
+ */
2103
+ const XYPad = Object.assign(XYPadImpl, {
2104
+ Thumb,
2105
+ Area
2106
+ });
2090
2107
 
2091
2108
  //#endregion
2092
2109
  //#region src/hooks/useAnimationFrame.ts
2110
+ /**
2111
+ * @category hooks
2112
+ */
2093
2113
  function useAnimationFrame(callback = () => {}, deps = []) {
2094
2114
  const reqIdRef = (0, react.useRef)(-1);
2095
2115
  const loop = (0, react.useCallback)(() => {
@@ -2102,29 +2122,92 @@ function useAnimationFrame(callback = () => {}, deps = []) {
2102
2122
  }, [loop]);
2103
2123
  }
2104
2124
 
2125
+ //#endregion
2126
+ //#region src/hooks/useMIDIAccess.ts
2127
+ /** @private */
2128
+ const PERMISSION_DENIED = "PERMISSION_DENIED";
2129
+ /** @private */
2130
+ const NOT_SUPPORTED = "NOT_SUPPORTED";
2131
+ /**
2132
+ * Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
2133
+ * @category hooks
2134
+ */
2135
+ function useMIDIAccess(requestOnMount = true) {
2136
+ const [midiAccess, setMidiAccess] = (0, react.useState)(null);
2137
+ const [error, setError] = (0, react.useState)(null);
2138
+ const request = (0, react.useCallback)(() => {
2139
+ if (navigator.requestMIDIAccess) navigator.requestMIDIAccess().then((access) => {
2140
+ setMidiAccess(access);
2141
+ setError(null);
2142
+ }).catch(() => {
2143
+ setError(PERMISSION_DENIED);
2144
+ });
2145
+ else setError(NOT_SUPPORTED);
2146
+ }, []);
2147
+ (0, react.useEffect)(() => {
2148
+ if (requestOnMount) request();
2149
+ }, [requestOnMount, request]);
2150
+ return {
2151
+ request,
2152
+ midiAccess,
2153
+ error
2154
+ };
2155
+ }
2156
+
2157
+ //#endregion
2158
+ //#region src/hooks/useMIDIMessage.ts
2159
+ /**
2160
+ * Hooks for when you want to process MIDI events in more detail than useMIDIInput.
2161
+ * @category hooks
2162
+ */
2163
+ function useMIDIMessage(midiAccess, onMIDIMessage) {
2164
+ (0, react.useEffect)(() => {
2165
+ if (!midiAccess) return;
2166
+ for (const input of midiAccess.inputs.values()) input.addEventListener("midimessage", onMIDIMessage);
2167
+ return () => {
2168
+ for (const input of midiAccess.inputs.values()) input.removeEventListener("midimessage", onMIDIMessage);
2169
+ };
2170
+ }, [midiAccess, onMIDIMessage]);
2171
+ }
2172
+
2173
+ //#endregion
2174
+ //#region src/hooks/useMIDIInput.ts
2175
+ const MIDI_EVENT_TO_NUMBER = {
2176
+ NOTE_ON: 144,
2177
+ NOTE_OFF: 128,
2178
+ PITCH_BEND: 224
2179
+ };
2180
+ /**
2181
+ * Hooks for handling note on/off events. To be used with useMIDIAccess. Internally uses useMIDIMessage.
2182
+ * @category hooks
2183
+ */
2184
+ function useMIDIInput(midiAccess, onNoteOnEvent, onNoteOffEvent, onPitchBendEvent) {
2185
+ useMIDIMessage(midiAccess, (0, react.useCallback)((event) => {
2186
+ if (!event.data) return;
2187
+ const kind = event.data[0] & 240;
2188
+ if (kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF || kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0) onNoteOffEvent?.(event.data[1]);
2189
+ else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) onNoteOnEvent?.(event.data[1], event.data[2]);
2190
+ else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) onPitchBendEvent?.(event.data[1], event.data[2]);
2191
+ }, [
2192
+ onNoteOffEvent,
2193
+ onNoteOnEvent,
2194
+ onPitchBendEvent
2195
+ ]));
2196
+ }
2197
+
2105
2198
  //#endregion
2106
2199
  exports.AnimationCanvas = AnimationCanvas;
2107
2200
  exports.AnimationKnob = AnimationKnob;
2108
- exports.BlackKey = BlackKey;
2109
- exports.DecrementStepper = DecrementStepper;
2110
2201
  exports.DragObserver = DragObserver;
2111
- exports.IncrementStepper = IncrementStepper;
2112
- exports.KeyLabel = KeyLabel;
2113
2202
  exports.Knob = Knob;
2203
+ exports.NOT_SUPPORTED = NOT_SUPPORTED;
2114
2204
  exports.NumberInput = NumberInput;
2205
+ exports.PERMISSION_DENIED = PERMISSION_DENIED;
2115
2206
  exports.Piano = Piano;
2116
2207
  exports.SHORTCUTS = SHORTCUTS;
2117
- exports.Scale = Scale;
2118
- exports.ScaleOption = ScaleOption;
2119
2208
  exports.Slider = Slider;
2120
- exports.SliderThumb = SliderThumb;
2121
- exports.SliderTrack = SliderTrack;
2122
- exports.Stepper = Stepper;
2123
2209
  exports.WheelObserver = WheelObserver;
2124
- exports.WhiteKey = WhiteKey;
2125
2210
  exports.XYPad = XYPad;
2126
- exports.XYPadArea = XYPadArea;
2127
- exports.XYPadThumb = XYPadThumb;
2128
2211
  exports.getNoteRangeArray = getNoteRangeArray;
2129
2212
  exports.useAnimationFrame = useAnimationFrame;
2130
2213
  exports.useDrag = useDrag;
@@ -2132,5 +2215,8 @@ exports.useDragWithElement = useDragWithElement;
2132
2215
  exports.useEventListener = useEventListener;
2133
2216
  exports.useInterval = useInterval;
2134
2217
  exports.useLongPress = useLongPress;
2218
+ exports.useMIDIAccess = useMIDIAccess;
2219
+ exports.useMIDIInput = useMIDIInput;
2220
+ exports.useMIDIMessage = useMIDIMessage;
2135
2221
  exports.useSliderContext = useSliderContext;
2136
2222
  //# sourceMappingURL=index.cjs.map