@tremolo-ui/vue 0.7.0 → 0.9.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 (47) hide show
  1. package/dist/index.cjs +865 -611
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +934 -169
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +934 -169
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +339 -128
  8. package/dist/index.js.map +1 -1
  9. package/package.json +8 -8
  10. package/src/components/AnimationCanvas/AnimationCanvas.ts +27 -30
  11. package/src/components/AnimationCanvas/index.ts +1 -5
  12. package/src/components/DropZone/DropZone.ts +19 -9
  13. package/src/components/DropZone/index.ts +1 -1
  14. package/src/components/FileInput/FileInput.ts +28 -14
  15. package/src/components/FileInput/index.ts +1 -1
  16. package/src/components/Knob/Knob.ts +107 -57
  17. package/src/components/Knob/KnobThumb.ts +32 -22
  18. package/src/components/Knob/index.ts +2 -2
  19. package/src/components/NumberInput/NumberInput.ts +136 -53
  20. package/src/components/NumberInput/NumberInputField.ts +5 -2
  21. package/src/components/NumberInput/NumberInputStepper.ts +25 -4
  22. package/src/components/NumberInput/context.ts +12 -0
  23. package/src/components/NumberInput/index.ts +1 -1
  24. package/src/components/NumberInput/stepper-button.ts +8 -1
  25. package/src/components/Piano/Piano.ts +51 -43
  26. package/src/components/Piano/index.ts +6 -1
  27. package/src/components/PointsEditor/PointsEditor.ts +43 -35
  28. package/src/components/PointsEditor/PointsEditorContainer.ts +2 -6
  29. package/src/components/PointsEditor/PointsEditorPoint.ts +82 -42
  30. package/src/components/PointsEditor/context.ts +1 -1
  31. package/src/components/PointsEditor/index.ts +5 -2
  32. package/src/components/Slider/Slider.ts +92 -49
  33. package/src/components/Slider/SliderMarks.ts +28 -14
  34. package/src/components/Slider/SliderMarksOption.ts +54 -33
  35. package/src/components/Slider/SliderThumb.ts +26 -8
  36. package/src/components/Slider/SliderTrack.ts +31 -16
  37. package/src/components/Slider/context.ts +1 -1
  38. package/src/components/Slider/index.ts +8 -5
  39. package/src/components/XYPad/XYPad.ts +94 -56
  40. package/src/components/XYPad/XYPadThumb.ts +21 -14
  41. package/src/components/XYPad/index.ts +2 -2
  42. package/src/components/_util/change-gesture.ts +44 -0
  43. package/src/components/_util/useCheckSteps.ts +1 -1
  44. package/src/components/_util/visually-hidden-range-input.ts +1 -1
  45. package/src/composables/useInstance.ts +1 -1
  46. package/src/composables/useWheel.ts +5 -2
  47. package/src/index.ts +21 -4
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { computed, defineComponent, h, inject, mergeProps, nextTick, onBeforeUnmount, onMounted, onScopeDispose, provide, ref, shallowRef, toValue, useId, watch, watchEffect } from "vue";
2
- 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, replaceOptions, selectModifier, sliderMarks, toXY, valuePercent, visuallyHiddenStyle, wheelDirection, wheelMove } from "@tremolo-ui/dom";
2
+ import { DEFAULT_DRAG_SENSITIVITY, DEFAULT_KEYBOARD_OPTIONS, DEFAULT_WHEEL_OPTIONS, KNOB_VIEWBOX_SIZE, createAnimationCanvas, createChangeGesture, createDrag, createDragValue, createDropZone, createLongPress, createMIDIAccess, createMIDIInput, createMIDIMessage, createPianoInput, createPointsEditor, createWheel, elementMapping, knobAngles, knobArcPath, knobArcRadius, notePosition, relativeMapping, valuePercent } from "@tremolo-ui/dom";
3
+ import { POINTS_EDITOR_DEFAULT_KEYBOARD, POINTS_EDITOR_DEFAULT_WHEEL, POINT_AXIS, applyDelta, arrowKeyDirection, arrowKeyMove, blackKeyWidth, caretAtDecimalOffset, caretDecimalOffset, checkSteps, clampPoint, commitNumberInputText, createStepperDrag, cssLength, fitWhiteKeyWidth, getNoteRangeArray, nudgeNumberInput, numberInputBounds, numberInputRanges, numberSpan, parseNumberText, partitionByAccept, pianoWidth, replaceOptions, selectModifier, sliderMarks, toXY, visuallyHiddenStyle, wheelDirection, wheelMove } from "@tremolo-ui/dom/internal";
3
4
  import { clamp, isWhiteKey, linearScale, noteKey } from "@tremolo-ui/functions";
4
- //#region src/components/AnimationCanvas/AnimationCanvas.ts
5
5
  /** A `<canvas>` drawn by `draw` on every animation frame. */
