@tremolo-ui/vue 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +13 -0
  3. package/dist/index.cjs +2486 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.d.cts +1678 -0
  6. package/dist/index.d.cts.map +1 -0
  7. package/dist/index.d.ts +1678 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +2444 -0
  10. package/dist/index.js.map +1 -0
  11. package/package.json +78 -0
  12. package/src/components/AnimationCanvas/AnimationCanvas.ts +105 -0
  13. package/src/components/AnimationCanvas/index.ts +5 -0
  14. package/src/components/DropZone/DropZone.ts +43 -0
  15. package/src/components/DropZone/index.ts +1 -0
  16. package/src/components/FileInput/FileInput.ts +82 -0
  17. package/src/components/FileInput/FileInputTrigger.ts +24 -0
  18. package/src/components/FileInput/context.ts +18 -0
  19. package/src/components/FileInput/index.ts +3 -0
  20. package/src/components/Knob/Knob.ts +237 -0
  21. package/src/components/Knob/KnobActiveLine.ts +31 -0
  22. package/src/components/Knob/KnobInactiveLine.ts +36 -0
  23. package/src/components/Knob/KnobSVGRoot.ts +25 -0
  24. package/src/components/Knob/KnobThumb.ts +53 -0
  25. package/src/components/Knob/context.ts +30 -0
  26. package/src/components/Knob/index.ts +6 -0
  27. package/src/components/NumberInput/NumberInput.ts +258 -0
  28. package/src/components/NumberInput/NumberInputDecrementStepper.ts +7 -0
  29. package/src/components/NumberInput/NumberInputField.ts +128 -0
  30. package/src/components/NumberInput/NumberInputIncrementStepper.ts +7 -0
  31. package/src/components/NumberInput/NumberInputStepper.ts +54 -0
  32. package/src/components/NumberInput/context.ts +63 -0
  33. package/src/components/NumberInput/index.ts +6 -0
  34. package/src/components/NumberInput/stepper-button.ts +65 -0
  35. package/src/components/Piano/Piano.ts +243 -0
  36. package/src/components/Piano/index.ts +1 -0
  37. package/src/components/PointsEditor/PointsEditor.ts +146 -0
  38. package/src/components/PointsEditor/PointsEditorBackground.ts +17 -0
  39. package/src/components/PointsEditor/PointsEditorContainer.ts +73 -0
  40. package/src/components/PointsEditor/PointsEditorPoint.ts +253 -0
  41. package/src/components/PointsEditor/PointsEditorSelectionBox.ts +36 -0
  42. package/src/components/PointsEditor/context.ts +39 -0
  43. package/src/components/PointsEditor/index.ts +9 -0
  44. package/src/components/Slider/Slider.ts +224 -0
  45. package/src/components/Slider/SliderMarks.ts +57 -0
  46. package/src/components/Slider/SliderMarksOption.ts +85 -0
  47. package/src/components/Slider/SliderThumb.ts +92 -0
  48. package/src/components/Slider/SliderTrack.ts +50 -0
  49. package/src/components/Slider/context.ts +33 -0
  50. package/src/components/Slider/index.ts +6 -0
  51. package/src/components/XYPad/XYPad.ts +250 -0
  52. package/src/components/XYPad/XYPadArea.ts +20 -0
  53. package/src/components/XYPad/XYPadThumb.ts +105 -0
  54. package/src/components/XYPad/context.ts +33 -0
  55. package/src/components/XYPad/index.ts +4 -0
  56. package/src/components/_util/context.ts +8 -0
  57. package/src/components/_util/placement.ts +36 -0
  58. package/src/components/_util/props.ts +32 -0
  59. package/src/components/_util/useCheckSteps.ts +22 -0
  60. package/src/components/_util/visually-hidden-range-input.ts +16 -0
  61. package/src/composables/useDrag.ts +22 -0
  62. package/src/composables/useDragValue.ts +20 -0
  63. package/src/composables/useDropZone.ts +32 -0
  64. package/src/composables/useInstance.ts +52 -0
  65. package/src/composables/useLongPress.ts +20 -0
  66. package/src/composables/useMIDIAccess.ts +58 -0
  67. package/src/composables/useMIDIInput.ts +33 -0
  68. package/src/composables/useMIDIMessage.ts +30 -0
  69. package/src/composables/useWheel.ts +23 -0
  70. package/src/index.ts +67 -0
