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