6
6
  const AnimationCanvas = /* @__PURE__ */ defineComponent({
7
7
  name: "AnimationCanvas",
@@ -12,7 +12,7 @@ const AnimationCanvas = /* @__PURE__ */ defineComponent({
12
12
  type: Function,
13
13
  required: true
14
14
  },
15
- /** Sets the canvas up before the first frame and after a resize. */
15
+ /** Sets the canvas up, once, before the first frame. */
16
16
  init: Function,
17
17
  /** Draw on every animation frame. @default true */
18
18
  animate: {
@@ -135,8 +135,6 @@ function useDropZone(target, options = {}) {
135
135
  useInstance(target, withState, createDropZone, (instance, next) => instance.update(next));
136
136
  return state;
137
137
  }
138
- //#endregion
139
- //#region src/components/DropZone/DropZone.ts
140
138
  /** A region that takes dropped files. */
141
139
  const DropZone = /* @__PURE__ */ defineComponent({
142
140
  name: "DropZone",
@@ -186,8 +184,6 @@ const FileInputKey = Symbol("FileInput");
186
184
  function useFileInputContext() {
187
185
  return injectContext(FileInputKey, "FileInput");
188
186
  }
189
- //#endregion
190
- //#region src/components/FileInput/FileInput.ts
191
187
  /**
192
188
  * Picks files. The control is the native file input, out of sight but in the
193
189
  * tab order, with `FileInputTrigger` as its label.
@@ -291,7 +287,37 @@ function useDragValue(target, options) {
291
287
  * `@tremolo-ui/dom`.
292
288
  */
293
289
  function useWheel(target, onWheel, options = {}) {
294
- useInstance(target, options, (element, opts) => createWheel(element, (event) => onWheel(event), opts), (instance, next) => instance.update(next));
290
+ useInstance(target, options, (element, opts) => createWheel(element, {
291
+ ...opts,
292
+ onWheel: (event) => onWheel(event)
293
+ }), (instance, next) => instance.update(next));
294
+ }
295
+ //#endregion
296
+ //#region src/components/_util/change-gesture.ts
297
+ /**
298
+ * The core's change gesture, reporting the control's value: the start gets
299
+ * the value before the change, the end the last one the control emitted.
300
+ * `modelValue` lags behind until the parent renders, and a drag ends in the
301
+ * same event as its last change, so the component reports each value it
302
+ * emits through `changed`.
303
+ */
304
+ function useChangeGesture(options) {
305
+ let last = options.value();
306
+ const gesture = createChangeGesture({
307
+ onStart: (source) => {
308
+ last = options.value();
309
+ options.onStart(last, source);
310
+ },
311
+ onEnd: (source) => options.onEnd(last, source)
312
+ });
313
+ watchEffect(() => gesture.update({ endDelay: options.endDelay() }));
314
+ watch(options.inactive, (inactive) => {
315
+ if (inactive) gesture.end();
316
+ });
317
+ onScopeDispose(() => gesture.end());
318
+ return Object.assign(gesture, { changed: (value) => {
319
+ last = value;
320
+ } });
295
321
  }
296
322
  //#endregion
297
323
  //#region src/components/_util/useCheckSteps.ts
@@ -309,8 +335,6 @@ function useCheckSteps(options) {
309
335
  });
310
336
  } catch {}
311
337
  }
312
- //#endregion
313
- //#region src/components/Knob/Knob.ts
314
338
  /**
315
339
  * Interactive rotary knob, drawn in SVG by `KnobSVGRoot` and the parts inside
316
340
  * it. Bind the value with `v-model`.
@@ -343,14 +367,23 @@ const Knob = /* @__PURE__ */ defineComponent({
343
367
  type: Object,
344
368
  default: () => linearScale
345
369
  },
346
- /** The value a double click restores. @default min */
347
- defaultValue: Number,
370
+ /**
371
+ * The value a double click restores. `null` turns the double click off.
372
+ * @default startValue
373
+ */
374
+ resetValue: {
375
+ type: Number,
376
+ default: void 0
377
+ },
348
378
  /** Where the active arc starts. @default min */
349
379
  startValue: Number,
350
380
  /** Width and height of the knob. Sets `--knob-size`. */
351
381
  size: [Number, String],
352
- /** The cursor to show while dragging. @default { cursor: 'grabbing' } */
353
- externalStyles: Object,
382
+ /** The cursor to show while dragging. @default 'grabbing' */
383
+ dragCursor: {
384
+ type: String,
385
+ default: "grabbing"
386
+ },
354
387
  /** How much one notch of the wheel moves the value. `null` turns it off. */
355
388
  wheel: {
356
389
  type: [Array, Object],
@@ -368,22 +401,35 @@ const Knob = /* @__PURE__ */ defineComponent({
368
401
  },
369
402
  /** Hide the cursor while dragging and read the movement directly. */
370
403
  pointerLock: Boolean,
371
- /** Restore `defaultValue` on a double click. @default true */
372
- enableDoubleClickDefault: {
373
- type: Boolean,
374
- default: true
375
- },
376
404
  /** Make the knob unchangeable and remove it from the tab order. */
377
405
  disabled: Boolean,
378
406
  /** Make the knob unchangeable while leaving it focusable. */
379
407
  readonly: Boolean,
408
+ /**
409
+ * How long after the last wheel notch or arrow key a change counts as over,
410
+ * in milliseconds, for `change-end`.
411
+ * @default 500
412
+ */
413
+ changeEndDelay: {
414
+ type: Number,
415
+ default: 500
416
+ },
380
417
  /** How far the knob turns from `min` to `max`, in degrees. @default 270 */
381
418
  angleRange: {
382
419
  type: Number,
383
420
  default: 270
384
421
  }
385
422
  },
386
- emits: { "update:modelValue": (value) => typeof value === "number" },
423
+ emits: {
424
+ "update:modelValue": (value) => typeof value === "number",
425
+ /**
426
+ * A change of the value started, with the value before it and what it is
427
+ * made with.
428
+ */
429
+ changeStart: (value, _source) => typeof value === "number",
430
+ /** The change ended, with the value it ended on. */
431
+ changeEnd: (value, _source) => typeof value === "number"
432
+ },
387
433
  setup(props, { slots, emit, expose }) {
388
434
  const root = ref(null);
389
435
  const dragging = ref(false);
@@ -434,7 +480,17 @@ const Knob = /* @__PURE__ */ defineComponent({
434
480
  return angles.value;
435
481
  }
436
482
  });
437
- const change = (next) => emit("update:modelValue", next);
483
+ const gesture = useChangeGesture({
484
+ value: () => props.modelValue,
485
+ inactive: () => inactive.value,
486
+ endDelay: () => props.changeEndDelay,
487
+ onStart: (value, source) => emit("changeStart", value, source),
488
+ onEnd: (value, source) => emit("changeEnd", value, source)
489
+ });
490
+ const change = (next) => {
491
+ gesture.changed(next);
492
+ emit("update:modelValue", next);
493
+ };
438
494
  useDragValue(root, () => ({
439
495
  axis: [range.value, {
440
496
  ...range.value,
@@ -446,7 +502,7 @@ const Knob = /* @__PURE__ */ defineComponent({
446
502
  }),
447
503
  getValue: () => [props.modelValue, props.modelValue],
448
504
  threshold: 1,
449
- cursor: inactive.value ? void 0 : props.externalStyles?.cursor ?? "grabbing",
505
+ cursor: inactive.value ? void 0 : props.dragCursor,
450
506
  pointerLock: inactive.value ? false : props.pointerLock,
451
507
  shouldStart: () => !inactive.value,
452
508
  onChange: (v) => {
@@ -454,9 +510,11 @@ const Knob = /* @__PURE__ */ defineComponent({
454
510
  },
455
511
  onDragStart: () => {
456
512
  dragging.value = true;
513
+ if (!inactive.value) gesture.hold("pointer");
457
514
  },
458
515
  onDragEnd: () => {
459
516
  dragging.value = false;
517
+ gesture.end();
460
518
  }
461
519
  }));
462
520
  useWheel(root, (event) => {
@@ -464,6 +522,7 @@ const Knob = /* @__PURE__ */ defineComponent({
464
522
  const direction = wheelDirection(event);
465
523
  if (direction === null) return;
466
524
  event.preventDefault();
525
+ gesture.pulse("wheel");
467
526
  change(applyDelta(props.modelValue, direction, props.wheel, range.value, event));
468
527
  }, { requireFocus: true });
469
528
  expose({
@@ -492,10 +551,11 @@ const Knob = /* @__PURE__ */ defineComponent({
492
551
  const direction = arrowKeyDirection(event.key);
493
552
  if (direction === null) return;
494
553
  event.preventDefault();
554
+ gesture.pulse("keyboard");
495
555
  change(applyDelta(props.modelValue, direction, props.keyboard, range.value, event));
496
556
  },
497
557
  onDblclick: () => {
498
- if (!inactive.value && props.enableDoubleClickDefault) change(props.defaultValue ?? props.min);
558
+ if (!inactive.value && props.resetValue !== null) gesture.instant("doubleClick", () => change(props.resetValue ?? props.startValue ?? props.min));
499
559
  }
500
560
  }, slots.default?.());
501
561
  }
@@ -599,34 +659,32 @@ const KnobSVGRoot = /* @__PURE__ */ defineComponent({
599
659
  }, slots.default?.());
600
660
  }
601
661
  });
602
- //#endregion
603
- //#region src/components/Knob/KnobThumb.ts
604
662
  /** The circle that turns with the value, and the line on it that points at it. */
605
663
  const KnobThumb = /* @__PURE__ */ defineComponent({
606
664
  name: "KnobThumb",
607
665
  props: {
608
666
  /** Fill colour of the circle. @default 'currentColor' */
609
- thumb: {
667
+ color: {
610
668
  type: String,
611
669
  default: "currentColor"
612
670
  },
613
671
  /** Colour of the line that points at the value. @default 'currentColor' */
614
- thumbLine: {
672
+ lineColor: {
615
673
  type: String,
616
674
  default: "currentColor"
617
675
  },
618
676
  /** Diameter of the circle, as a percentage of the knob. @default 84 */
619
- thumbSize: {
677
+ size: {
620
678
  type: Number,
621
679
  default: 84
622
680
  },
623
681
  /** Thickness of the line, as a percentage of the knob. @default 6 */
624
- thumbLineWeight: {
682
+ lineWeight: {
625
683
  type: Number,
626
684
  default: 6
627
685
  },
628
686
  /** How far down the line reaches, as a percentage. @default 35 */
629
- thumbLineLength: {
687
+ lineLength: {
630
688
  type: Number,
631
689
  default: 35
632
690
  },
@@ -639,16 +697,16 @@ const KnobThumb = /* @__PURE__ */ defineComponent({
639
697
  return () => h("svg", null, [h("circle", {
640
698
  cx: "50%",
641
699
  cy: "50%",
642
- r: `${props.thumbSize / 2}%`,
643
- fill: props.thumb
700
+ r: `${props.size / 2}%`,
701
+ fill: props.color
644
702
  }), h("line", {
645
- class: props.classes?.thumbLine,
703
+ class: props.classes?.line,
646
704
  x1: "50%",
647
- y1: `${(KNOB_VIEWBOX_SIZE - clamp(props.thumbSize, 0, 100)) / 2}%`,
705
+ y1: `${(KNOB_VIEWBOX_SIZE - clamp(props.size, 0, 100)) / 2}%`,
648
706
  x2: "50%",
649
- y2: `${props.thumbLineLength}%`,
650
- stroke: props.thumbLine,
651
- "stroke-width": `${props.thumbLineWeight}%`,
707
+ y2: `${props.lineLength}%`,
708
+ stroke: props.lineColor,
709
+ "stroke-width": `${props.lineWeight}%`,
652
710
  style: {
653
711
  transform: `rotate(${knob.angles.r1 + knob.angles.p * knob.angleRange}deg)`,
654
712
  transformOrigin: "50% 50%"
@@ -663,6 +721,7 @@ const NumberInputKey = Symbol("NumberInput");
663
721
  function useNumberInputContext() {
664
722
  return injectContext(NumberInputKey, "NumberInput");
665
723
  }
724
+ const NumberInputGestureKey = Symbol("NumberInputGesture");
666
725
  /** @internal Whether the drag on `NumberInputStepper` has moved the value. */
667
726
  const StepperKey = Symbol("NumberInputStepper");
668
727
  //#endregion
@@ -682,8 +741,6 @@ const dragSensitivityProp = {
682
741
  type: [Number, Object],
683
742
  default: () => DEFAULT_DRAG_SENSITIVITY
684
743
  };
685
- //#endregion
686
- //#region src/components/NumberInput/NumberInput.ts
687
744
  /** Input with some useful functions for entering numbers. Bind it with `v-model`. */
688
745
  const NumberInput = /* @__PURE__ */ defineComponent({
689
746
  name: "NumberInput",
@@ -732,6 +789,11 @@ const NumberInput = /* @__PURE__ */ defineComponent({
732
789
  },
733
790
  /** Hide the cursor while dragging the stepper. */
734
791
  pointerLock: Boolean,
792
+ /** The cursor to show while dragging the stepper. @default 'ns-resize' */
793
+ dragCursor: {
794
+ type: String,
795
+ default: "ns-resize"
796
+ },
735
797
  /** Select the text when the field takes focus. @default 'none' */
736
798
  selectOnFocus: {
737
799
  type: String,
@@ -749,9 +811,28 @@ const NumberInput = /* @__PURE__ */ defineComponent({
749
811
  /** Make the input unchangeable and remove it from the tab order. */
750
812
  disabled: Boolean,
751
813
  /** Make the value unchangeable while leaving the field focusable. */
752
- readonly: Boolean
814
+ readonly: Boolean,
815
+ /**
816
+ * How long after the last wheel notch, arrow key or typed character a change
817
+ * counts as over, in milliseconds, for `change-end`.
818
+ * @default 500
819
+ */
820
+ changeEndDelay: {
821
+ type: Number,
822
+ default: 500
823
+ }
824
+ },
825
+ emits: {
826
+ "update:modelValue": (value) => typeof value === "number",
827
+ /**
828
+ * A change of the value started — a press on a stepper, the first wheel
829
+ * notch, arrow key or typed character — with the value before it and what
830
+ * it is made with.
831
+ */
832
+ changeStart: (value, _source) => typeof value === "number",
833
+ /** The change ended, with the value it ended on. */
834
+ changeEnd: (value, _source) => typeof value === "number"
753
835
  },
754
- emits: { "update:modelValue": (value) => typeof value === "number" },
755
836
  setup(props, { slots, emit, expose }) {
756
837
  const root = ref(null);
757
838
  /**
@@ -786,11 +867,40 @@ const NumberInput = /* @__PURE__ */ defineComponent({
786
867
  wheel: props.wheel,
787
868
  format: props.format
788
869
  }));
870
+ const gesture = useChangeGesture({
871
+ value: () => props.modelValue,
872
+ inactive: () => inactive.value,
873
+ endDelay: () => props.changeEndDelay,
874
+ onStart: (value, source) => emit("changeStart", value, source),
875
+ onEnd: (value, source) => emit("changeEnd", value, source)
876
+ });
789
877
  function changeValue(next) {
790
878
  if (inactive.value) return;
791
879
  draft.value = null;
792
- if (next !== props.modelValue) emit("update:modelValue", next);
880
+ if (next === props.modelValue) return;
881
+ gesture.changed(next);
882
+ emit("update:modelValue", next);
793
883
  }
884
+ let pressed = false;
885
+ provide(NumberInputGestureKey, {
886
+ press: () => {
887
+ if (inactive.value) return;
888
+ gesture.hold("pointer");
889
+ if (pressed) return;
890
+ pressed = true;
891
+ const release = () => {
892
+ pressed = false;
893
+ gesture.end();
894
+ window.removeEventListener("pointerup", release);
895
+ window.removeEventListener("pointercancel", release);
896
+ };
897
+ window.addEventListener("pointerup", release);
898
+ window.addEventListener("pointercancel", release);
899
+ },
900
+ key: () => {
901
+ if (!inactive.value) gesture.pulse("keyboard");
902
+ }
903
+ });
794
904
  provide(NumberInputKey, {
795
905
  get value() {
796
906
  return props.modelValue;
@@ -822,6 +932,9 @@ const NumberInput = /* @__PURE__ */ defineComponent({
822
932
  get pointerLock() {
823
933
  return props.pointerLock;
824
934
  },
935
+ get dragCursor() {
936
+ return props.dragCursor;
937
+ },
825
938
  get selectOnFocus() {
826
939
  return props.selectOnFocus;
827
940
  },
@@ -856,7 +969,12 @@ const NumberInput = /* @__PURE__ */ defineComponent({
856
969
  if (inactive.value) return;
857
970
  draft.value = next;
858
971
  const parsed = props.parse(next);
859
- if (Number.isFinite(parsed)) emit("update:modelValue", parsed);
972
+ if (!Number.isFinite(parsed)) return;
973
+ if (parsed !== props.modelValue) {
974
+ gesture.pulse("keyboard");
975
+ gesture.changed(parsed);
976
+ }
977
+ emit("update:modelValue", parsed);
860
978
  },
861
979
  commitDraft: () => {
862
980
  if (draft.value === null || inactive.value) return;
@@ -865,8 +983,13 @@ const NumberInput = /* @__PURE__ */ defineComponent({
865
983
  max: props.max,
866
984
  clampValue: props.clampValue
867
985
  });
868
- if (committed === null) draft.value = null;
869
- else changeValue(committed);
986
+ if (committed === null) {
987
+ draft.value = null;
988
+ return;
989
+ }
990
+ if (committed !== props.modelValue) gesture.pulse("keyboard");
991
+ changeValue(committed);
992
+ gesture.end();
870
993
  },
871
994
  changeValue,
872
995
  nudge: (direction, option, modifiers) => changeValue(nudgeNumberInput(props.modelValue, direction, option, ranges.value, modifiers)),
@@ -878,6 +1001,7 @@ const NumberInput = /* @__PURE__ */ defineComponent({
878
1001
  const direction = wheelDirection(event);
879
1002
  if (!props.wheel || inactive.value || direction === null) return;
880
1003
  event.preventDefault();
1004
+ gesture.pulse("wheel");
881
1005
  changeValue(nudgeNumberInput(props.modelValue, direction, props.wheel, ranges.value, event));
882
1006
  }, { requireFocus: true });
883
1007
  expose({
@@ -916,9 +1040,11 @@ function stepperButton(name, direction) {
916
1040
  setup(_, { slots }) {
917
1041
  const field = useNumberInputContext();
918
1042
  const stepper = inject(StepperKey, null);
1043
+ const gesture = inject(NumberInputGestureKey, null);
919
1044
  const blocked = computed(() => field.disabled || (direction > 0 ? field.atMax : field.atMin));
920
1045
  const press = useLongPress({ onPress: () => {
921
1046
  if (field.disabled || field.readonly) return;
1047
+ gesture?.press();
922
1048
  if (stepper?.moved()) return;
923
1049
  field.nudge(direction, ["raw", field.step]);
924
1050
  } });
@@ -959,6 +1085,7 @@ const NumberInputField = /* @__PURE__ */ defineComponent({
959
1085
  name: "NumberInputField",
960
1086
  setup(_, { expose }) {
961
1087
  const field = useNumberInputContext();
1088
+ const gesture = inject(NumberInputGestureKey, null);
962
1089
  const input = ref(null);
963
1090
  const focused = ref(false);
964
1091
  watchEffect(() => field.setInput(input.value));
@@ -979,6 +1106,7 @@ const NumberInputField = /* @__PURE__ */ defineComponent({
979
1106
  const element = event.currentTarget;
980
1107
  const from = element.value;
981
1108
  const offset = field.keepCaretOnStep && element.selectionStart !== null ? caretDecimalOffset(from, element.selectionStart) : null;
1109
+ gesture?.key();
982
1110
  field.nudge(direction, field.keyboard, event);
983
1111
  if (offset === null) return;
984
1112
  await nextTick();
@@ -1044,6 +1172,7 @@ const NumberInputStepper = /* @__PURE__ */ defineComponent({
1044
1172
  name: "NumberInputStepper",
1045
1173
  setup(_, { slots }) {
1046
1174
  const field = useNumberInputContext();
1175
+ const gesture = inject(NumberInputGestureKey, null);
1047
1176
  const el = ref(null);
1048
1177
  let instance = null;
1049
1178
  const enabled = computed(() => field.drag !== null && !field.disabled && !field.readonly);
@@ -1051,7 +1180,8 @@ const NumberInputStepper = /* @__PURE__ */ defineComponent({
1051
1180
  range: field.rawRange,
1052
1181
  pixels: field.drag ?? 1,
1053
1182
  sensitivity: field.dragSensitivity,
1054
- pointerLock: field.pointerLock
1183
+ pointerLock: field.pointerLock,
1184
+ cursor: field.dragCursor
1055
1185
  });
1056
1186
  watch([el, enabled], ([element, on], _, onCleanup) => {
1057
1187
  if (!element || !on) return;
@@ -1071,11 +1201,14 @@ const NumberInputStepper = /* @__PURE__ */ defineComponent({
1071
1201
  });
1072
1202
  watch(options, (next) => instance?.update(next));
1073
1203
  provide(StepperKey, { moved: () => instance?.moved() ?? false });
1074
- return () => h("div", { ref: el }, slots.default?.());
1204
+ return () => h("div", {
1205
+ ref: el,
1206
+ onPointerdown: () => {
1207
+ if (enabled.value) gesture?.press();
1208
+ }
1209
+ }, slots.default?.());
1075
1210
  }
1076
1211
  });
1077
- //#endregion
1078
- //#region src/components/Piano/Piano.ts
1079
1212
  /**
1080
1213
  * Customizable piano. The keys are drawn by the component; what goes inside
1081
1214
  * one is the `label` slot, which receives the note and its state.
@@ -1285,8 +1418,11 @@ const PointsEditor = /* @__PURE__ */ defineComponent({
1285
1418
  disabled: Boolean,
1286
1419
  /** Make the points unmovable. */
1287
1420
  readonly: Boolean,
1288
- /** The cursor to show while dragging a point. @default { cursor: 'grabbing' } */
1289
- externalStyles: Object,
1421
+ /** The cursor to show while dragging a point. @default 'grabbing' */
1422
+ dragCursor: {
1423
+ type: String,
1424
+ default: "grabbing"
1425
+ },
1290
1426
  /** How much one wheel notch moves the focused point. */
1291
1427
  wheel: {
1292
1428
  type: [Array, Object],
@@ -1346,8 +1482,8 @@ const PointsEditor = /* @__PURE__ */ defineComponent({
1346
1482
  get dragSensitivity() {
1347
1483
  return props.dragSensitivity;
1348
1484
  },
1349
- get cursor() {
1350
- return props.externalStyles?.cursor ?? "grabbing";
1485
+ get dragCursor() {
1486
+ return props.dragCursor;
1351
1487
  },
1352
1488
  get selectable() {
1353
1489
  return props.selectable;
@@ -1484,6 +1620,15 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1484
1620
  type: Boolean,
1485
1621
  default: void 0
1486
1622
  },
1623
+ /**
1624
+ * How long after the last wheel notch or arrow key a change counts as over,
1625
+ * in milliseconds, for `change-end`.
1626
+ * @default 500
1627
+ */
1628
+ changeEndDelay: {
1629
+ type: Number,
1630
+ default: 500
1631
+ },
1487
1632
  /** Overrides the `wheel` of `PointsEditor`; `null` turns it off. */
1488
1633
  wheel: {
1489
1634
  type: [Array, Object],
@@ -1501,8 +1646,15 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1501
1646
  },
1502
1647
  emits: {
1503
1648
  "update:modelValue": (value) => typeof value === "object",
1504
- dragStart: (value) => typeof value === "object",
1505
- dragEnd: (value) => typeof value === "object"
1649
+ /**
1650
+ * A change of this point started — a drag on it, the first wheel notch or
1651
+ * arrow key while it has the focus — with where it is and what the change
1652
+ * is made with. The points that move along with a selection report
1653
+ * through `update:modelValue` only.
1654
+ */
1655
+ changeStart: (value, _source) => typeof value === "object",
1656
+ /** The change ended, with where the point is. */
1657
+ changeEnd: (value, _source) => typeof value === "object"
1506
1658
  },
1507
1659
  setup(props, { slots, attrs, emit }) {
1508
1660
  const points = usePointsEditorContext();
@@ -1515,6 +1667,13 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1515
1667
  const wheel = computed(() => props.wheel === void 0 ? points.wheel : props.wheel);
1516
1668
  const keyboard = computed(() => props.keyboard === void 0 ? points.keyboard : props.keyboard);
1517
1669
  const current = computed(() => clampPoint(props.modelValue, props.min, props.max));
1670
+ const gesture = useChangeGesture({
1671
+ value: () => current.value,
1672
+ inactive: () => inactive.value,
1673
+ endDelay: () => props.changeEndDelay,
1674
+ onStart: (value, source) => emit("changeStart", value, source),
1675
+ onEnd: (value, source) => emit("changeEnd", value, source)
1676
+ });
1518
1677
  const el = ref(null);
1519
1678
  const x = ref(null);
1520
1679
  const dragging = ref(false);
@@ -1523,9 +1682,13 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1523
1682
  min: props.min,
1524
1683
  max: props.max,
1525
1684
  readonly: inactive.value,
1526
- onChange: (next) => emit("update:modelValue", next),
1685
+ onChange: (next) => {
1686
+ gesture.changed(next);
1687
+ emit("update:modelValue", next);
1688
+ },
1527
1689
  element: el.value,
1528
- wheel: wheel.value
1690
+ wheel: wheel.value,
1691
+ beforeWheel: () => gesture.pulse("wheel")
1529
1692
  });
1530
1693
  watch(id, (key, _, onCleanup) => onCleanup(points.editor.registerPoint(key, read)), { immediate: true });
1531
1694
  /** Where the pointer was when the drag started. */
@@ -1535,7 +1698,7 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1535
1698
  const drag = createDragValue(element, {
1536
1699
  axis: POINT_AXIS,
1537
1700
  mapping: elementMapping(() => points.container, { sensitivity: (state) => selectModifier(points.dragSensitivity, state.event).value }),
1538
- cursor: inactive.value ? void 0 : points.cursor,
1701
+ cursor: inactive.value ? void 0 : points.dragCursor,
1539
1702
  shouldStart: () => !disabled.value,
1540
1703
  onChange: ([px, py]) => {
1541
1704
  if (origin) points.editor.movePointDrag({
@@ -1551,15 +1714,15 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1551
1714
  };
1552
1715
  dragging.value = true;
1553
1716
  x.value?.focus();
1554
- if (!inactive.value) emit("dragStart", current.value);
1717
+ if (!inactive.value) gesture.hold("pointer");
1555
1718
  },
1556
1719
  onDragEnd: () => {
1557
1720
  origin = null;
1558
1721
  dragging.value = false;
1559
- if (!inactive.value) emit("dragEnd", current.value);
1722
+ gesture.end();
1560
1723
  }
1561
1724
  });
1562
- const stop = watch(() => inactive.value ? void 0 : points.cursor, (cursor) => drag.update({ cursor }));
1725
+ const stop = watch(() => inactive.value ? void 0 : points.dragCursor, (cursor) => drag.update({ cursor }));
1563
1726
  onCleanup(() => {
1564
1727
  stop();
1565
1728
  drag.destroy();
@@ -1624,6 +1787,7 @@ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1624
1787
  if (!move) return;
1625
1788
  event.preventDefault();
1626
1789
  if (inactive.value || !keyboard.value) return;
1790
+ gesture.pulse("keyboard");
1627
1791
  points.editor.nudgePoint(id.value, move.axis === 0 ? "x" : "y", move.direction, keyboard.value, event);
1628
1792
  }
1629
1793
  }, {
@@ -1671,8 +1835,6 @@ const SliderKey = Symbol("Slider");
1671
1835
  function useSliderContext() {
1672
1836
  return injectContext(SliderKey, "Slider");
1673
1837
  }
1674
- //#endregion
1675
- //#region src/components/Slider/Slider.ts
1676
1838
  /** Customizable slider. Bind the value with `v-model`. */
1677
1839
  const Slider = /* @__PURE__ */ defineComponent({
1678
1840
  name: "Slider",
@@ -1702,33 +1864,55 @@ const Slider = /* @__PURE__ */ defineComponent({
1702
1864
  type: Object,
1703
1865
  default: () => linearScale
1704
1866
  },
1705
- /** Run the slider vertically, with the value growing upwards. */
1706
- vertical: Boolean,
1867
+ /**
1868
+ * Which way the slider runs. A vertical slider grows upwards.
1869
+ * @default 'horizontal'
1870
+ */
1871
+ orientation: {
1872
+ type: String,
1873
+ default: "horizontal"
1874
+ },
1707
1875
  /** Grow the value the other way. */
1708
1876
  reverse: Boolean,
1709
- /** The cursor to show while dragging. @default { cursor: 'pointer' } */
1710
- externalStyles: Object,
1877
+ /** The cursor to show while dragging. @default 'pointer' */
1878
+ dragCursor: {
1879
+ type: String,
1880
+ default: "pointer"
1881
+ },
1711
1882
  wheel: wheelProp,
1712
1883
  keyboard: keyboardProp,
1713
1884
  dragSensitivity: dragSensitivityProp,
1714
1885
  /** Make the slider unchangeable and remove it from the tab order. */
1715
1886
  disabled: Boolean,
1716
1887
  /** Make the value unchangeable. */
1717
- readonly: Boolean
1888
+ readonly: Boolean,
1889
+ /**
1890
+ * How long after the last wheel notch or arrow key a change counts as over,
1891
+ * in milliseconds, for `change-end`.
1892
+ * @default 500
1893
+ */
1894
+ changeEndDelay: {
1895
+ type: Number,
1896
+ default: 500
1897
+ }
1718
1898
  },
1719
1899
  emits: {
1720
1900
  "update:modelValue": (value) => typeof value === "number",
1721
- /** A drag started, with the value where the track was pressed. */
1722
- dragStart: (value) => typeof value === "number",
1723
- /** The drag ended, with the value it ended on. */
1724
- dragEnd: (value) => typeof value === "number"
1901
+ /**
1902
+ * A change of the value started, with the value before it and what it is
1903
+ * made with.
1904
+ */
1905
+ changeStart: (value, _source) => typeof value === "number",
1906
+ /** The change ended, with the value it ended on. */
1907
+ changeEnd: (value, _source) => typeof value === "number"
1725
1908
  },
1726
1909
  setup(props, { slots, emit, expose }) {
1727
1910
  const root = ref(null);
1728
1911
  let track = null;
1729
1912
  let thumb = null;
1730
1913
  const inactive = computed(() => props.disabled || props.readonly);
1731
- const displayReversed = computed(() => props.vertical !== props.reverse);
1914
+ const vertical = computed(() => props.orientation === "vertical");
1915
+ const displayReversed = computed(() => vertical.value !== props.reverse);
1732
1916
  const percent = computed(() => valuePercent(props.modelValue, {
1733
1917
  min: props.min,
1734
1918
  max: props.max,
@@ -1747,7 +1931,17 @@ const Slider = /* @__PURE__ */ defineComponent({
1747
1931
  keyboard: props.keyboard,
1748
1932
  wheel: props.wheel
1749
1933
  }));
1750
- const change = (next) => emit("update:modelValue", next);
1934
+ const gesture = useChangeGesture({
1935
+ value: () => props.modelValue,
1936
+ inactive: () => inactive.value,
1937
+ endDelay: () => props.changeEndDelay,
1938
+ onStart: (value, source) => emit("changeStart", value, source),
1939
+ onEnd: (value, source) => emit("changeEnd", value, source)
1940
+ });
1941
+ const change = (next) => {
1942
+ gesture.changed(next);
1943
+ emit("update:modelValue", next);
1944
+ };
1751
1945
  provide(SliderKey, {
1752
1946
  get value() {
1753
1947
  return props.modelValue;
@@ -1764,8 +1958,8 @@ const Slider = /* @__PURE__ */ defineComponent({
1764
1958
  get scale() {
1765
1959
  return props.scale;
1766
1960
  },
1767
- get vertical() {
1768
- return props.vertical;
1961
+ get orientation() {
1962
+ return props.orientation;
1769
1963
  },
1770
1964
  get reverse() {
1771
1965
  return props.reverse;
@@ -1787,30 +1981,29 @@ const Slider = /* @__PURE__ */ defineComponent({
1787
1981
  thumb = input;
1788
1982
  }
1789
1983
  });
1790
- const valueOf = (v) => v[props.vertical ? 1 : 0];
1984
+ const valueOf = (v) => v[vertical.value ? 1 : 0];
1791
1985
  useDragValue(root, () => ({
1792
1986
  axis: axis.value,
1793
1987
  mapping: elementMapping(() => track, { sensitivity: (state) => selectModifier(props.dragSensitivity, state.event).value }),
1794
- cursor: inactive.value ? void 0 : props.externalStyles?.cursor ?? "pointer",
1988
+ cursor: inactive.value ? void 0 : props.dragCursor,
1795
1989
  shouldStart: () => !inactive.value,
1796
1990
  updateOnPointerDown: true,
1797
1991
  onChange: (v) => {
1798
1992
  if (!inactive.value) change(valueOf(v));
1799
1993
  },
1800
- onDragStart: (v) => {
1994
+ onDragStart: () => {
1801
1995
  if (inactive.value) return;
1996
+ gesture.hold("pointer");
1802
1997
  thumb?.focus();
1803
- emit("dragStart", valueOf(v));
1804
1998
  },
1805
- onDragEnd: (v) => {
1806
- if (!inactive.value) emit("dragEnd", valueOf(v));
1807
- }
1999
+ onDragEnd: () => gesture.end()
1808
2000
  }));
1809
2001
  useWheel(root, (event) => {
1810
2002
  if (!props.wheel || inactive.value) return;
1811
- const direction = wheelDirection(event, { horizontal: !props.vertical });
2003
+ const direction = wheelDirection(event, { horizontal: !vertical.value });
1812
2004
  if (direction === null) return;
1813
2005
  event.preventDefault();
2006
+ gesture.pulse("wheel");
1814
2007
  change(applyDelta(props.modelValue, props.reverse ? -direction : direction, props.wheel, axis.value, event));
1815
2008
  }, { requireFocus: true });
1816
2009
  expose({
@@ -1825,7 +2018,7 @@ const Slider = /* @__PURE__ */ defineComponent({
1825
2018
  ref: root,
1826
2019
  role: "group",
1827
2020
  tabindex: -1,
1828
- "data-orientation": props.vertical ? "vertical" : "horizontal",
2021
+ "data-orientation": props.orientation,
1829
2022
  "data-disabled": props.disabled ? "" : void 0,
1830
2023
  "data-readonly": props.readonly ? "" : void 0,
1831
2024
  onKeydown: (event) => {
@@ -1833,6 +2026,7 @@ const Slider = /* @__PURE__ */ defineComponent({
1833
2026
  if (direction === null) return;
1834
2027
  event.preventDefault();
1835
2028
  if (!props.keyboard || inactive.value) return;
2029
+ gesture.pulse("keyboard");
1836
2030
  change(applyDelta(props.modelValue, props.reverse ? -direction : direction, props.keyboard, axis.value, event));
1837
2031
  },
1838
2032
  onFocus: () => {
@@ -1842,8 +2036,6 @@ const Slider = /* @__PURE__ */ defineComponent({
1842
2036
  }, slots.default?.());
1843
2037
  }
1844
2038
  });
1845
- //#endregion
1846
- //#region src/components/Slider/SliderMarksOption.ts
1847
2039
  /** One mark along the slider, and its label. */
1848
2040
  const SliderMarksOption = /* @__PURE__ */ defineComponent({
1849
2041
  name: "SliderMarksOption",
@@ -1888,9 +2080,9 @@ const SliderMarksOption = /* @__PURE__ */ defineComponent({
1888
2080
  min: slider.min,
1889
2081
  max: slider.max,
1890
2082
  scale: slider.scale
1891
- }, slider.vertical !== slider.reverse));
2083
+ }, slider.orientation === "vertical" !== slider.reverse));
1892
2084
  return () => {
1893
- const orientation = slider.vertical ? "vertical" : "horizontal";
2085
+ const orientation = slider.orientation;
1894
2086
  return h("div", {
1895
2087
  "data-orientation": orientation,
1896
2088
  style: {
@@ -1898,10 +2090,10 @@ const SliderMarksOption = /* @__PURE__ */ defineComponent({
1898
2090
  "--length": cssLength(props.length),
1899
2091
  "--gap": cssLength(props.gap),
1900
2092
  position: "absolute",
1901
- translate: slider.vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
2093
+ translate: slider.orientation === "vertical" ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
1902
2094
  zIndex: 10,
1903
- left: slider.vertical ? void 0 : `${percent.value}%`,
1904
- top: slider.vertical ? `${percent.value}%` : void 0
2095
+ left: slider.orientation === "vertical" ? void 0 : `${percent.value}%`,
2096
+ top: slider.orientation === "vertical" ? `${percent.value}%` : void 0
1905
2097
  }
1906
2098
  }, [props.mark ? h("div", {
1907
2099
  class: props.classes?.mark,
@@ -1914,8 +2106,6 @@ const SliderMarksOption = /* @__PURE__ */ defineComponent({
1914
2106
  };
1915
2107
  }
1916
2108
  });
1917
- //#endregion
1918
- //#region src/components/Slider/SliderMarks.ts
1919
2109
  /** The marks along the slider: written out as options, or built from `options`. */
1920
2110
  const SliderMarks = /* @__PURE__ */ defineComponent({
1921
2111
  name: "SliderMarks",
@@ -1939,10 +2129,10 @@ const SliderMarks = /* @__PURE__ */ defineComponent({
1939
2129
  const marks = computed(() => {
1940
2130
  if (props.options === void 0) return [];
1941
2131
  const list = sliderMarks(props.options, slider.min, slider.max, slider.step);
1942
- return slider.vertical !== slider.reverse ? list.reverse() : list;
2132
+ return slider.orientation === "vertical" !== slider.reverse ? list.reverse() : list;
1943
2133
  });
1944
2134
  return () => h("div", {
1945
- "data-orientation": slider.vertical ? "vertical" : "horizontal",
2135
+ "data-orientation": slider.orientation,
1946
2136
  style: {
1947
2137
  "--gap": cssLength(props.gap),
1948
2138
  position: "relative"
@@ -1955,8 +2145,6 @@ const SliderMarks = /* @__PURE__ */ defineComponent({
1955
2145
  })) : slots.default?.());
1956
2146
  }
1957
2147
  });
1958
- //#endregion
1959
- //#region src/components/Slider/SliderThumb.ts
1960
2148
  /**
1961
2149
  * The thumb, placed at the value. The ARIA (`aria-label` and friends) lands
1962
2150
  * on the range input inside it, which is the control and takes the focus.
@@ -1994,8 +2182,8 @@ color: String },
1994
2182
  },
1995
2183
  style,
1996
2184
  {
1997
- top: slider.vertical ? `${slider.percent}%` : "50%",
1998
- left: slider.vertical ? "50%" : `${slider.percent}%`
2185
+ top: slider.orientation === "vertical" ? `${slider.percent}%` : "50%",
2186
+ left: slider.orientation === "vertical" ? "50%" : `${slider.percent}%`
1999
2187
  }
2000
2188
  ]
2001
2189
  }, [visuallyHiddenRangeInput({
@@ -2006,7 +2194,7 @@ color: String },
2006
2194
  step: slider.step,
2007
2195
  disabled: slider.disabled,
2008
2196
  "aria-readonly": slider.readonly,
2009
- "aria-orientation": slider.vertical ? "vertical" : "horizontal",
2197
+ "aria-orientation": slider.orientation,
2010
2198
  "aria-label": ariaLabel,
2011
2199
  "aria-labelledby": ariaLabelledby,
2012
2200
  "aria-describedby": ariaDescribedby,
@@ -2023,8 +2211,6 @@ color: String },
2023
2211
  };
2024
2212
  }
2025
2213
  });
2026
- //#endregion
2027
- //#region src/components/Slider/SliderTrack.ts
2028
2214
  /** The track the thumb runs along, and what the pointer is measured against. */
2029
2215
  const SliderTrack = /* @__PURE__ */ defineComponent({
2030
2216
  name: "SliderTrack",
@@ -2033,10 +2219,10 @@ const SliderTrack = /* @__PURE__ */ defineComponent({
2033
2219
  length: [Number, String],
2034
2220
  /** How thick the track is across that axis. Sets `--thickness`. */
2035
2221
  thickness: [Number, String],
2036
- /** Colour of the part from `min` to the value. Sets `--active`. */
2037
- active: String,
2038
- /** Colour of the rest of the track. Sets `--inactive`. */
2039
- inactive: String
2222
+ /** Colour of the part from `min` to the value. Sets `--active-color`. */
2223
+ activeColor: String,
2224
+ /** Colour of the rest of the track. Sets `--inactive-color`. */
2225
+ inactiveColor: String
2040
2226
  },
2041
2227
  setup(props, { slots }) {
2042
2228
  const slider = useSliderContext();
@@ -2047,11 +2233,11 @@ const SliderTrack = /* @__PURE__ */ defineComponent({
2047
2233
  return () => h("div", {
2048
2234
  ref: el,
2049
2235
  "data-disabled": slider.disabled ? "" : void 0,
2050
- "data-orientation": slider.vertical ? "vertical" : "horizontal",
2051
- "data-flipped": slider.vertical !== slider.reverse ? "" : void 0,
2236
+ "data-orientation": slider.orientation,
2237
+ "data-flipped": slider.orientation === "vertical" !== slider.reverse ? "" : void 0,
2052
2238
  style: {
2053
- "--active": props.active,
2054
- "--inactive": props.inactive,
2239
+ "--active-color": props.activeColor,
2240
+ "--inactive-color": props.inactiveColor,
2055
2241
  "--length": cssLength(props.length),
2056
2242
  "--thickness": cssLength(props.thickness),
2057
2243
  "--percent": `${slider.percent}%`,
@@ -2067,8 +2253,6 @@ const XYPadKey = Symbol("XYPad");
2067
2253
  function useXYPadContext() {
2068
2254
  return injectContext(XYPadKey, "XYPad");
2069
2255
  }
2070
- //#endregion
2071
- //#region src/components/XYPad/XYPad.ts
2072
2256
  /**
2073
2257
  * Two-dimensional slider. The per-axis settings are `[x, y]` tuples, and a
2074
2258
  * plain value applies to both axes. Bind the value with `v-model`.
@@ -2106,20 +2290,37 @@ const XYPad = /* @__PURE__ */ defineComponent({
2106
2290
  type: [Boolean, Array],
2107
2291
  default: false
2108
2292
  },
2109
- /** The cursor to show while dragging. @default { cursor: 'pointer' } */
2110
- externalStyles: Object,
2293
+ /** The cursor to show while dragging. @default 'pointer' */
2294
+ dragCursor: {
2295
+ type: String,
2296
+ default: "pointer"
2297
+ },
2111
2298
  wheel: wheelProp,
2112
2299
  keyboard: keyboardProp,
2113
2300
  dragSensitivity: dragSensitivityProp,
2114
2301
  /** Make the pad unchangeable and remove it from the tab order. */
2115
2302
  disabled: Boolean,
2116
2303
  /** Make the value unchangeable. */
2117
- readonly: Boolean
2304
+ readonly: Boolean,
2305
+ /**
2306
+ * How long after the last wheel notch or arrow key a change counts as over,
2307
+ * in milliseconds, for `change-end`.
2308
+ * @default 500
2309
+ */
2310
+ changeEndDelay: {
2311
+ type: Number,
2312
+ default: 500
2313
+ }
2118
2314
  },
2119
2315
  emits: {
2120
2316
  "update:modelValue": (value) => Array.isArray(value),
2121
- dragStart: (value) => Array.isArray(value),
2122
- dragEnd: (value) => Array.isArray(value)
2317
+ /**
2318
+ * A change of the value started, with the value before it and what it is
2319
+ * made with.
2320
+ */
2321
+ changeStart: (value, _source) => Array.isArray(value),
2322
+ /** The change ended, with the value it ended on. */
2323
+ changeEnd: (value, _source) => Array.isArray(value)
2123
2324
  },
2124
2325
  setup(props, { slots, emit, expose }) {
2125
2326
  const root = ref(null);
@@ -2150,7 +2351,17 @@ const XYPad = /* @__PURE__ */ defineComponent({
2150
2351
  keyboard: props.keyboard,
2151
2352
  wheel: props.wheel
2152
2353
  }));
2153
- const change = (next) => emit("update:modelValue", next);
2354
+ const gesture = useChangeGesture({
2355
+ value: () => props.modelValue,
2356
+ inactive: () => inactive.value,
2357
+ endDelay: () => props.changeEndDelay,
2358
+ onStart: (value, source) => emit("changeStart", value, source),
2359
+ onEnd: (value, source) => emit("changeEnd", value, source)
2360
+ });
2361
+ const change = (next) => {
2362
+ gesture.changed(next);
2363
+ emit("update:modelValue", next);
2364
+ };
2154
2365
  /** Move one axis by one press of `option`, in screen coordinates. */
2155
2366
  function nudge({ axis: i, direction }, option, modifiers) {
2156
2367
  const value = props.modelValue;
@@ -2197,25 +2408,24 @@ const XYPad = /* @__PURE__ */ defineComponent({
2197
2408
  axis: axis.value,
2198
2409
  mapping: elementMapping(() => area, { sensitivity: (state) => selectModifier(props.dragSensitivity, state.event).value }),
2199
2410
  updateOnPointerDown: true,
2200
- cursor: inactive.value ? void 0 : props.externalStyles?.cursor ?? "pointer",
2411
+ cursor: inactive.value ? void 0 : props.dragCursor,
2201
2412
  shouldStart: () => !inactive.value,
2202
2413
  onChange: (v) => {
2203
2414
  if (!inactive.value) change(v);
2204
2415
  },
2205
- onDragStart: (v) => {
2416
+ onDragStart: () => {
2206
2417
  if (inactive.value) return;
2418
+ gesture.hold("pointer");
2207
2419
  thumb?.focus();
2208
- emit("dragStart", v);
2209
2420
  },
2210
- onDragEnd: (v) => {
2211
- if (!inactive.value) emit("dragEnd", v);
2212
- }
2421
+ onDragEnd: () => gesture.end()
2213
2422
  }));
2214
2423
  useWheel(root, (event) => {
2215
2424
  if (!props.wheel || inactive.value) return;
2216
2425
  const move = wheelMove(event);
2217
2426
  if (!move) return;
2218
2427
  event.preventDefault();
2428
+ gesture.pulse("wheel");
2219
2429
  nudge(move, props.wheel, event);
2220
2430
  }, { requireFocus: true });
2221
2431
  expose({
@@ -2234,7 +2444,10 @@ const XYPad = /* @__PURE__ */ defineComponent({
2234
2444
  const move = arrowKeyMove(event.key);
2235
2445
  if (!move) return;
2236
2446
  event.preventDefault();
2237
- if (props.keyboard && !inactive.value) nudge(move, props.keyboard, event);
2447
+ if (props.keyboard && !inactive.value) {
2448
+ gesture.pulse("keyboard");
2449
+ nudge(move, props.keyboard, event);
2450
+ }
2238
2451
  },
2239
2452
  onFocus: () => {
2240
2453
  if (!props.disabled) thumb?.focus();
@@ -2260,8 +2473,6 @@ const XYPadArea = /* @__PURE__ */ defineComponent({
2260
2473
  }, slots.default?.());
2261
2474
  }
2262
2475
  });
2263
- //#endregion
2264
- //#region src/components/XYPad/XYPadThumb.ts
2265
2476
  /** The thumb, placed at the value, with one range input per axis inside. */
2266
2477
  const XYPadThumb = /* @__PURE__ */ defineComponent({
2267
2478
  name: "XYPadThumb",