package/dist/index.js ADDED
@@ -0,0 +1,2444 @@
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";
3
+ import { clamp, isWhiteKey, linearScale, noteKey } from "@tremolo-ui/functions";
4
+ //#region src/components/AnimationCanvas/AnimationCanvas.ts
5
+ /** A `<canvas>` drawn by `draw` on every animation frame. */
6
+ const AnimationCanvas = /* @__PURE__ */ defineComponent({
7
+ name: "AnimationCanvas",
8
+ inheritAttrs: false,
9
+ props: {
10
+ /** Draws a frame. */
11
+ draw: {
12
+ type: Function,
13
+ required: true
14
+ },
15
+ /** Sets the canvas up before the first frame and after a resize. */
16
+ init: Function,
17
+ /** Draw on every animation frame. @default true */
18
+ animate: {
19
+ type: Boolean,
20
+ default: true
21
+ },
22
+ /** Settings for the 2D context. Read once, when the canvas is created. */
23
+ options: Object,
24
+ /** Keep the drawing while the canvas is resized. @default true */
25
+ reduceFlickering: {
26
+ type: Boolean,
27
+ default: true
28
+ },
29
+ /** Follow the size of the parent element; `width` and `height` are ignored. */
30
+ resizable: Boolean,
31
+ /** Width in CSS pixels, while not `resizable`. @default 100 */
32
+ width: {
33
+ type: Number,
34
+ default: 100
35
+ },
36
+ /** Height in CSS pixels, while not `resizable`. @default 100 */
37
+ height: {
38
+ type: Number,
39
+ default: 100
40
+ }
41
+ },
42
+ setup(props, { attrs }) {
43
+ const canvas = ref(null);
44
+ const instance = shallowRef(null);
45
+ watch([canvas, () => props.resizable], ([element, resizable], _, onCleanup) => {
46
+ if (!element) return;
47
+ const current = createAnimationCanvas(element, {
48
+ draw: (context, frame) => props.draw(context, frame),
49
+ init: (context, size) => props.init?.(context, size),
50
+ animate: props.animate,
51
+ size: {
52
+ width: props.width,
53
+ height: props.height
54
+ },
55
+ reduceFlickering: props.reduceFlickering,
56
+ resizable,
57
+ contextAttributes: props.options
58
+ });
59
+ instance.value = current;
60
+ onCleanup(() => {
61
+ if (instance.value === current) instance.value = null;
62
+ current.destroy();
63
+ });
64
+ }, {
65
+ immediate: true,
66
+ flush: "post"
67
+ });
68
+ watchEffect(() => {
69
+ props.draw;
70
+ props.init;
71
+ instance.value?.update({
72
+ animate: props.animate,
73
+ size: {
74
+ width: props.width,
75
+ height: props.height
76
+ },
77
+ reduceFlickering: props.reduceFlickering
78
+ });
79
+ }, { flush: "post" });
80
+ return () => h("canvas", {
81
+ onContextmenu: (event) => event.preventDefault(),
82
+ ...attrs,
83
+ ref: canvas
84
+ });
85
+ }
86
+ });
87
+ //#endregion
88
+ //#region src/composables/useInstance.ts
89
+ /**
90
+ * Create an instance for the element `target` holds, and destroy it when the
91
+ * element goes away or changes. Options are pushed to the instance with
92
+ * `update` as they change, so that changing them does not interrupt a drag in
93
+ * progress.
94
+ *
95
+ * Call it during `setup`: the watchers stop with the component.
96
+ */
97
+ function useInstance(target, options, create, update) {
98
+ let instance = null;
99
+ watch(() => toValue(target), (element, _, onCleanup) => {
100
+ if (!element) return;
101
+ const current = create(element, toValue(options));
102
+ instance = current;
103
+ onCleanup(() => {
104
+ current.destroy();
105
+ if (instance === current) instance = null;
106
+ });
107
+ }, {
108
+ immediate: true,
109
+ flush: "post"
110
+ });
111
+ watch(() => ({ ...toValue(options) }), (next, previous) => {
112
+ if (instance) update(instance, replaceOptions(previous, next));
113
+ }, { flush: "post" });
114
+ return { current: () => instance };
115
+ }
116
+ //#endregion
117
+ //#region src/composables/useDropZone.ts
118
+ /**
119
+ * Take files dropped on an element. See `createDropZone` in
120
+ * `@tremolo-ui/dom`. Returns whether files are over it, and whether they
121
+ * would be refused, for styling it while they are.
122
+ */
123
+ function useDropZone(target, options = {}) {
124
+ const state = shallowRef({
125
+ over: false,
126
+ invalid: false
127
+ });
128
+ const withState = () => ({
129
+ ...toValue(options),
130
+ onStateChange: (next) => {
131
+ state.value = next;
132
+ toValue(options).onStateChange?.(next);
133
+ }
134
+ });
135
+ useInstance(target, withState, createDropZone, (instance, next) => instance.update(next));
136
+ return state;
137
+ }
138
+ //#endregion
139
+ //#region src/components/DropZone/DropZone.ts
140
+ /** A region that takes dropped files. */
141
+ const DropZone = /* @__PURE__ */ defineComponent({
142
+ name: "DropZone",
143
+ props: {
144
+ /** Which files to take, as the `accept` attribute of a file input. */
145
+ accept: String,
146
+ /** Take every file dropped, rather than the first. */
147
+ multiple: Boolean,
148
+ /** Ignore drops. The zone carries `data-disabled`. */
149
+ disabled: Boolean
150
+ },
151
+ emits: {
152
+ /** The dropped files that satisfy `accept`. */
153
+ drop: (files, _event) => Array.isArray(files),
154
+ /** The dropped files that do not satisfy `accept`, after `drop` for the same drop. */
155
+ reject: (files, _event) => Array.isArray(files)
156
+ },
157
+ setup(props, { slots, emit }) {
158
+ const el = ref(null);
159
+ const state = useDropZone(el, () => ({
160
+ accept: props.accept,
161
+ multiple: props.multiple,
162
+ disabled: props.disabled,
163
+ onDrop: (files, event) => emit("drop", files, event),
164
+ onReject: (files, event) => emit("reject", files, event)
165
+ }));
166
+ return () => h("div", {
167
+ ref: el,
168
+ "data-dragover": state.value.over ? "" : void 0,
169
+ "data-invalid": state.value.invalid ? "" : void 0,
170
+ "data-disabled": props.disabled ? "" : void 0
171
+ }, slots.default?.());
172
+ }
173
+ });
174
+ //#endregion
175
+ //#region src/components/_util/context.ts
176
+ /** Read a component's context, or explain which parent is missing. */
177
+ function injectContext(key, parent) {
178
+ const context = inject(key, null);
179
+ if (!context) throw new Error(`Missing ${parent} in the tree`);
180
+ return context;
181
+ }
182
+ //#endregion
183
+ //#region src/components/FileInput/context.ts
184
+ const FileInputKey = Symbol("FileInput");
185
+ /** The context of the enclosing `FileInput`, for a part of your own. */
186
+ function useFileInputContext() {
187
+ return injectContext(FileInputKey, "FileInput");
188
+ }
189
+ //#endregion
190
+ //#region src/components/FileInput/FileInput.ts
191
+ /**
192
+ * Picks files. The control is the native file input, out of sight but in the
193
+ * tab order, with `FileInputTrigger` as its label.
194
+ */
195
+ const FileInput = /* @__PURE__ */ defineComponent({
196
+ name: "FileInput",
197
+ inheritAttrs: false,
198
+ props: {
199
+ /**
200
+ * Which files to take: extensions, MIME types and type groups. It is
201
+ * checked again when the files arrive, since the picker only takes it as
202
+ * a hint.
203
+ */
204
+ accept: String,
205
+ /** Let several files be picked at once. */
206
+ multiple: Boolean,
207
+ /** Make the input unusable. The parts carry `data-disabled`. */
208
+ disabled: Boolean
209
+ },
210
+ emits: {
211
+ /** The picked files that satisfy `accept`. */
212
+ change: (files) => Array.isArray(files),
213
+ /** The picked files that do not satisfy `accept`, after `change` for the same selection. */
214
+ reject: (files) => Array.isArray(files)
215
+ },
216
+ setup(props, { slots, emit, attrs }) {
217
+ const inputId = useId();
218
+ provide(FileInputKey, {
219
+ inputId,
220
+ get disabled() {
221
+ return props.disabled;
222
+ }
223
+ });
224
+ function onChange(event) {
225
+ const input = event.target;
226
+ const { accepted, rejected } = partitionByAccept(Array.from(input.files ?? []), props.accept);
227
+ input.value = "";
228
+ if (accepted.length > 0) emit("change", accepted);
229
+ if (rejected.length > 0) emit("reject", rejected);
230
+ }
231
+ return () => {
232
+ const { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, ...rest } = attrs;
233
+ return h("div", {
234
+ ...rest,
235
+ "data-disabled": props.disabled ? "" : void 0
236
+ }, [h("input", {
237
+ id: inputId,
238
+ type: "file",
239
+ accept: props.accept,
240
+ multiple: props.multiple,
241
+ disabled: props.disabled,
242
+ style: visuallyHiddenStyle,
243
+ "aria-label": ariaLabel,
244
+ "aria-labelledby": ariaLabelledby,
245
+ "aria-describedby": ariaDescribedby,
246
+ onChange
247
+ }), slots.default?.()]);
248
+ };
249
+ }
250
+ });
251
+ //#endregion
252
+ //#region src/components/FileInput/FileInputTrigger.ts
253
+ /**
254
+ * Opens the file picker and names the input. A `<label>` rather than a
255
+ * button: the browser forwards the click, the input stays the one thing in
256
+ * the tab order, and its name comes from this text.
257
+ */
258
+ const FileInputTrigger = /* @__PURE__ */ defineComponent({
259
+ name: "FileInputTrigger",
260
+ setup(_, { slots }) {
261
+ const fileInput = useFileInputContext();
262
+ return () => h("label", {
263
+ for: fileInput.inputId,
264
+ "data-disabled": fileInput.disabled ? "" : void 0
265
+ }, slots.default?.());
266
+ }
267
+ });
268
+ //#endregion
269
+ //#region src/components/Knob/context.ts
270
+ const KnobKey = Symbol("Knob");
271
+ /** The context of the enclosing `Knob`, for a part of your own. */
272
+ function useKnobContext() {
273
+ return injectContext(KnobKey, "Knob");
274
+ }
275
+ //#endregion
276
+ //#region src/composables/useDragValue.ts
277
+ /**
278
+ * Drive a value with a pointer drag. See `createDragValue` in
279
+ * `@tremolo-ui/dom` for the options; `elementMapping` and `relativeMapping`
280
+ * from there choose how the pointer becomes a value. `mapping` is read when
281
+ * the element is attached.
282
+ */
283
+ function useDragValue(target, options) {
284
+ useInstance(target, options, createDragValue, (instance, next) => instance.update(next));
285
+ }
286
+ //#endregion
287
+ //#region src/composables/useWheel.ts
288
+ /**
289
+ * Listen to the wheel on an element, as a non-passive listener so that
290
+ * `preventDefault()` can keep the page from scrolling. See `createWheel` in
291
+ * `@tremolo-ui/dom`.
292
+ */
293
+ function useWheel(target, onWheel, options = {}) {
294
+ useInstance(target, options, (element, opts) => createWheel(element, (event) => onWheel(event), opts), (instance, next) => instance.update(next));
295
+ }
296
+ //#endregion
297
+ //#region src/components/_util/useCheckSteps.ts
298
+ /**
299
+ * Warn, in development only, when a key press or a wheel notch cannot produce
300
+ * a change the user can see. The probing is `checkSteps` in the core.
301
+ *
302
+ * Takes a getter, so that the check runs again when the settings change.
303
+ */
304
+ function useCheckSteps(options) {
305
+ try {
306
+ if (process.env.NODE_ENV === "production") return;
307
+ watchEffect(() => {
308
+ for (const warning of checkSteps(options())) console.warn(warning);
309
+ });
310
+ } catch {}
311
+ }
312
+ //#endregion
313
+ //#region src/components/Knob/Knob.ts
314
+ /**
315
+ * Interactive rotary knob, drawn in SVG by `KnobSVGRoot` and the parts inside
316
+ * it. Bind the value with `v-model`.
317
+ */
318
+ const Knob = /* @__PURE__ */ defineComponent({
319
+ name: "Knob",
320
+ props: {
321
+ /** The current value. Bind it with `v-model`. */
322
+ modelValue: {
323
+ type: Number,
324
+ required: true
325
+ },
326
+ /** The value with the knob turned all the way down. */
327
+ min: {
328
+ type: Number,
329
+ required: true
330
+ },
331
+ /** The value with the knob turned all the way up. */
332
+ max: {
333
+ type: Number,
334
+ required: true
335
+ },
336
+ /** Granularity of the value. @default 1 */
337
+ step: {
338
+ type: Number,
339
+ default: 1
340
+ },
341
+ /** How the value is distributed across the travel. @default linearScale */
342
+ scale: {
343
+ type: Object,
344
+ default: () => linearScale
345
+ },
346
+ /** The value a double click restores. @default min */
347
+ defaultValue: Number,
348
+ /** Where the active arc starts. @default min */
349
+ startValue: Number,
350
+ /** Width and height of the knob. Sets `--knob-size`. */
351
+ size: [Number, String],
352
+ /** The cursor to show while dragging. @default { cursor: 'grabbing' } */
353
+ externalStyles: Object,
354
+ /** How much one notch of the wheel moves the value. `null` turns it off. */
355
+ wheel: {
356
+ type: [Array, Object],
357
+ default: () => DEFAULT_WHEEL_OPTIONS
358
+ },
359
+ /** How much one arrow key press moves the value. `null` turns it off. */
360
+ keyboard: {
361
+ type: [Array, Object],
362
+ default: () => DEFAULT_KEYBOARD_OPTIONS
363
+ },
364
+ /** How much a drag moves the value, per modifier key. */
365
+ dragSensitivity: {
366
+ type: [Number, Object],
367
+ default: () => DEFAULT_DRAG_SENSITIVITY
368
+ },
369
+ /** Hide the cursor while dragging and read the movement directly. */
370
+ pointerLock: Boolean,
371
+ /** Restore `defaultValue` on a double click. @default true */
372
+ enableDoubleClickDefault: {
373
+ type: Boolean,
374
+ default: true
375
+ },
376
+ /** Make the knob unchangeable and remove it from the tab order. */
377
+ disabled: Boolean,
378
+ /** Make the knob unchangeable while leaving it focusable. */
379
+ readonly: Boolean,
380
+ /** How far the knob turns from `min` to `max`, in degrees. @default 270 */
381
+ angleRange: {
382
+ type: Number,
383
+ default: 270
384
+ }
385
+ },
386
+ emits: { "update:modelValue": (value) => typeof value === "number" },
387
+ setup(props, { slots, emit, expose }) {
388
+ const root = ref(null);
389
+ const dragging = ref(false);
390
+ const inactive = computed(() => props.disabled || props.readonly);
391
+ const range = computed(() => ({
392
+ min: props.min,
393
+ max: props.max,
394
+ step: props.step,
395
+ scale: props.scale
396
+ }));
397
+ const angles = computed(() => knobAngles({
398
+ value: props.modelValue,
399
+ min: props.min,
400
+ max: props.max,
401
+ scale: props.scale,
402
+ startValue: props.startValue ?? props.min,
403
+ angleRange: props.angleRange
404
+ }));
405
+ useCheckSteps(() => ({
406
+ component: "Knob",
407
+ range: range.value,
408
+ keyboard: props.keyboard,
409
+ wheel: props.wheel
410
+ }));
411
+ provide(KnobKey, {
412
+ get value() {
413
+ return props.modelValue;
414
+ },
415
+ get min() {
416
+ return props.min;
417
+ },
418
+ get max() {
419
+ return props.max;
420
+ },
421
+ get step() {
422
+ return props.step;
423
+ },
424
+ get scale() {
425
+ return props.scale;
426
+ },
427
+ get startValue() {
428
+ return props.startValue ?? props.min;
429
+ },
430
+ get angleRange() {
431
+ return props.angleRange;
432
+ },
433
+ get angles() {
434
+ return angles.value;
435
+ }
436
+ });
437
+ const change = (next) => emit("update:modelValue", next);
438
+ useDragValue(root, () => ({
439
+ axis: [range.value, {
440
+ ...range.value,
441
+ reverse: true
442
+ }],
443
+ mapping: relativeMapping({
444
+ pixelRange: 100,
445
+ sensitivity: (state) => selectModifier(props.dragSensitivity, state.event).value
446
+ }),
447
+ getValue: () => [props.modelValue, props.modelValue],
448
+ threshold: 1,
449
+ cursor: inactive.value ? void 0 : props.externalStyles?.cursor ?? "grabbing",
450
+ pointerLock: inactive.value ? false : props.pointerLock,
451
+ shouldStart: () => !inactive.value,
452
+ onChange: (v) => {
453
+ if (!inactive.value) change(v[1]);
454
+ },
455
+ onDragStart: () => {
456
+ dragging.value = true;
457
+ },
458
+ onDragEnd: () => {
459
+ dragging.value = false;
460
+ }
461
+ }));
462
+ useWheel(root, (event) => {
463
+ if (!props.wheel || inactive.value) return;
464
+ const direction = wheelDirection(event);
465
+ if (direction === null) return;
466
+ event.preventDefault();
467
+ change(applyDelta(props.modelValue, direction, props.wheel, range.value, event));
468
+ }, { requireFocus: true });
469
+ expose({
470
+ /** Move the focus to the knob. */
471
+ focus: () => {
472
+ if (!props.disabled) root.value?.focus();
473
+ },
474
+ /** Take the focus away from the knob. */
475
+ blur: () => root.value?.blur()
476
+ });
477
+ return () => h("div", {
478
+ ref: root,
479
+ role: "slider",
480
+ tabindex: props.disabled ? -1 : 0,
481
+ "aria-valuenow": props.modelValue,
482
+ "aria-valuemin": props.min,
483
+ "aria-valuemax": props.max,
484
+ "aria-disabled": props.disabled,
485
+ "aria-readonly": props.readonly,
486
+ "data-disabled": props.disabled ? "" : void 0,
487
+ "data-readonly": props.readonly ? "" : void 0,
488
+ "data-dragging": dragging.value ? "" : void 0,
489
+ style: { "--knob-size": cssLength(props.size) },
490
+ onKeydown: (event) => {
491
+ if (!props.keyboard || inactive.value) return;
492
+ const direction = arrowKeyDirection(event.key);
493
+ if (direction === null) return;
494
+ event.preventDefault();
495
+ change(applyDelta(props.modelValue, direction, props.keyboard, range.value, event));
496
+ },
497
+ onDblclick: () => {
498
+ if (!inactive.value && props.enableDoubleClickDefault) change(props.defaultValue ?? props.min);
499
+ }
500
+ }, slots.default?.());
501
+ }
502
+ });
503
+ //#endregion
504
+ //#region src/components/_util/placement.ts
505
+ const PLACEMENT = Symbol("tremolo-ui placement");
506
+ /**
507
+ * Mark the children of this component as being inside `name`, for
508
+ * {@link checkPlacement}. Call it during `setup`.
509
+ */
510
+ function providePlacement(name) {
511
+ provide(PLACEMENT, name);
512
+ }
513
+ /**
514
+ * Warn, in development only, when `child` is rendered outside `parent`. A
515
+ * part positioned against its parent still renders anywhere else, so nothing
516
+ * fails when it is misplaced — its position just comes out wrong. The names
517
+ * are the ones written in templates (`'KnobThumb'`), since they are what the
518
+ * warning shows. Call it during `setup`.
519
+ */
520
+ function checkPlacement(child, parent) {
521
+ try {
522
+ if (process.env.NODE_ENV === "production") return;
523
+ const found = inject(PLACEMENT, null);
524
+ if (found === parent) return;
525
+ console.warn(`[tremolo-ui] ${child} has to be rendered inside ${parent}` + (found === null ? "." : `, but it is inside ${found}.`) + " It renders either way, so nothing fails — its position just comes out wrong.");
526
+ } catch {}
527
+ }
528
+ //#endregion
529
+ //#region src/components/Knob/KnobActiveLine.ts
530
+ /** The arc between `startValue` and the value, as an SVG `<path>`. */
531
+ const KnobActiveLine = /* @__PURE__ */ defineComponent({
532
+ name: "KnobActiveLine",
533
+ props: {
534
+ stroke: {
535
+ type: String,
536
+ default: "currentColor"
537
+ },
538
+ strokeWidth: {
539
+ type: [Number, String],
540
+ default: 6
541
+ }
542
+ },
543
+ setup(props) {
544
+ checkPlacement("KnobActiveLine", "KnobSVGRoot");
545
+ const knob = useKnobContext();
546
+ return () => h("path", {
547
+ d: knobArcPath(knob.angles.r2, knob.angles.r3, knobArcRadius(props.strokeWidth)),
548
+ fill: "none",
549
+ stroke: props.stroke,
550
+ "stroke-width": props.strokeWidth
551
+ });
552
+ }
553
+ });
554
+ //#endregion
555
+ //#region src/components/Knob/KnobInactiveLine.ts
556
+ /**
557
+ * The travel outside the value, as SVG `<path>`s: one on each side of the
558
+ * active arc that has any length.
559
+ */
560
+ const KnobInactiveLine = /* @__PURE__ */ defineComponent({
561
+ name: "KnobInactiveLine",
562
+ inheritAttrs: false,
563
+ props: {
564
+ stroke: {
565
+ type: String,
566
+ default: "currentColor"
567
+ },
568
+ strokeWidth: {
569
+ type: [Number, String],
570
+ default: 6
571
+ }
572
+ },
573
+ setup(props, { attrs }) {
574
+ checkPlacement("KnobInactiveLine", "KnobSVGRoot");
575
+ const knob = useKnobContext();
576
+ const path = (from, to) => h("path", {
577
+ ...attrs,
578
+ d: knobArcPath(from, to, knobArcRadius(props.strokeWidth)),
579
+ fill: "none",
580
+ stroke: props.stroke,
581
+ "stroke-width": props.strokeWidth
582
+ });
583
+ return () => [knob.startValue > knob.min ? path(knob.angles.r1, knob.angles.r2) : null, knob.startValue < knob.max ? path(knob.angles.r3, knob.angles.r4) : null];
584
+ }
585
+ });
586
+ //#endregion
587
+ //#region src/components/Knob/KnobSVGRoot.ts
588
+ /**
589
+ * The `<svg>` the drawing parts go in, in the order you want them painted.
590
+ * It sets the view box they are drawn against.
591
+ */
592
+ const KnobSVGRoot = /* @__PURE__ */ defineComponent({
593
+ name: "KnobSVGRoot",
594
+ setup(_, { slots }) {
595
+ providePlacement("KnobSVGRoot");
596
+ return () => h("svg", {
597
+ viewBox: `0 0 ${KNOB_VIEWBOX_SIZE} ${KNOB_VIEWBOX_SIZE}`,
598
+ style: { display: "block" }
599
+ }, slots.default?.());
600
+ }
601
+ });
602
+ //#endregion
603
+ //#region src/components/Knob/KnobThumb.ts
604
+ /** The circle that turns with the value, and the line on it that points at it. */
605
+ const KnobThumb = /* @__PURE__ */ defineComponent({
606
+ name: "KnobThumb",
607
+ props: {
608
+ /** Fill colour of the circle. @default 'currentColor' */
609
+ thumb: {
610
+ type: String,
611
+ default: "currentColor"
612
+ },
613
+ /** Colour of the line that points at the value. @default 'currentColor' */
614
+ thumbLine: {
615
+ type: String,
616
+ default: "currentColor"
617
+ },
618
+ /** Diameter of the circle, as a percentage of the knob. @default 84 */
619
+ thumbSize: {
620
+ type: Number,
621
+ default: 84
622
+ },
623
+ /** Thickness of the line, as a percentage of the knob. @default 6 */
624
+ thumbLineWeight: {
625
+ type: Number,
626
+ default: 6
627
+ },
628
+ /** How far down the line reaches, as a percentage. @default 35 */
629
+ thumbLineLength: {
630
+ type: Number,
631
+ default: 35
632
+ },
633
+ /** Classes for the line that points at the value. */
634
+ classes: Object
635
+ },
636
+ setup(props) {
637
+ checkPlacement("KnobThumb", "KnobSVGRoot");
638
+ const knob = useKnobContext();
639
+ return () => h("svg", null, [h("circle", {
640
+ cx: "50%",
641
+ cy: "50%",
642
+ r: `${props.thumbSize / 2}%`,
643
+ fill: props.thumb
644
+ }), h("line", {
645
+ class: props.classes?.thumbLine,
646
+ x1: "50%",
647
+ y1: `${(KNOB_VIEWBOX_SIZE - clamp(props.thumbSize, 0, 100)) / 2}%`,
648
+ x2: "50%",
649
+ y2: `${props.thumbLineLength}%`,
650
+ stroke: props.thumbLine,
651
+ "stroke-width": `${props.thumbLineWeight}%`,
652
+ style: {
653
+ transform: `rotate(${knob.angles.r1 + knob.angles.p * knob.angleRange}deg)`,
654
+ transformOrigin: "50% 50%"
655
+ }
656
+ })]);
657
+ }
658
+ });
659
+ //#endregion
660
+ //#region src/components/NumberInput/context.ts
661
+ const NumberInputKey = Symbol("NumberInput");
662
+ /** The context of the enclosing `NumberInput`, for a part of your own. */
663
+ function useNumberInputContext() {
664
+ return injectContext(NumberInputKey, "NumberInput");
665
+ }
666
+ /** @internal Whether the drag on `NumberInputStepper` has moved the value. */
667
+ const StepperKey = Symbol("NumberInputStepper");
668
+ //#endregion
669
+ //#region src/components/_util/props.ts
670
+ /** How much one notch of the wheel moves the value. `null` turns it off. */
671
+ const wheelProp = {
672
+ type: [Array, Object],
673
+ default: () => DEFAULT_WHEEL_OPTIONS
674
+ };
675
+ /** How much one arrow key press moves the value. `null` turns it off. */
676
+ const keyboardProp = {
677
+ type: [Array, Object],
678
+ default: () => DEFAULT_KEYBOARD_OPTIONS
679
+ };
680
+ /** How much a drag moves the value, per modifier key. */
681
+ const dragSensitivityProp = {
682
+ type: [Number, Object],
683
+ default: () => DEFAULT_DRAG_SENSITIVITY
684
+ };
685
+ //#endregion
686
+ //#region src/components/NumberInput/NumberInput.ts
687
+ /** Input with some useful functions for entering numbers. Bind it with `v-model`. */
688
+ const NumberInput = /* @__PURE__ */ defineComponent({
689
+ name: "NumberInput",
690
+ props: {
691
+ /** The current value. Bind it with `v-model`. */
692
+ modelValue: {
693
+ type: Number,
694
+ required: true
695
+ },
696
+ /** The lowest value. Leave it out for no lower end. */
697
+ min: Number,
698
+ /** The highest value. Leave it out for no upper end. */
699
+ max: Number,
700
+ /** Granularity of the value. @default 1 */
701
+ step: {
702
+ type: Number,
703
+ default: 1
704
+ },
705
+ /** How the value is distributed, for a `normalized` amount. */
706
+ scale: {
707
+ type: Object,
708
+ default: () => linearScale
709
+ },
710
+ /** The text shown for a value. @default String */
711
+ format: {
712
+ type: Function,
713
+ default: String
714
+ },
715
+ /** Read a value back out of the text. Has to undo `format`. */
716
+ parse: {
717
+ type: Function,
718
+ default: parseNumberText
719
+ },
720
+ /** Keep the value within `min` and `max` when committed or stepped. */
721
+ clampValue: {
722
+ type: Boolean,
723
+ default: true
724
+ },
725
+ wheel: wheelProp,
726
+ keyboard: keyboardProp,
727
+ dragSensitivity: dragSensitivityProp,
728
+ /** Pixels of vertical drag on the stepper per `step`. `null` turns it off. */
729
+ drag: {
730
+ type: Number,
731
+ default: 1
732
+ },
733
+ /** Hide the cursor while dragging the stepper. */
734
+ pointerLock: Boolean,
735
+ /** Select the text when the field takes focus. @default 'none' */
736
+ selectOnFocus: {
737
+ type: String,
738
+ default: "none"
739
+ },
740
+ /** Show the plain value while the field has focus. */
741
+ unformatOnFocus: Boolean,
742
+ /** Put the caret back at the same digit after an arrow key step. */
743
+ keepCaretOnStep: Boolean,
744
+ /** Commit and leave the field on Enter. @default true */
745
+ blurOnEnter: {
746
+ type: Boolean,
747
+ default: true
748
+ },
749
+ /** Make the input unchangeable and remove it from the tab order. */
750
+ disabled: Boolean,
751
+ /** Make the value unchangeable while leaving the field focusable. */
752
+ readonly: Boolean
753
+ },
754
+ emits: { "update:modelValue": (value) => typeof value === "number" },
755
+ setup(props, { slots, emit, expose }) {
756
+ const root = ref(null);
757
+ /**
758
+ * The text being typed. The only state here: not a copy of the value, but
759
+ * the half-finished entry that has no value to be derived from yet.
760
+ */
761
+ const draft = ref(null);
762
+ let input = null;
763
+ const inactive = computed(() => props.disabled || props.readonly);
764
+ const ranges = computed(() => numberInputRanges({
765
+ min: props.min,
766
+ max: props.max,
767
+ step: props.step,
768
+ scale: props.scale,
769
+ clampValue: props.clampValue
770
+ }));
771
+ const bounds = computed(() => numberInputBounds(props.modelValue, {
772
+ min: props.min,
773
+ max: props.max,
774
+ clampValue: props.clampValue
775
+ }));
776
+ const text = computed(() => draft.value ?? props.format(props.modelValue));
777
+ useCheckSteps(() => ({
778
+ component: "NumberInput",
779
+ range: props.min !== void 0 && props.max !== void 0 && props.min < props.max ? {
780
+ min: props.min,
781
+ max: props.max,
782
+ step: props.step,
783
+ scale: props.scale
784
+ } : null,
785
+ keyboard: props.keyboard,
786
+ wheel: props.wheel,
787
+ format: props.format
788
+ }));
789
+ function changeValue(next) {
790
+ if (inactive.value) return;
791
+ draft.value = null;
792
+ if (next !== props.modelValue) emit("update:modelValue", next);
793
+ }
794
+ provide(NumberInputKey, {
795
+ get value() {
796
+ return props.modelValue;
797
+ },
798
+ get min() {
799
+ return props.min;
800
+ },
801
+ get max() {
802
+ return props.max;
803
+ },
804
+ get step() {
805
+ return props.step;
806
+ },
807
+ get disabled() {
808
+ return props.disabled;
809
+ },
810
+ get readonly() {
811
+ return props.readonly;
812
+ },
813
+ get keyboard() {
814
+ return props.keyboard;
815
+ },
816
+ get drag() {
817
+ return props.drag;
818
+ },
819
+ get dragSensitivity() {
820
+ return props.dragSensitivity;
821
+ },
822
+ get pointerLock() {
823
+ return props.pointerLock;
824
+ },
825
+ get selectOnFocus() {
826
+ return props.selectOnFocus;
827
+ },
828
+ get unformatOnFocus() {
829
+ return props.unformatOnFocus;
830
+ },
831
+ get keepCaretOnStep() {
832
+ return props.keepCaretOnStep;
833
+ },
834
+ get blurOnEnter() {
835
+ return props.blurOnEnter;
836
+ },
837
+ get text() {
838
+ return text.value;
839
+ },
840
+ get editing() {
841
+ return draft.value !== null;
842
+ },
843
+ get outOfRange() {
844
+ return draft.value === null && bounds.value.outOfRange;
845
+ },
846
+ get atMin() {
847
+ return bounds.value.atMin;
848
+ },
849
+ get atMax() {
850
+ return bounds.value.atMax;
851
+ },
852
+ get rawRange() {
853
+ return ranges.value.raw;
854
+ },
855
+ setDraft: (next) => {
856
+ if (inactive.value) return;
857
+ draft.value = next;
858
+ const parsed = props.parse(next);
859
+ if (Number.isFinite(parsed)) emit("update:modelValue", parsed);
860
+ },
861
+ commitDraft: () => {
862
+ if (draft.value === null || inactive.value) return;
863
+ const committed = commitNumberInputText(draft.value, props.parse, {
864
+ min: props.min,
865
+ max: props.max,
866
+ clampValue: props.clampValue
867
+ });
868
+ if (committed === null) draft.value = null;
869
+ else changeValue(committed);
870
+ },
871
+ changeValue,
872
+ nudge: (direction, option, modifiers) => changeValue(nudgeNumberInput(props.modelValue, direction, option, ranges.value, modifiers)),
873
+ setInput: (element) => {
874
+ input = element;
875
+ }
876
+ });
877
+ useWheel(root, (event) => {
878
+ const direction = wheelDirection(event);
879
+ if (!props.wheel || inactive.value || direction === null) return;
880
+ event.preventDefault();
881
+ changeValue(nudgeNumberInput(props.modelValue, direction, props.wheel, ranges.value, event));
882
+ }, { requireFocus: true });
883
+ expose({
884
+ focus: () => {
885
+ if (!props.disabled) input?.focus();
886
+ },
887
+ blur: () => input?.blur()
888
+ });
889
+ return () => h("div", {
890
+ ref: root,
891
+ "data-disabled": props.disabled ? "" : void 0,
892
+ "data-readonly": props.readonly ? "" : void 0
893
+ }, slots.default?.());
894
+ }
895
+ });
896
+ //#endregion
897
+ //#region src/composables/useLongPress.ts
898
+ /**
899
+ * Repeat `onPress` while a pointer is held down: once on the press, then
900
+ * every `interval` after `delay`. See `createLongPress` in `@tremolo-ui/dom`.
901
+ *
902
+ * Returns the function that starts a press, for `@pointerdown`.
903
+ */
904
+ function useLongPress(options) {
905
+ const instance = createLongPress(toValue(options));
906
+ watch(() => toValue(options), (next) => instance.update(next));
907
+ onScopeDispose(() => instance.destroy());
908
+ return (event) => instance.start(event);
909
+ }
910
+ //#endregion
911
+ //#region src/components/NumberInput/stepper-button.ts
912
+ /** The body of `NumberInputIncrementStepper` and `NumberInputDecrementStepper`. */
913
+ function stepperButton(name, direction) {
914
+ return defineComponent({
915
+ name,
916
+ setup(_, { slots }) {
917
+ const field = useNumberInputContext();
918
+ const stepper = inject(StepperKey, null);
919
+ const blocked = computed(() => field.disabled || (direction > 0 ? field.atMax : field.atMin));
920
+ const press = useLongPress({ onPress: () => {
921
+ if (field.disabled || field.readonly) return;
922
+ if (stepper?.moved()) return;
923
+ field.nudge(direction, ["raw", field.step]);
924
+ } });
925
+ return () => h("div", {
926
+ role: "button",
927
+ tabindex: -1,
928
+ "aria-label": direction > 0 ? "Increment" : "Decrement",
929
+ "aria-disabled": blocked.value,
930
+ "data-disabled": blocked.value ? "" : void 0,
931
+ "data-readonly": field.readonly ? "" : void 0,
932
+ onPointerdown: (event) => press(event)
933
+ }, slots.default ? slots.default() : h("svg", {
934
+ style: {
935
+ width: "var(--stepper-icon-size)",
936
+ height: "var(--stepper-icon-size)"
937
+ },
938
+ viewBox: "0 0 24 24",
939
+ fill: "none",
940
+ stroke: "currentColor",
941
+ "stroke-width": 2,
942
+ "stroke-linecap": "round",
943
+ "stroke-linejoin": "round"
944
+ }, h("polyline", { points: direction > 0 ? "18 15 12 9 6 15" : "6 9 12 15 18 9" })));
945
+ }
946
+ });
947
+ }
948
+ //#endregion
949
+ //#region src/components/NumberInput/NumberInputDecrementStepper.ts
950
+ /** Lowers the value by one `step`, repeating while held. */
951
+ const NumberInputDecrementStepper = /* @__PURE__ */ stepperButton("NumberInputDecrementStepper", -1);
952
+ //#endregion
953
+ //#region src/components/NumberInput/NumberInputField.ts
954
+ /**
955
+ * The text field, and the only place the value can be typed. While the user
956
+ * types, their own text stands rather than `format(value)`.
957
+ */
958
+ const NumberInputField = /* @__PURE__ */ defineComponent({
959
+ name: "NumberInputField",
960
+ setup(_, { expose }) {
961
+ const field = useNumberInputContext();
962
+ const input = ref(null);
963
+ const focused = ref(false);
964
+ watchEffect(() => field.setInput(input.value));
965
+ onBeforeUnmount(() => field.setInput(null));
966
+ const shown = computed(() => field.unformatOnFocus && focused.value && !field.editing ? String(field.value) : field.text);
967
+ async function select() {
968
+ if (field.selectOnFocus === "none") return;
969
+ await nextTick();
970
+ const element = input.value;
971
+ if (!element || !focused.value) return;
972
+ const span = field.selectOnFocus === "all" ? {
973
+ start: 0,
974
+ end: element.value.length
975
+ } : numberSpan(element.value);
976
+ if (span) element.setSelectionRange(span.start, span.end);
977
+ }
978
+ async function step(direction, event) {
979
+ const element = event.currentTarget;
980
+ const from = element.value;
981
+ const offset = field.keepCaretOnStep && element.selectionStart !== null ? caretDecimalOffset(from, element.selectionStart) : null;
982
+ field.nudge(direction, field.keyboard, event);
983
+ if (offset === null) return;
984
+ await nextTick();
985
+ if (element.value === from) return;
986
+ const caret = caretAtDecimalOffset(element.value, offset);
987
+ element.setSelectionRange(caret, caret);
988
+ }
989
+ expose({
990
+ focus: () => input.value?.focus(),
991
+ blur: () => input.value?.blur()
992
+ });
993
+ return () => h("input", {
994
+ ref: input,
995
+ type: "text",
996
+ inputmode: "decimal",
997
+ role: "spinbutton",
998
+ value: shown.value,
999
+ disabled: field.disabled,
1000
+ readonly: field.readonly,
1001
+ "aria-disabled": field.disabled,
1002
+ "aria-readonly": field.readonly,
1003
+ "aria-valuenow": field.value,
1004
+ "aria-valuemin": field.min,
1005
+ "aria-valuemax": field.max,
1006
+ "aria-valuetext": field.text,
1007
+ step: field.step,
1008
+ "data-disabled": field.disabled ? "" : void 0,
1009
+ "data-readonly": field.readonly ? "" : void 0,
1010
+ "data-out-of-range": field.outOfRange ? "" : void 0,
1011
+ onInput: (event) => field.setDraft(event.target.value),
1012
+ onFocus: () => {
1013
+ focused.value = true;
1014
+ select();
1015
+ },
1016
+ onBlur: () => {
1017
+ focused.value = false;
1018
+ field.commitDraft();
1019
+ },
1020
+ onKeydown: (event) => {
1021
+ const key = event.key;
1022
+ if (key === "Enter") {
1023
+ field.commitDraft();
1024
+ if (field.blurOnEnter) event.target.blur();
1025
+ } else if (field.keyboard && !field.disabled && !field.readonly && (key === "ArrowUp" || key === "ArrowDown") && !event.isComposing) {
1026
+ event.preventDefault();
1027
+ step(key === "ArrowUp" ? 1 : -1, event);
1028
+ }
1029
+ }
1030
+ });
1031
+ }
1032
+ });
1033
+ //#endregion
1034
+ //#region src/components/NumberInput/NumberInputIncrementStepper.ts
1035
+ /** Raises the value by one `step`, repeating while held. */
1036
+ const NumberInputIncrementStepper = /* @__PURE__ */ stepperButton("NumberInputIncrementStepper", 1);
1037
+ //#endregion
1038
+ //#region src/components/NumberInput/NumberInputStepper.ts
1039
+ /**
1040
+ * The area the steppers sit in, and a drag handle in its own right: dragging
1041
+ * it up and down moves the value one `step` every `drag` pixels.
1042
+ */
1043
+ const NumberInputStepper = /* @__PURE__ */ defineComponent({
1044
+ name: "NumberInputStepper",
1045
+ setup(_, { slots }) {
1046
+ const field = useNumberInputContext();
1047
+ const el = ref(null);
1048
+ let instance = null;
1049
+ const enabled = computed(() => field.drag !== null && !field.disabled && !field.readonly);
1050
+ const options = () => ({
1051
+ range: field.rawRange,
1052
+ pixels: field.drag ?? 1,
1053
+ sensitivity: field.dragSensitivity,
1054
+ pointerLock: field.pointerLock
1055
+ });
1056
+ watch([el, enabled], ([element, on], _, onCleanup) => {
1057
+ if (!element || !on) return;
1058
+ const current = createStepperDrag(element, {
1059
+ ...options(),
1060
+ getValue: () => field.value,
1061
+ onChange: (next) => field.changeValue(next)
1062
+ });
1063
+ instance = current;
1064
+ onCleanup(() => {
1065
+ current.destroy();
1066
+ if (instance === current) instance = null;
1067
+ });
1068
+ }, {
1069
+ immediate: true,
1070
+ flush: "post"
1071
+ });
1072
+ watch(options, (next) => instance?.update(next));
1073
+ provide(StepperKey, { moved: () => instance?.moved() ?? false });
1074
+ return () => h("div", { ref: el }, slots.default?.());
1075
+ }
1076
+ });
1077
+ //#endregion
1078
+ //#region src/components/Piano/Piano.ts
1079
+ /**
1080
+ * Customizable piano. The keys are drawn by the component; what goes inside
1081
+ * one is the `label` slot, which receives the note and its state.
1082
+ */
1083
+ const Piano = /* @__PURE__ */ defineComponent({
1084
+ name: "Piano",
1085
+ props: {
1086
+ /** The notes drawn, from `first` to `last`. */
1087
+ noteRange: {
1088
+ type: Object,
1089
+ required: true
1090
+ },
1091
+ /** Let a pointer slide from one key to the next. @default true */
1092
+ glissando: {
1093
+ type: Boolean,
1094
+ default: true
1095
+ },
1096
+ /** Highest note that can sound. @default 127 */
1097
+ midiMax: {
1098
+ type: Number,
1099
+ default: 127
1100
+ },
1101
+ /** Play notes from the computer keyboard. See `SHORTCUTS` in `@tremolo-ui/dom`. */
1102
+ keyboardShortcuts: Object,
1103
+ /** Where keyboard shortcuts listen. @default 'root' */
1104
+ keyboardShortcutsScope: {
1105
+ type: String,
1106
+ default: "root"
1107
+ },
1108
+ /** Follow the width of the parent element. */
1109
+ resizable: Boolean,
1110
+ /** Width of a white key in pixels. @default 40 */
1111
+ whiteKeyWidth: {
1112
+ type: Number,
1113
+ default: 40
1114
+ },
1115
+ /** Space between two white keys, in pixels. @default 1 */
1116
+ keyGap: {
1117
+ type: Number,
1118
+ default: 1
1119
+ },
1120
+ /** Width of a black key, as a fraction of a white one. @default 0.65 */
1121
+ blackKeyWidthRatio: {
1122
+ type: Number,
1123
+ default: .65
1124
+ },
1125
+ /** Height of a black key, as a fraction of the keyboard. @default 0.6 */
1126
+ blackKeyHeightRatio: {
1127
+ type: Number,
1128
+ default: .6
1129
+ },
1130
+ /** Classes for the label inside each key. */
1131
+ classes: Object,
1132
+ /**
1133
+ * Extra attributes for one key, by note. The geometry of the key is
1134
+ * applied after the returned style and cannot be overridden.
1135
+ */
1136
+ keyProps: Function
1137
+ },
1138
+ emits: {
1139
+ /** A note started sounding. */
1140
+ playNote: (note, _velocity) => typeof note === "number",
1141
+ /** Everything holding a note let go of it. */
1142
+ stopNote: (note) => typeof note === "number"
1143
+ },
1144
+ slots: Object,
1145
+ setup(props, { slots, emit, expose }) {
1146
+ const root = ref(null);
1147
+ const activeNotes = ref([]);
1148
+ /** Set while `resizable` is on, measured from the parent. */
1149
+ const resizedKeyWidth = ref(null);
1150
+ const notes = computed(() => getNoteRangeArray(props.noteRange));
1151
+ const layout = computed(() => ({
1152
+ noteRange: props.noteRange,
1153
+ whiteKeyWidth: props.resizable ? resizedKeyWidth.value ?? props.whiteKeyWidth : props.whiteKeyWidth,
1154
+ keyGap: props.keyGap,
1155
+ blackKeyWidthRatio: props.blackKeyWidthRatio,
1156
+ blackKeyHeightRatio: props.blackKeyHeightRatio
1157
+ }));
1158
+ const settings = () => ({
1159
+ layout: layout.value,
1160
+ glissando: props.glissando,
1161
+ midiMax: props.midiMax,
1162
+ keyboardShortcuts: props.keyboardShortcuts,
1163
+ keyboardShortcutsScope: props.keyboardShortcutsScope
1164
+ });
1165
+ let instance = null;
1166
+ watch(root, (element, _, onCleanup) => {
1167
+ if (!element) return;
1168
+ const current = createPianoInput(element, {
1169
+ ...settings(),
1170
+ onPlayNote: (note, velocity) => emit("playNote", note, velocity),
1171
+ onStopNote: (note) => emit("stopNote", note),
1172
+ onActiveNotesChange: (next) => {
1173
+ activeNotes.value = next;
1174
+ }
1175
+ });
1176
+ instance = current;
1177
+ onCleanup(() => {
1178
+ instance = null;
1179
+ current.destroy();
1180
+ });
1181
+ }, {
1182
+ immediate: true,
1183
+ flush: "post"
1184
+ });
1185
+ watch(settings, (next) => instance?.update(next));
1186
+ watch(() => [
1187
+ root.value,
1188
+ props.resizable,
1189
+ props.noteRange.first,
1190
+ props.noteRange.last,
1191
+ props.keyGap,
1192
+ props.blackKeyWidthRatio
1193
+ ], ([element, resizable, first, last, keyGap, blackKeyWidthRatio], _, onCleanup) => {
1194
+ if (!resizable || !element) return;
1195
+ const parent = element.parentElement;
1196
+ if (!parent) throw new Error("doesn't have a parent element.");
1197
+ const observer = new ResizeObserver(() => {
1198
+ resizedKeyWidth.value = fitWhiteKeyWidth(element.clientWidth, {
1199
+ noteRange: {
1200
+ first,
1201
+ last
1202
+ },
1203
+ keyGap,
1204
+ blackKeyWidthRatio
1205
+ });
1206
+ });
1207
+ observer.observe(parent);
1208
+ onCleanup(() => observer.disconnect());
1209
+ }, {
1210
+ immediate: true,
1211
+ flush: "post"
1212
+ });
1213
+ expose({
1214
+ /** Start a note, as a key would. */
1215
+ playNote: (note, velocity) => instance?.noteOn(note, {
1216
+ source: "api",
1217
+ velocity
1218
+ }),
1219
+ /** Release a note started with `playNote`. */
1220
+ stopNote: (note) => instance?.noteOff(note, { source: "api" })
1221
+ });
1222
+ return () => h("div", {
1223
+ ref: root,
1224
+ role: "group",
1225
+ tabindex: 0,
1226
+ "data-resizable": props.resizable ? "" : void 0,
1227
+ style: {
1228
+ position: "relative",
1229
+ width: props.resizable ? "100%" : `${pianoWidth(layout.value)}px`
1230
+ }
1231
+ }, notes.value.map((note, index) => {
1232
+ const state = {
1233
+ index,
1234
+ keyType: isWhiteKey(note) ? "white" : "black",
1235
+ active: activeNotes.value.includes(note),
1236
+ disabled: note > props.midiMax
1237
+ };
1238
+ const { style: keyStyle, ...attributes } = props.keyProps?.(note, state) ?? {};
1239
+ const white = state.keyType === "white";
1240
+ const label = slots.label?.({
1241
+ note,
1242
+ state
1243
+ });
1244
+ return h("div", {
1245
+ key: note,
1246
+ "data-note": note,
1247
+ "data-note-key": noteKey(note),
1248
+ "data-active": state.active ? "" : void 0,
1249
+ "data-disabled": state.disabled ? "" : void 0,
1250
+ ...attributes,
1251
+ style: [
1252
+ {
1253
+ position: "absolute",
1254
+ zIndex: white ? 1 : 2
1255
+ },
1256
+ keyStyle,
1257
+ {
1258
+ left: `${notePosition(note, layout.value)}px`,
1259
+ width: `${white ? layout.value.whiteKeyWidth : blackKeyWidth(layout.value)}px`,
1260
+ height: white ? "100%" : `${props.blackKeyHeightRatio * 100}%`
1261
+ }
1262
+ ]
1263
+ }, label ? h("div", { class: props.classes?.keyLabelWrapper }, [h("div", { class: props.classes?.keyLabel }, label)]) : void 0);
1264
+ }));
1265
+ }
1266
+ });
1267
+ //#endregion
1268
+ //#region src/components/PointsEditor/context.ts
1269
+ const PointsEditorKey = Symbol("PointsEditor");
1270
+ /** The context of the enclosing `PointsEditor`, for a part of your own. */
1271
+ function usePointsEditorContext() {
1272
+ return injectContext(PointsEditorKey, "PointsEditor");
1273
+ }
1274
+ //#endregion
1275
+ //#region src/components/PointsEditor/PointsEditor.ts
1276
+ const EMPTY = [];
1277
+ /**
1278
+ * Multiple point controller. Bind the selection with `v-model:selection` to
1279
+ * hold it yourself; the editor keeps its own otherwise.
1280
+ */
1281
+ const PointsEditor = /* @__PURE__ */ defineComponent({
1282
+ name: "PointsEditor",
1283
+ props: {
1284
+ /** Make the points unchangeable and remove them from the tab order. */
1285
+ disabled: Boolean,
1286
+ /** Make the points unmovable. */
1287
+ readonly: Boolean,
1288
+ /** The cursor to show while dragging a point. @default { cursor: 'grabbing' } */
1289
+ externalStyles: Object,
1290
+ /** How much one wheel notch moves the focused point. */
1291
+ wheel: {
1292
+ type: [Array, Object],
1293
+ default: () => POINTS_EDITOR_DEFAULT_WHEEL
1294
+ },
1295
+ /** How much one arrow key press moves a point. */
1296
+ keyboard: {
1297
+ type: [Array, Object],
1298
+ default: () => POINTS_EDITOR_DEFAULT_KEYBOARD
1299
+ },
1300
+ /** How much a drag moves a point, per modifier key. */
1301
+ dragSensitivity: {
1302
+ type: [Number, Object],
1303
+ default: () => DEFAULT_DRAG_SENSITIVITY
1304
+ },
1305
+ /**
1306
+ * Let points be selected, and a selection be moved as one. A selection
1307
+ * updates several points in the same tick.
1308
+ */
1309
+ selectable: Boolean,
1310
+ /** Ids of the selected points. Bind it with `v-model:selection`. */
1311
+ selection: Array
1312
+ },
1313
+ emits: { "update:selection": (selection) => Array.isArray(selection) },
1314
+ setup(props, { slots, emit }) {
1315
+ const ownSelection = shallowRef(props.selection ?? []);
1316
+ const selectionBox = shallowRef(null);
1317
+ const container = shallowRef(null);
1318
+ const current = computed(() => props.selectable ? props.selection ?? ownSelection.value : EMPTY);
1319
+ const editor = createPointsEditor({
1320
+ onSelectionBoxChange: (rect) => {
1321
+ selectionBox.value = rect;
1322
+ },
1323
+ onSelectionChange: (next) => {
1324
+ ownSelection.value = next;
1325
+ emit("update:selection", next);
1326
+ }
1327
+ });
1328
+ watchEffect(() => editor.update({
1329
+ selectable: props.selectable,
1330
+ selection: current.value
1331
+ }), { flush: "sync" });
1332
+ onScopeDispose(() => editor.destroy());
1333
+ provide(PointsEditorKey, {
1334
+ get disabled() {
1335
+ return props.disabled;
1336
+ },
1337
+ get readonly() {
1338
+ return props.readonly;
1339
+ },
1340
+ get wheel() {
1341
+ return props.wheel;
1342
+ },
1343
+ get keyboard() {
1344
+ return props.keyboard;
1345
+ },
1346
+ get dragSensitivity() {
1347
+ return props.dragSensitivity;
1348
+ },
1349
+ get cursor() {
1350
+ return props.externalStyles?.cursor ?? "grabbing";
1351
+ },
1352
+ get selectable() {
1353
+ return props.selectable;
1354
+ },
1355
+ get selection() {
1356
+ return current.value;
1357
+ },
1358
+ get selectionBox() {
1359
+ return selectionBox.value;
1360
+ },
1361
+ get container() {
1362
+ return container.value;
1363
+ },
1364
+ editor,
1365
+ setContainer: (element) => {
1366
+ container.value = element;
1367
+ }
1368
+ });
1369
+ return () => h("div", {
1370
+ "data-disabled": props.disabled ? "" : void 0,
1371
+ "data-readonly": props.readonly ? "" : void 0,
1372
+ style: { position: "relative" }
1373
+ }, slots.default?.());
1374
+ }
1375
+ });
1376
+ //#endregion
1377
+ //#region src/components/PointsEditor/PointsEditorBackground.ts
1378
+ /**
1379
+ * Behind the points, filling the editor. The order of the layers holds
1380
+ * whichever way they are written, so the z-index is part of the mechanics.
1381
+ */
1382
+ const PointsEditorBackground = /* @__PURE__ */ defineComponent({
1383
+ name: "PointsEditorBackground",
1384
+ setup(_, { slots }) {
1385
+ return () => h("div", { style: {
1386
+ position: "absolute",
1387
+ inset: 0,
1388
+ zIndex: 0
1389
+ } }, slots.default?.());
1390
+ }
1391
+ });
1392
+ //#endregion
1393
+ //#region src/components/PointsEditor/PointsEditorContainer.ts
1394
+ /** The layer the points are placed in, over the background. */
1395
+ const PointsEditorContainer = /* @__PURE__ */ defineComponent({
1396
+ name: "PointsEditorContainer",
1397
+ setup(_, { slots }) {
1398
+ const points = usePointsEditorContext();
1399
+ providePlacement("PointsEditorContainer");
1400
+ const el = ref(null);
1401
+ watchEffect(() => points.setContainer(el.value));
1402
+ const selecting = computed(() => points.selectable && !points.disabled);
1403
+ watch([el, selecting], ([element, on], _, onCleanup) => {
1404
+ if (!element || !on) return;
1405
+ const drag = createDragValue(element, {
1406
+ axis: POINT_AXIS,
1407
+ mapping: elementMapping(() => element),
1408
+ shouldStart: (event) => !points.editor.isPointElement(event.target),
1409
+ onDragStart: ([x, y], state) => points.editor.beginSelectionBox({
1410
+ x,
1411
+ y
1412
+ }, state.event),
1413
+ onChange: ([x, y]) => points.editor.moveSelectionBox({
1414
+ x,
1415
+ y
1416
+ }),
1417
+ onDragEnd: () => points.editor.endSelectionBox()
1418
+ });
1419
+ onCleanup(() => drag.destroy());
1420
+ }, {
1421
+ immediate: true,
1422
+ flush: "post"
1423
+ });
1424
+ useWheel(el, (event) => {
1425
+ const move = wheelMove(event);
1426
+ if (!move) return;
1427
+ const axis = move.axis === 0 ? "x" : "y";
1428
+ if (points.editor.nudgeFocusedPoint(axis, move.direction, event)) event.preventDefault();
1429
+ });
1430
+ return () => h("div", {
1431
+ ref: el,
1432
+ style: {
1433
+ position: "absolute",
1434
+ inset: 0,
1435
+ zIndex: 10
1436
+ }
1437
+ }, slots.default?.());
1438
+ }
1439
+ });
1440
+ //#endregion
1441
+ //#region src/components/_util/visually-hidden-range-input.ts
1442
+ /**
1443
+ * The control underneath a headless part: out of sight, but in the
1444
+ * accessibility tree and the tab order, carrying the ARIA and the keyboard.
1445
+ *
1446
+ * A plain element rather than a component, so that a `ref` on it reaches the
1447
+ * `<input>` itself.
1448
+ */
1449
+ function visuallyHiddenRangeInput(props) {
1450
+ return h("input", {
1451
+ ...props,
1452
+ type: "range",
1453
+ style: visuallyHiddenStyle
1454
+ });
1455
+ }
1456
+ //#endregion
1457
+ //#region src/components/PointsEditor/PointsEditorPoint.ts
1458
+ const perAxis = (setting, axis) => typeof setting === "string" ? setting : setting?.[axis];
1459
+ /** One point, bound with `v-model` as `{ x, y }` from 0 to 1. */
1460
+ const PointsEditorPoint = /* @__PURE__ */ defineComponent({
1461
+ name: "PointsEditorPoint",
1462
+ inheritAttrs: false,
1463
+ props: {
1464
+ /** Where the point is, with `y` growing downwards. Bind it with `v-model`. */
1465
+ modelValue: {
1466
+ type: Object,
1467
+ required: true
1468
+ },
1469
+ /** How the selection refers to this point. One is generated when left out. */
1470
+ id: String,
1471
+ /** The lowest position the point can take, per axis. */
1472
+ min: Object,
1473
+ /** The highest position the point can take, per axis. */
1474
+ max: Object,
1475
+ /** Sets `--color`, for the theme to colour the point with. */
1476
+ color: String,
1477
+ /** Overrides the `disabled` of `PointsEditor`. */
1478
+ disabled: {
1479
+ type: Boolean,
1480
+ default: void 0
1481
+ },
1482
+ /** Overrides the `readonly` of `PointsEditor`. */
1483
+ readonly: {
1484
+ type: Boolean,
1485
+ default: void 0
1486
+ },
1487
+ /** Overrides the `wheel` of `PointsEditor`; `null` turns it off. */
1488
+ wheel: {
1489
+ type: [Array, Object],
1490
+ default: void 0
1491
+ },
1492
+ /** Overrides the `keyboard` of `PointsEditor`; `null` turns it off. */
1493
+ keyboard: {
1494
+ type: [Array, Object],
1495
+ default: void 0
1496
+ },
1497
+ /** The accessible name of each axis, or one for both. */
1498
+ ariaLabel: [String, Object],
1499
+ /** What the value of each axis means, or one for both. */
1500
+ ariaValuetext: [String, Object]
1501
+ },
1502
+ emits: {
1503
+ "update:modelValue": (value) => typeof value === "object",
1504
+ dragStart: (value) => typeof value === "object",
1505
+ dragEnd: (value) => typeof value === "object"
1506
+ },
1507
+ setup(props, { slots, attrs, emit }) {
1508
+ const points = usePointsEditorContext();
1509
+ checkPlacement("PointsEditorPoint", "PointsEditorContainer");
1510
+ const generatedId = useId();
1511
+ const id = computed(() => props.id ?? generatedId);
1512
+ const disabled = computed(() => props.disabled ?? points.disabled);
1513
+ const readonly = computed(() => props.readonly ?? points.readonly);
1514
+ const inactive = computed(() => disabled.value || readonly.value);
1515
+ const wheel = computed(() => props.wheel === void 0 ? points.wheel : props.wheel);
1516
+ const keyboard = computed(() => props.keyboard === void 0 ? points.keyboard : props.keyboard);
1517
+ const current = computed(() => clampPoint(props.modelValue, props.min, props.max));
1518
+ const el = ref(null);
1519
+ const x = ref(null);
1520
+ const dragging = ref(false);
1521
+ const read = () => ({
1522
+ value: props.modelValue,
1523
+ min: props.min,
1524
+ max: props.max,
1525
+ readonly: inactive.value,
1526
+ onChange: (next) => emit("update:modelValue", next),
1527
+ element: el.value,
1528
+ wheel: wheel.value
1529
+ });
1530
+ watch(id, (key, _, onCleanup) => onCleanup(points.editor.registerPoint(key, read)), { immediate: true });
1531
+ /** Where the pointer was when the drag started. */
1532
+ let origin = null;
1533
+ watch(el, (element, _, onCleanup) => {
1534
+ if (!element) return;
1535
+ const drag = createDragValue(element, {
1536
+ axis: POINT_AXIS,
1537
+ mapping: elementMapping(() => points.container, { sensitivity: (state) => selectModifier(points.dragSensitivity, state.event).value }),
1538
+ cursor: inactive.value ? void 0 : points.cursor,
1539
+ shouldStart: () => !disabled.value,
1540
+ onChange: ([px, py]) => {
1541
+ if (origin) points.editor.movePointDrag({
1542
+ x: px - origin.x,
1543
+ y: py - origin.y
1544
+ });
1545
+ },
1546
+ onDragStart: ([px, py], state) => {
1547
+ points.editor.beginPointDrag(id.value, state.event);
1548
+ origin = {
1549
+ x: px,
1550
+ y: py
1551
+ };
1552
+ dragging.value = true;
1553
+ x.value?.focus();
1554
+ if (!inactive.value) emit("dragStart", current.value);
1555
+ },
1556
+ onDragEnd: () => {
1557
+ origin = null;
1558
+ dragging.value = false;
1559
+ if (!inactive.value) emit("dragEnd", current.value);
1560
+ }
1561
+ });
1562
+ const stop = watch(() => inactive.value ? void 0 : points.cursor, (cursor) => drag.update({ cursor }));
1563
+ onCleanup(() => {
1564
+ stop();
1565
+ drag.destroy();
1566
+ });
1567
+ }, {
1568
+ immediate: true,
1569
+ flush: "post"
1570
+ });
1571
+ function onInput(axis, target) {
1572
+ if (readonly.value) {
1573
+ target.value = String(current.value[axis]);
1574
+ return;
1575
+ }
1576
+ const delta = target.valueAsNumber - props.modelValue[axis];
1577
+ points.editor.nudgeSelection(id.value, {
1578
+ x: axis === "x" ? delta : 0,
1579
+ y: axis === "y" ? delta : 0
1580
+ });
1581
+ }
1582
+ const input = (axis) => visuallyHiddenRangeInput({
1583
+ ref: axis === "x" ? x : void 0,
1584
+ "data-axis": axis,
1585
+ value: current.value[axis],
1586
+ min: props.min?.[axis] ?? 0,
1587
+ max: props.max?.[axis] ?? 1,
1588
+ step: "any",
1589
+ disabled: disabled.value,
1590
+ "aria-readonly": readonly.value,
1591
+ "aria-orientation": axis === "x" ? "horizontal" : "vertical",
1592
+ "aria-label": perAxis(props.ariaLabel, axis) ?? axis,
1593
+ "aria-valuetext": perAxis(props.ariaValuetext, axis),
1594
+ onInput: (event) => onInput(axis, event.target)
1595
+ });
1596
+ return () => {
1597
+ const { style, onFocus, onKeydown, ...rest } = attrs;
1598
+ return h("div", {
1599
+ ref: el,
1600
+ tabindex: -1,
1601
+ "data-disabled": disabled.value ? "" : void 0,
1602
+ "data-readonly": readonly.value ? "" : void 0,
1603
+ "data-dragging": dragging.value ? "" : void 0,
1604
+ "data-selected": points.selection.includes(id.value) ? "" : void 0,
1605
+ ...rest,
1606
+ style: [
1607
+ {
1608
+ position: "absolute",
1609
+ translate: "var(--translate, -50% -50%)",
1610
+ "--color": props.color
1611
+ },
1612
+ style,
1613
+ {
1614
+ left: `${props.modelValue.x * 100}%`,
1615
+ top: `${props.modelValue.y * 100}%`
1616
+ }
1617
+ ],
1618
+ ...mergeProps({
1619
+ onFocus: (event) => {
1620
+ if (!disabled.value && event.target === event.currentTarget) x.value?.focus();
1621
+ },
1622
+ onKeydown: (event) => {
1623
+ const move = arrowKeyMove(event.key);
1624
+ if (!move) return;
1625
+ event.preventDefault();
1626
+ if (inactive.value || !keyboard.value) return;
1627
+ points.editor.nudgePoint(id.value, move.axis === 0 ? "x" : "y", move.direction, keyboard.value, event);
1628
+ }
1629
+ }, {
1630
+ onFocus,
1631
+ onKeydown
1632
+ })
1633
+ }, [
1634
+ input("x"),
1635
+ input("y"),
1636
+ slots.default?.()
1637
+ ]);
1638
+ };
1639
+ }
1640
+ });
1641
+ //#endregion
1642
+ //#region src/components/PointsEditor/PointsEditorSelectionBox.ts
1643
+ /**
1644
+ * The box a drag on empty space draws. Nothing is rendered while no drag is
1645
+ * running, and it takes none of the pointer.
1646
+ */
1647
+ const PointsEditorSelectionBox = /* @__PURE__ */ defineComponent({
1648
+ name: "PointsEditorSelectionBox",
1649
+ setup(_, { slots }) {
1650
+ const points = usePointsEditorContext();
1651
+ checkPlacement("PointsEditorSelectionBox", "PointsEditorContainer");
1652
+ return () => {
1653
+ const box = points.selectionBox;
1654
+ if (!box) return null;
1655
+ return h("div", { style: {
1656
+ position: "absolute",
1657
+ zIndex: 20,
1658
+ pointerEvents: "none",
1659
+ left: `${box.x * 100}%`,
1660
+ top: `${box.y * 100}%`,
1661
+ width: `${box.width * 100}%`,
1662
+ height: `${box.height * 100}%`
1663
+ } }, slots.default?.());
1664
+ };
1665
+ }
1666
+ });
1667
+ //#endregion
1668
+ //#region src/components/Slider/context.ts
1669
+ const SliderKey = Symbol("Slider");
1670
+ /** The context of the enclosing `Slider`, for a part of your own. */
1671
+ function useSliderContext() {
1672
+ return injectContext(SliderKey, "Slider");
1673
+ }
1674
+ //#endregion
1675
+ //#region src/components/Slider/Slider.ts
1676
+ /** Customizable slider. Bind the value with `v-model`. */
1677
+ const Slider = /* @__PURE__ */ defineComponent({
1678
+ name: "Slider",
1679
+ props: {
1680
+ /** The current value. Bind it with `v-model`. */
1681
+ modelValue: {
1682
+ type: Number,
1683
+ required: true
1684
+ },
1685
+ /** The value at the start of the travel. */
1686
+ min: {
1687
+ type: Number,
1688
+ required: true
1689
+ },
1690
+ /** The value at the end of the travel. */
1691
+ max: {
1692
+ type: Number,
1693
+ required: true
1694
+ },
1695
+ /** Granularity of the value. @default 1 */
1696
+ step: {
1697
+ type: Number,
1698
+ default: 1
1699
+ },
1700
+ /** How the value is distributed across the travel. @default linearScale */
1701
+ scale: {
1702
+ type: Object,
1703
+ default: () => linearScale
1704
+ },
1705
+ /** Run the slider vertically, with the value growing upwards. */
1706
+ vertical: Boolean,
1707
+ /** Grow the value the other way. */
1708
+ reverse: Boolean,
1709
+ /** The cursor to show while dragging. @default { cursor: 'pointer' } */
1710
+ externalStyles: Object,
1711
+ wheel: wheelProp,
1712
+ keyboard: keyboardProp,
1713
+ dragSensitivity: dragSensitivityProp,
1714
+ /** Make the slider unchangeable and remove it from the tab order. */
1715
+ disabled: Boolean,
1716
+ /** Make the value unchangeable. */
1717
+ readonly: Boolean
1718
+ },
1719
+ emits: {
1720
+ "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"
1725
+ },
1726
+ setup(props, { slots, emit, expose }) {
1727
+ const root = ref(null);
1728
+ let track = null;
1729
+ let thumb = null;
1730
+ const inactive = computed(() => props.disabled || props.readonly);
1731
+ const displayReversed = computed(() => props.vertical !== props.reverse);
1732
+ const percent = computed(() => valuePercent(props.modelValue, {
1733
+ min: props.min,
1734
+ max: props.max,
1735
+ scale: props.scale
1736
+ }, displayReversed.value));
1737
+ const axis = computed(() => ({
1738
+ min: props.min,
1739
+ max: props.max,
1740
+ step: props.step,
1741
+ scale: props.scale,
1742
+ reverse: displayReversed.value
1743
+ }));
1744
+ useCheckSteps(() => ({
1745
+ component: "Slider",
1746
+ range: axis.value,
1747
+ keyboard: props.keyboard,
1748
+ wheel: props.wheel
1749
+ }));
1750
+ const change = (next) => emit("update:modelValue", next);
1751
+ provide(SliderKey, {
1752
+ get value() {
1753
+ return props.modelValue;
1754
+ },
1755
+ get min() {
1756
+ return props.min;
1757
+ },
1758
+ get max() {
1759
+ return props.max;
1760
+ },
1761
+ get step() {
1762
+ return props.step;
1763
+ },
1764
+ get scale() {
1765
+ return props.scale;
1766
+ },
1767
+ get vertical() {
1768
+ return props.vertical;
1769
+ },
1770
+ get reverse() {
1771
+ return props.reverse;
1772
+ },
1773
+ get disabled() {
1774
+ return props.disabled;
1775
+ },
1776
+ get readonly() {
1777
+ return props.readonly;
1778
+ },
1779
+ get percent() {
1780
+ return percent.value;
1781
+ },
1782
+ change,
1783
+ setTrack: (element) => {
1784
+ track = element;
1785
+ },
1786
+ setThumb: (input) => {
1787
+ thumb = input;
1788
+ }
1789
+ });
1790
+ const valueOf = (v) => v[props.vertical ? 1 : 0];
1791
+ useDragValue(root, () => ({
1792
+ axis: axis.value,
1793
+ mapping: elementMapping(() => track, { sensitivity: (state) => selectModifier(props.dragSensitivity, state.event).value }),
1794
+ cursor: inactive.value ? void 0 : props.externalStyles?.cursor ?? "pointer",
1795
+ shouldStart: () => !inactive.value,
1796
+ updateOnPointerDown: true,
1797
+ onChange: (v) => {
1798
+ if (!inactive.value) change(valueOf(v));
1799
+ },
1800
+ onDragStart: (v) => {
1801
+ if (inactive.value) return;
1802
+ thumb?.focus();
1803
+ emit("dragStart", valueOf(v));
1804
+ },
1805
+ onDragEnd: (v) => {
1806
+ if (!inactive.value) emit("dragEnd", valueOf(v));
1807
+ }
1808
+ }));
1809
+ useWheel(root, (event) => {
1810
+ if (!props.wheel || inactive.value) return;
1811
+ const direction = wheelDirection(event, { horizontal: !props.vertical });
1812
+ if (direction === null) return;
1813
+ event.preventDefault();
1814
+ change(applyDelta(props.modelValue, props.reverse ? -direction : direction, props.wheel, axis.value, event));
1815
+ }, { requireFocus: true });
1816
+ expose({
1817
+ /** Move the focus to the thumb. */
1818
+ focus: () => {
1819
+ if (!props.disabled) thumb?.focus();
1820
+ },
1821
+ /** Take the focus away from the thumb. */
1822
+ blur: () => thumb?.blur()
1823
+ });
1824
+ return () => h("div", {
1825
+ ref: root,
1826
+ role: "group",
1827
+ tabindex: -1,
1828
+ "data-orientation": props.vertical ? "vertical" : "horizontal",
1829
+ "data-disabled": props.disabled ? "" : void 0,
1830
+ "data-readonly": props.readonly ? "" : void 0,
1831
+ onKeydown: (event) => {
1832
+ const direction = arrowKeyDirection(event.key);
1833
+ if (direction === null) return;
1834
+ event.preventDefault();
1835
+ if (!props.keyboard || inactive.value) return;
1836
+ change(applyDelta(props.modelValue, props.reverse ? -direction : direction, props.keyboard, axis.value, event));
1837
+ },
1838
+ onFocus: () => {
1839
+ if (!props.disabled) thumb?.focus();
1840
+ },
1841
+ onBlur: () => thumb?.blur()
1842
+ }, slots.default?.());
1843
+ }
1844
+ });
1845
+ //#endregion
1846
+ //#region src/components/Slider/SliderMarksOption.ts
1847
+ /** One mark along the slider, and its label. */
1848
+ const SliderMarksOption = /* @__PURE__ */ defineComponent({
1849
+ name: "SliderMarksOption",
1850
+ props: {
1851
+ /** Where the mark sits, on the same scale as the thumb. */
1852
+ value: {
1853
+ type: Number,
1854
+ required: true
1855
+ },
1856
+ /** Draw the mark itself. @default true */
1857
+ mark: {
1858
+ type: Boolean,
1859
+ default: true
1860
+ },
1861
+ /**
1862
+ * Text shown next to the mark. Leave it out to show the value; `null`
1863
+ * leaves the label out.
1864
+ */
1865
+ label: {
1866
+ type: [
1867
+ Number,
1868
+ String,
1869
+ null
1870
+ ],
1871
+ default: void 0
1872
+ },
1873
+ /** Thickness of the mark. Sets `--thickness`. */
1874
+ thickness: [Number, String],
1875
+ /** Length of the mark. Sets `--length`. */
1876
+ length: [Number, String],
1877
+ /** Space between the mark and the label. Sets `--gap`. */
1878
+ gap: [Number, String],
1879
+ /** Classes for the mark and the label inside the option. */
1880
+ classes: Object,
1881
+ /** Styles for the mark and the label inside the option. */
1882
+ styles: Object
1883
+ },
1884
+ setup(props) {
1885
+ const slider = useSliderContext();
1886
+ checkPlacement("SliderMarksOption", "SliderMarks");
1887
+ const percent = computed(() => valuePercent(props.value, {
1888
+ min: slider.min,
1889
+ max: slider.max,
1890
+ scale: slider.scale
1891
+ }, slider.vertical !== slider.reverse));
1892
+ return () => {
1893
+ const orientation = slider.vertical ? "vertical" : "horizontal";
1894
+ return h("div", {
1895
+ "data-orientation": orientation,
1896
+ style: {
1897
+ "--thickness": cssLength(props.thickness),
1898
+ "--length": cssLength(props.length),
1899
+ "--gap": cssLength(props.gap),
1900
+ position: "absolute",
1901
+ translate: slider.vertical ? "var(--translate, 0 -50%)" : "var(--translate, -50% 0)",
1902
+ zIndex: 10,
1903
+ left: slider.vertical ? void 0 : `${percent.value}%`,
1904
+ top: slider.vertical ? `${percent.value}%` : void 0
1905
+ }
1906
+ }, [props.mark ? h("div", {
1907
+ class: props.classes?.mark,
1908
+ style: props.styles?.mark,
1909
+ "data-orientation": orientation
1910
+ }) : null, props.label !== null ? h("div", {
1911
+ class: props.classes?.label,
1912
+ style: props.styles?.label
1913
+ }, String(props.label ?? props.value)) : null]);
1914
+ };
1915
+ }
1916
+ });
1917
+ //#endregion
1918
+ //#region src/components/Slider/SliderMarks.ts
1919
+ /** The marks along the slider: written out as options, or built from `options`. */
1920
+ const SliderMarks = /* @__PURE__ */ defineComponent({
1921
+ name: "SliderMarks",
1922
+ props: {
1923
+ /** Space between the marks and the track. Sets `--gap`. */
1924
+ gap: [Number, String],
1925
+ /**
1926
+ * Build the marks instead of writing `SliderMarksOption` out: a number
1927
+ * puts one every that many, `'step'` one every `step`. The default slot
1928
+ * is ignored while it is set.
1929
+ */
1930
+ options: [
1931
+ String,
1932
+ Number,
1933
+ Object
1934
+ ]
1935
+ },
1936
+ setup(props, { slots }) {
1937
+ const slider = useSliderContext();
1938
+ providePlacement("SliderMarks");
1939
+ const marks = computed(() => {
1940
+ if (props.options === void 0) return [];
1941
+ const list = sliderMarks(props.options, slider.min, slider.max, slider.step);
1942
+ return slider.vertical !== slider.reverse ? list.reverse() : list;
1943
+ });
1944
+ return () => h("div", {
1945
+ "data-orientation": slider.vertical ? "vertical" : "horizontal",
1946
+ style: {
1947
+ "--gap": cssLength(props.gap),
1948
+ position: "relative"
1949
+ }
1950
+ }, props.options !== void 0 ? marks.value.map(({ value, mark, label }) => h(SliderMarksOption, {
1951
+ key: value,
1952
+ value,
1953
+ mark,
1954
+ label: label ? void 0 : null
1955
+ })) : slots.default?.());
1956
+ }
1957
+ });
1958
+ //#endregion
1959
+ //#region src/components/Slider/SliderThumb.ts
1960
+ /**
1961
+ * The thumb, placed at the value. The ARIA (`aria-label` and friends) lands
1962
+ * on the range input inside it, which is the control and takes the focus.
1963
+ */
1964
+ const SliderThumb = /* @__PURE__ */ defineComponent({
1965
+ name: "SliderThumb",
1966
+ inheritAttrs: false,
1967
+ props: {
1968
+ /** Sets `--color`, for the theme to colour the thumb with. */
1969
+ color: String },
1970
+ setup(props, { slots, attrs, expose }) {
1971
+ const slider = useSliderContext();
1972
+ checkPlacement("SliderThumb", "SliderTrack");
1973
+ const input = ref(null);
1974
+ watchEffect(() => slider.setThumb(input.value));
1975
+ onBeforeUnmount(() => slider.setThumb(null));
1976
+ expose({
1977
+ focus: () => {
1978
+ if (!slider.disabled) input.value?.focus();
1979
+ },
1980
+ blur: () => input.value?.blur()
1981
+ });
1982
+ return () => {
1983
+ const { "aria-label": ariaLabel, "aria-labelledby": ariaLabelledby, "aria-describedby": ariaDescribedby, "aria-valuetext": ariaValuetext, style, ...rest } = attrs;
1984
+ return h("div", {
1985
+ "data-disabled": slider.disabled ? "" : void 0,
1986
+ "data-readonly": slider.readonly ? "" : void 0,
1987
+ ...rest,
1988
+ style: [
1989
+ {
1990
+ position: "absolute",
1991
+ translate: "var(--translate, -50% -50%)",
1992
+ zIndex: 100,
1993
+ "--color": props.color
1994
+ },
1995
+ style,
1996
+ {
1997
+ top: slider.vertical ? `${slider.percent}%` : "50%",
1998
+ left: slider.vertical ? "50%" : `${slider.percent}%`
1999
+ }
2000
+ ]
2001
+ }, [visuallyHiddenRangeInput({
2002
+ ref: input,
2003
+ value: slider.value,
2004
+ min: slider.min,
2005
+ max: slider.max,
2006
+ step: slider.step,
2007
+ disabled: slider.disabled,
2008
+ "aria-readonly": slider.readonly,
2009
+ "aria-orientation": slider.vertical ? "vertical" : "horizontal",
2010
+ "aria-label": ariaLabel,
2011
+ "aria-labelledby": ariaLabelledby,
2012
+ "aria-describedby": ariaDescribedby,
2013
+ "aria-valuetext": ariaValuetext,
2014
+ onInput: (event) => {
2015
+ const target = event.target;
2016
+ if (slider.readonly) {
2017
+ target.value = String(slider.value);
2018
+ return;
2019
+ }
2020
+ slider.change(target.valueAsNumber);
2021
+ }
2022
+ }), slots.default?.()]);
2023
+ };
2024
+ }
2025
+ });
2026
+ //#endregion
2027
+ //#region src/components/Slider/SliderTrack.ts
2028
+ /** The track the thumb runs along, and what the pointer is measured against. */
2029
+ const SliderTrack = /* @__PURE__ */ defineComponent({
2030
+ name: "SliderTrack",
2031
+ props: {
2032
+ /** How long the track is along the axis the slider runs. Sets `--length`. */
2033
+ length: [Number, String],
2034
+ /** How thick the track is across that axis. Sets `--thickness`. */
2035
+ 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
2040
+ },
2041
+ setup(props, { slots }) {
2042
+ const slider = useSliderContext();
2043
+ providePlacement("SliderTrack");
2044
+ const el = ref(null);
2045
+ watchEffect(() => slider.setTrack(el.value));
2046
+ onBeforeUnmount(() => slider.setTrack(null));
2047
+ return () => h("div", {
2048
+ ref: el,
2049
+ "data-disabled": slider.disabled ? "" : void 0,
2050
+ "data-orientation": slider.vertical ? "vertical" : "horizontal",
2051
+ "data-flipped": slider.vertical !== slider.reverse ? "" : void 0,
2052
+ style: {
2053
+ "--active": props.active,
2054
+ "--inactive": props.inactive,
2055
+ "--length": cssLength(props.length),
2056
+ "--thickness": cssLength(props.thickness),
2057
+ "--percent": `${slider.percent}%`,
2058
+ position: "relative"
2059
+ }
2060
+ }, slots.default?.());
2061
+ }
2062
+ });
2063
+ //#endregion
2064
+ //#region src/components/XYPad/context.ts
2065
+ const XYPadKey = Symbol("XYPad");
2066
+ /** The context of the enclosing `XYPad`, for a part of your own. */
2067
+ function useXYPadContext() {
2068
+ return injectContext(XYPadKey, "XYPad");
2069
+ }
2070
+ //#endregion
2071
+ //#region src/components/XYPad/XYPad.ts
2072
+ /**
2073
+ * Two-dimensional slider. The per-axis settings are `[x, y]` tuples, and a
2074
+ * plain value applies to both axes. Bind the value with `v-model`.
2075
+ */
2076
+ const XYPad = /* @__PURE__ */ defineComponent({
2077
+ name: "XYPad",
2078
+ props: {
2079
+ /** The current value as `[x, y]`. Bind it with `v-model`. */
2080
+ modelValue: {
2081
+ type: Array,
2082
+ required: true
2083
+ },
2084
+ /** The value at the start of the travel, per axis. */
2085
+ min: {
2086
+ type: [Number, Array],
2087
+ required: true
2088
+ },
2089
+ /** The value at the end of the travel, per axis. */
2090
+ max: {
2091
+ type: [Number, Array],
2092
+ required: true
2093
+ },
2094
+ /** Granularity of the value, per axis. @default 1 */
2095
+ step: {
2096
+ type: [Number, Array],
2097
+ default: 1
2098
+ },
2099
+ /** How the value is distributed, per axis. @default linearScale */
2100
+ scale: {
2101
+ type: [Object, Array],
2102
+ default: () => linearScale
2103
+ },
2104
+ /** Grow the value the other way, per axis. `y` grows downwards unless reversed. */
2105
+ reverse: {
2106
+ type: [Boolean, Array],
2107
+ default: false
2108
+ },
2109
+ /** The cursor to show while dragging. @default { cursor: 'pointer' } */
2110
+ externalStyles: Object,
2111
+ wheel: wheelProp,
2112
+ keyboard: keyboardProp,
2113
+ dragSensitivity: dragSensitivityProp,
2114
+ /** Make the pad unchangeable and remove it from the tab order. */
2115
+ disabled: Boolean,
2116
+ /** Make the value unchangeable. */
2117
+ readonly: Boolean
2118
+ },
2119
+ emits: {
2120
+ "update:modelValue": (value) => Array.isArray(value),
2121
+ dragStart: (value) => Array.isArray(value),
2122
+ dragEnd: (value) => Array.isArray(value)
2123
+ },
2124
+ setup(props, { slots, emit, expose }) {
2125
+ const root = ref(null);
2126
+ let area = null;
2127
+ let thumb = null;
2128
+ const inactive = computed(() => props.disabled || props.readonly);
2129
+ const min = computed(() => toXY(props.min));
2130
+ const max = computed(() => toXY(props.max));
2131
+ const step = computed(() => toXY(props.step));
2132
+ const scale = computed(() => toXY(props.scale));
2133
+ const reverse = computed(() => toXY(props.reverse));
2134
+ const percent = computed(() => [0, 1].map((i) => valuePercent(props.modelValue[i], {
2135
+ min: min.value[i],
2136
+ max: max.value[i],
2137
+ scale: scale.value[i]
2138
+ }, reverse.value[i])));
2139
+ const axis = computed(() => [0, 1].map((i) => ({
2140
+ min: min.value[i],
2141
+ max: max.value[i],
2142
+ step: step.value[i],
2143
+ scale: scale.value[i],
2144
+ reverse: reverse.value[i]
2145
+ })));
2146
+ for (const [i, name] of [[0, "x"], [1, "y"]]) useCheckSteps(() => ({
2147
+ component: "XYPad",
2148
+ axis: name,
2149
+ range: axis.value[i],
2150
+ keyboard: props.keyboard,
2151
+ wheel: props.wheel
2152
+ }));
2153
+ const change = (next) => emit("update:modelValue", next);
2154
+ /** Move one axis by one press of `option`, in screen coordinates. */
2155
+ function nudge({ axis: i, direction }, option, modifiers) {
2156
+ const value = props.modelValue;
2157
+ const next = applyDelta(value[i], reverse.value[i] ? -direction : direction, option, axis.value[i], modifiers);
2158
+ change(i === 0 ? [next, value[1]] : [value[0], next]);
2159
+ }
2160
+ provide(XYPadKey, {
2161
+ get value() {
2162
+ return props.modelValue;
2163
+ },
2164
+ get min() {
2165
+ return min.value;
2166
+ },
2167
+ get max() {
2168
+ return max.value;
2169
+ },
2170
+ get step() {
2171
+ return step.value;
2172
+ },
2173
+ get scale() {
2174
+ return scale.value;
2175
+ },
2176
+ get reverse() {
2177
+ return reverse.value;
2178
+ },
2179
+ get disabled() {
2180
+ return props.disabled;
2181
+ },
2182
+ get readonly() {
2183
+ return props.readonly;
2184
+ },
2185
+ get percent() {
2186
+ return percent.value;
2187
+ },
2188
+ change,
2189
+ setArea: (element) => {
2190
+ area = element;
2191
+ },
2192
+ setThumb: (input) => {
2193
+ thumb = input;
2194
+ }
2195
+ });
2196
+ useDragValue(root, () => ({
2197
+ axis: axis.value,
2198
+ mapping: elementMapping(() => area, { sensitivity: (state) => selectModifier(props.dragSensitivity, state.event).value }),
2199
+ updateOnPointerDown: true,
2200
+ cursor: inactive.value ? void 0 : props.externalStyles?.cursor ?? "pointer",
2201
+ shouldStart: () => !inactive.value,
2202
+ onChange: (v) => {
2203
+ if (!inactive.value) change(v);
2204
+ },
2205
+ onDragStart: (v) => {
2206
+ if (inactive.value) return;
2207
+ thumb?.focus();
2208
+ emit("dragStart", v);
2209
+ },
2210
+ onDragEnd: (v) => {
2211
+ if (!inactive.value) emit("dragEnd", v);
2212
+ }
2213
+ }));
2214
+ useWheel(root, (event) => {
2215
+ if (!props.wheel || inactive.value) return;
2216
+ const move = wheelMove(event);
2217
+ if (!move) return;
2218
+ event.preventDefault();
2219
+ nudge(move, props.wheel, event);
2220
+ }, { requireFocus: true });
2221
+ expose({
2222
+ focus: () => {
2223
+ if (!props.disabled) thumb?.focus();
2224
+ },
2225
+ blur: () => thumb?.blur()
2226
+ });
2227
+ return () => h("div", {
2228
+ ref: root,
2229
+ role: "group",
2230
+ tabindex: -1,
2231
+ "data-disabled": props.disabled ? "" : void 0,
2232
+ "data-readonly": props.readonly ? "" : void 0,
2233
+ onKeydown: (event) => {
2234
+ const move = arrowKeyMove(event.key);
2235
+ if (!move) return;
2236
+ event.preventDefault();
2237
+ if (props.keyboard && !inactive.value) nudge(move, props.keyboard, event);
2238
+ },
2239
+ onFocus: () => {
2240
+ if (!props.disabled) thumb?.focus();
2241
+ },
2242
+ onBlur: () => thumb?.blur()
2243
+ }, slots.default?.());
2244
+ }
2245
+ });
2246
+ //#endregion
2247
+ //#region src/components/XYPad/XYPadArea.ts
2248
+ /** The area the thumb moves in, and what the pointer is measured against. */
2249
+ const XYPadArea = /* @__PURE__ */ defineComponent({
2250
+ name: "XYPadArea",
2251
+ setup(_, { slots }) {
2252
+ const pad = useXYPadContext();
2253
+ providePlacement("XYPadArea");
2254
+ const el = ref(null);
2255
+ watchEffect(() => pad.setArea(el.value));
2256
+ onBeforeUnmount(() => pad.setArea(null));
2257
+ return () => h("div", {
2258
+ ref: el,
2259
+ style: { position: "relative" }
2260
+ }, slots.default?.());
2261
+ }
2262
+ });
2263
+ //#endregion
2264
+ //#region src/components/XYPad/XYPadThumb.ts
2265
+ /** The thumb, placed at the value, with one range input per axis inside. */
2266
+ const XYPadThumb = /* @__PURE__ */ defineComponent({
2267
+ name: "XYPadThumb",
2268
+ inheritAttrs: false,
2269
+ props: {
2270
+ /** Sets `--color`, for the theme to colour the thumb with. */
2271
+ color: String,
2272
+ /** The accessible name of each axis, as `[x, y]`. */
2273
+ ariaLabel: [String, Array],
2274
+ /** The ids of what labels each axis, as `[x, y]` or one for both. */
2275
+ ariaLabelledby: [String, Array],
2276
+ /** The ids of what describes each axis, as `[x, y]` or one for both. */
2277
+ ariaDescribedby: [String, Array],
2278
+ /** What the value of each axis means, as `[x, y]` or one for both. */
2279
+ ariaValuetext: [String, Array]
2280
+ },
2281
+ setup(props, { slots, attrs, expose }) {
2282
+ const pad = useXYPadContext();
2283
+ checkPlacement("XYPadThumb", "XYPadArea");
2284
+ const x = ref(null);
2285
+ const y = ref(null);
2286
+ watchEffect(() => pad.setThumb(x.value));
2287
+ onBeforeUnmount(() => pad.setThumb(null));
2288
+ expose({
2289
+ focus: () => {
2290
+ if (!pad.disabled) x.value?.focus();
2291
+ },
2292
+ blur: () => {
2293
+ x.value?.blur();
2294
+ y.value?.blur();
2295
+ }
2296
+ });
2297
+ function input(axis) {
2298
+ const labels = toXY(props.ariaLabel);
2299
+ return visuallyHiddenRangeInput({
2300
+ ref: axis === 0 ? x : y,
2301
+ "data-axis": axis === 0 ? "x" : "y",
2302
+ value: pad.value[axis],
2303
+ min: pad.min[axis],
2304
+ max: pad.max[axis],
2305
+ step: pad.step[axis],
2306
+ disabled: pad.disabled,
2307
+ "aria-readonly": pad.readonly,
2308
+ "aria-orientation": axis === 0 ? "horizontal" : "vertical",
2309
+ "aria-label": labels[axis] ?? (axis === 0 ? "x" : "y"),
2310
+ "aria-labelledby": toXY(props.ariaLabelledby)[axis],
2311
+ "aria-describedby": toXY(props.ariaDescribedby)[axis],
2312
+ "aria-valuetext": toXY(props.ariaValuetext)[axis],
2313
+ onInput: (event) => {
2314
+ const target = event.target;
2315
+ if (pad.readonly) {
2316
+ target.value = String(pad.value[axis]);
2317
+ return;
2318
+ }
2319
+ const next = [...pad.value];
2320
+ next[axis] = target.valueAsNumber;
2321
+ pad.change(next);
2322
+ }
2323
+ });
2324
+ }
2325
+ return () => {
2326
+ const { style, ...rest } = attrs;
2327
+ return h("div", {
2328
+ "data-disabled": pad.disabled ? "" : void 0,
2329
+ "data-readonly": pad.readonly ? "" : void 0,
2330
+ ...rest,
2331
+ style: [
2332
+ {
2333
+ position: "absolute",
2334
+ translate: "var(--translate, -50% -50%)",
2335
+ zIndex: 100,
2336
+ "--color": props.color
2337
+ },
2338
+ style,
2339
+ {
2340
+ left: `${pad.percent[0]}%`,
2341
+ top: `${pad.percent[1]}%`
2342
+ }
2343
+ ]
2344
+ }, [
2345
+ input(0),
2346
+ input(1),
2347
+ slots.default?.()
2348
+ ]);
2349
+ };
2350
+ }
2351
+ });
2352
+ //#endregion
2353
+ //#region src/composables/useDrag.ts
2354
+ /**
2355
+ * Track a pointer drag on an element. See `createDrag` in `@tremolo-ui/dom`
2356
+ * for the options.
2357
+ *
2358
+ * @example
2359
+ * const el = useTemplateRef('el')
2360
+ * useDrag(el, { onDrag: (state) => (x.value += state.deltaX) })
2361
+ */
2362
+ function useDrag(target, options = {}) {
2363
+ useInstance(target, options, createDrag, (instance, next) => instance.update(next));
2364
+ }
2365
+ //#endregion
2366
+ //#region src/composables/useMIDIAccess.ts
2367
+ /**
2368
+ * Request MIDI access in the browser. Call it during `setup`.
2369
+ *
2370
+ * `state` holds the access, the error and the connected inputs, and follows
2371
+ * devices as they are plugged in and unplugged.
2372
+ *
2373
+ * @param requestOnMount ask for access once the component is mounted — never
2374
+ * during server rendering, where there is no MIDI to ask for. Leave it off to
2375
+ * ask from a click instead, so that opening a page does not bring up a
2376
+ * permission prompt. A ref or a getter asks once it turns true.
2377
+ */
2378
+ function useMIDIAccess(requestOnMount = true) {
2379
+ const instance = createMIDIAccess();
2380
+ const state = shallowRef(instance.getState());
2381
+ const unsubscribe = instance.subscribe(() => {
2382
+ state.value = instance.getState();
2383
+ });
2384
+ onMounted(() => {
2385
+ watch(() => toValue(requestOnMount), (request) => {
2386
+ if (request) instance.request();
2387
+ }, { immediate: true });
2388
+ });
2389
+ onScopeDispose(() => {
2390
+ unsubscribe();
2391
+ instance.destroy();
2392
+ });
2393
+ return {
2394
+ state: computed(() => state.value),
2395
+ /**
2396
+ * Ask for access. Takes `{ sysex: true }` when the app needs system
2397
+ * exclusive messages, which browsers treat as a separate permission.
2398
+ */
2399
+ request: (options) => instance.request(options)
2400
+ };
2401
+ }
2402
+ //#endregion
2403
+ //#region src/composables/useMIDIInput.ts
2404
+ /**
2405
+ * Handle the channel voice messages of every connected input. Call it during
2406
+ * `setup`. See `createMIDIInput` in `@tremolo-ui/dom` for the handlers.
2407
+ *
2408
+ * The listeners follow `midiAccess` as it is granted, and new handlers
2409
+ * replace the old ones without re-subscribing.
2410
+ */
2411
+ function useMIDIInput(midiAccess, handlers) {
2412
+ let instance = null;
2413
+ watch(() => toValue(midiAccess), (access, _, onCleanup) => {
2414
+ const current = createMIDIInput(access, toValue(handlers));
2415
+ instance = current;
2416
+ onCleanup(() => {
2417
+ current.destroy();
2418
+ if (instance === current) instance = null;
2419
+ });
2420
+ }, { immediate: true });
2421
+ watch(() => toValue(handlers), (next) => instance?.update(next));
2422
+ }
2423
+ //#endregion
2424
+ //#region src/composables/useMIDIMessage.ts
2425
+ /**
2426
+ * Listen to raw `midimessage` events on every input. Call it during `setup`.
2427
+ * For note and controller events, `useMIDIInput` decodes the message for you.
2428
+ */
2429
+ function useMIDIMessage(midiAccess, onMIDIMessage) {
2430
+ let instance = null;
2431
+ watch(() => toValue(midiAccess), (access, _, onCleanup) => {
2432
+ const current = createMIDIMessage(access, toValue(onMIDIMessage));
2433
+ instance = current;
2434
+ onCleanup(() => {
2435
+ current.destroy();
2436
+ if (instance === current) instance = null;
2437
+ });
2438
+ }, { immediate: true });
2439
+ watch(() => toValue(onMIDIMessage), (next) => instance?.update(next));
2440
+ }
2441
+ //#endregion
2442
+ export { AnimationCanvas, DropZone, FileInput, FileInputTrigger, Knob, KnobActiveLine, KnobInactiveLine, KnobSVGRoot, KnobThumb, NumberInput, NumberInputDecrementStepper, NumberInputField, NumberInputIncrementStepper, NumberInputStepper, Piano, PointsEditor, PointsEditorBackground, PointsEditorContainer, PointsEditorPoint, PointsEditorSelectionBox, Slider, SliderMarks, SliderMarksOption, SliderThumb, SliderTrack, XYPad, XYPadArea, XYPadThumb, useDrag, useDragValue, useDropZone, useFileInputContext, useKnobContext, useLongPress, useMIDIAccess, useMIDIInput, useMIDIMessage, useNumberInputContext, usePointsEditorContext, useSliderContext, useWheel, useXYPadContext };
2443
+
2444
+ //# sourceMappingURL=index.js.map