@tremolo-ui/vue 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/index.cjs +865 -611
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +954 -189
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +954 -189
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +339 -128
  8. package/dist/index.js.map +1 -1
  9. package/package.json +7 -7
  10. package/src/components/AnimationCanvas/AnimationCanvas.ts +27 -30
  11. package/src/components/AnimationCanvas/index.ts +1 -5
  12. package/src/components/DropZone/DropZone.ts +19 -9
  13. package/src/components/DropZone/index.ts +1 -1
  14. package/src/components/FileInput/FileInput.ts +28 -14
  15. package/src/components/FileInput/index.ts +1 -1
  16. package/src/components/Knob/Knob.ts +107 -57
  17. package/src/components/Knob/KnobThumb.ts +32 -22
  18. package/src/components/Knob/index.ts +2 -2
  19. package/src/components/NumberInput/NumberInput.ts +136 -53
  20. package/src/components/NumberInput/NumberInputField.ts +5 -2
  21. package/src/components/NumberInput/NumberInputStepper.ts +25 -4
  22. package/src/components/NumberInput/context.ts +12 -0
  23. package/src/components/NumberInput/index.ts +1 -1
  24. package/src/components/NumberInput/stepper-button.ts +8 -1
  25. package/src/components/Piano/Piano.ts +51 -43
  26. package/src/components/Piano/index.ts +6 -1
  27. package/src/components/PointsEditor/PointsEditor.ts +43 -35
  28. package/src/components/PointsEditor/PointsEditorContainer.ts +2 -6
  29. package/src/components/PointsEditor/PointsEditorPoint.ts +82 -42
  30. package/src/components/PointsEditor/context.ts +1 -1
  31. package/src/components/PointsEditor/index.ts +5 -2
  32. package/src/components/Slider/Slider.ts +92 -49
  33. package/src/components/Slider/SliderMarks.ts +28 -14
  34. package/src/components/Slider/SliderMarksOption.ts +54 -33
  35. package/src/components/Slider/SliderThumb.ts +26 -8
  36. package/src/components/Slider/SliderTrack.ts +31 -16
  37. package/src/components/Slider/context.ts +1 -1
  38. package/src/components/Slider/index.ts +8 -5
  39. package/src/components/XYPad/XYPad.ts +94 -56
  40. package/src/components/XYPad/XYPadThumb.ts +21 -14
  41. package/src/components/XYPad/index.ts +2 -2
  42. package/src/components/_util/change-gesture.ts +44 -0
  43. package/src/components/_util/useCheckSteps.ts +1 -1
  44. package/src/components/_util/visually-hidden-range-input.ts +1 -1
  45. package/src/composables/useInstance.ts +1 -1
  46. package/src/composables/useWheel.ts +5 -2
  47. package/src/index.ts +21 -4
@@ -1,5 +1,15 @@
1
- import { computed, defineComponent, h, provide, ref, type PropType } from 'vue'
1
+ import {
2
+ type ComponentObjectPropsOptions,
3
+ computed,
4
+ defineComponent,
5
+ type ExtractPublicPropTypes,
6
+ h,
7
+ type PropType,
8
+ provide,
9
+ ref,
10
+ } from 'vue'
2
11
 
12
+ import { type ChangeSource } from '@tremolo-ui/dom'
3
13
  import {
4
14
  commitNumberInputText,
5
15
  numberInputBounds,
@@ -7,66 +17,89 @@ import {
7
17
  nudgeNumberInput,
8
18
  parseNumberText,
9
19
  wheelDirection,
10
- } from '@tremolo-ui/dom'
20
+ } from '@tremolo-ui/dom/internal'
11
21
  import { linearScale, type Scale } from '@tremolo-ui/functions'
12
22
 
13
23
  import { useWheel } from '../../composables/useWheel'
24
+ import { useChangeGesture } from '../_util/change-gesture'
14
25
  import { dragSensitivityProp, keyboardProp, wheelProp } from '../_util/props'
15
26
  import { useCheckSteps } from '../_util/useCheckSteps'
16
27
 
17
- import { NumberInputKey } from './context'
28
+ import { NumberInputGestureKey, NumberInputKey } from './context'
29
+
30
+ const numberInputProps = {
31
+ /** The current value. Bind it with `v-model`. */
32
+ modelValue: { type: Number, required: true },
33
+ /** The lowest value. Leave it out for no lower end. */
34
+ min: Number,
35
+ /** The highest value. Leave it out for no upper end. */
36
+ max: Number,
37
+ /** Granularity of the value. @default 1 */
38
+ step: { type: Number, default: 1 },
39
+ /** How the value is distributed, for a `normalized` amount. */
40
+ scale: { type: Object as PropType<Scale>, default: () => linearScale },
41
+ /** The text shown for a value. @default String */
42
+ format: {
43
+ type: Function as PropType<(value: number) => string>,
44
+ default: String,
45
+ },
46
+ /** Read a value back out of the text. Has to undo `format`. */
47
+ parse: {
48
+ type: Function as PropType<(text: string) => number>,
49
+ default: parseNumberText,
50
+ },
51
+ /** Keep the value within `min` and `max` when committed or stepped. */
52
+ clampValue: { type: Boolean, default: true },
53
+ wheel: wheelProp,
54
+ keyboard: keyboardProp,
55
+ dragSensitivity: dragSensitivityProp,
56
+ /** Pixels of vertical drag on the stepper per `step`. `null` turns it off. */
57
+ drag: { type: Number as PropType<number | null>, default: 1 },
58
+ /** Hide the cursor while dragging the stepper. */
59
+ pointerLock: Boolean,
60
+ /** The cursor to show while dragging the stepper. @default 'ns-resize' */
61
+ dragCursor: { type: String, default: 'ns-resize' },
62
+ /** Select the text when the field takes focus. @default 'none' */
63
+ selectOnFocus: {
64
+ type: String as PropType<'all' | 'number' | 'none'>,
65
+ default: 'none',
66
+ },
67
+ /** Show the plain value while the field has focus. */
68
+ unformatOnFocus: Boolean,
69
+ /** Put the caret back at the same digit after an arrow key step. */
70
+ keepCaretOnStep: Boolean,
71
+ /** Commit and leave the field on Enter. @default true */
72
+ blurOnEnter: { type: Boolean, default: true },
73
+ /** Make the input unchangeable and remove it from the tab order. */
74
+ disabled: Boolean,
75
+ /** Make the value unchangeable while leaving the field focusable. */
76
+ readonly: Boolean,
77
+ /**
78
+ * How long after the last wheel notch, arrow key or typed character a change
79
+ * counts as over, in milliseconds, for `change-end`.
80
+ * @default 500
81
+ */
82
+ changeEndDelay: { type: Number, default: 500 },
83
+ } satisfies ComponentObjectPropsOptions
84
+
85
+ export type NumberInputProps = ExtractPublicPropTypes<typeof numberInputProps>
18
86
 
19
87
  /** Input with some useful functions for entering numbers. Bind it with `v-model`. */
20
88
  export const NumberInput = /* @__PURE__ */ defineComponent({
21
89
  name: 'NumberInput',
22
- props: {
23
- /** The current value. Bind it with `v-model`. */
24
- modelValue: { type: Number, required: true },
25
- /** The lowest value. Leave it out for no lower end. */
26
- min: Number,
27
- /** The highest value. Leave it out for no upper end. */
28
- max: Number,
29
- /** Granularity of the value. @default 1 */
30
- step: { type: Number, default: 1 },
31
- /** How the value is distributed, for a `normalized` amount. */
32
- scale: { type: Object as PropType<Scale>, default: () => linearScale },
33
- /** The text shown for a value. @default String */
34
- format: {
35
- type: Function as PropType<(value: number) => string>,
36
- default: String,
37
- },
38
- /** Read a value back out of the text. Has to undo `format`. */
39
- parse: {
40
- type: Function as PropType<(text: string) => number>,
41
- default: parseNumberText,
42
- },
43
- /** Keep the value within `min` and `max` when committed or stepped. */
44
- clampValue: { type: Boolean, default: true },
45
- wheel: wheelProp,
46
- keyboard: keyboardProp,
47
- dragSensitivity: dragSensitivityProp,
48
- /** Pixels of vertical drag on the stepper per `step`. `null` turns it off. */
49
- drag: { type: Number as PropType<number | null>, default: 1 },
50
- /** Hide the cursor while dragging the stepper. */
51
- pointerLock: Boolean,
52
- /** Select the text when the field takes focus. @default 'none' */
53
- selectOnFocus: {
54
- type: String as PropType<'all' | 'number' | 'none'>,
55
- default: 'none',
56
- },
57
- /** Show the plain value while the field has focus. */
58
- unformatOnFocus: Boolean,
59
- /** Put the caret back at the same digit after an arrow key step. */
60
- keepCaretOnStep: Boolean,
61
- /** Commit and leave the field on Enter. @default true */
62
- blurOnEnter: { type: Boolean, default: true },
63
- /** Make the input unchangeable and remove it from the tab order. */
64
- disabled: Boolean,
65
- /** Make the value unchangeable while leaving the field focusable. */
66
- readonly: Boolean,
67
- },
90
+ props: numberInputProps,
68
91
  emits: {
69
92
  'update:modelValue': (value: number) => typeof value === 'number',
93
+ /**
94
+ * A change of the value started — a press on a stepper, the first wheel
95
+ * notch, arrow key or typed character — with the value before it and what
96
+ * it is made with.
97
+ */
98
+ changeStart: (value: number, _source: ChangeSource) =>
99
+ typeof value === 'number',
100
+ /** The change ended, with the value it ended on. */
101
+ changeEnd: (value: number, _source: ChangeSource) =>
102
+ typeof value === 'number',
70
103
  },
71
104
  setup(props, { slots, emit, expose }) {
72
105
  const root = ref<HTMLDivElement | null>(null)
@@ -115,12 +148,46 @@ export const NumberInput = /* @__PURE__ */ defineComponent({
115
148
  format: props.format,
116
149
  }))
117
150
 
151
+ const gesture = useChangeGesture({
152
+ value: () => props.modelValue,
153
+ inactive: () => inactive.value,
154
+ endDelay: () => props.changeEndDelay,
155
+ onStart: (value, source) => emit('changeStart', value, source),
156
+ onEnd: (value, source) => emit('changeEnd', value, source),
157
+ })
158
+
118
159
  function changeValue(next: number) {
119
160
  if (inactive.value) return
120
161
  draft.value = null
121
- if (next !== props.modelValue) emit('update:modelValue', next)
162
+ if (next === props.modelValue) return
163
+ gesture.changed(next)
164
+ emit('update:modelValue', next)
122
165
  }
123
166
 
167
+ let pressed = false
168
+ provide(NumberInputGestureKey, {
169
+ press: () => {
170
+ if (inactive.value) return
171
+ gesture.hold('pointer')
172
+ // Called again by every repeat of a held stepper; one release will do.
173
+ if (pressed) return
174
+ pressed = true
175
+ // Released wherever the pointer ends up: the stepper drag captures
176
+ // it, and a press that wanders off the button still ends.
177
+ const release = () => {
178
+ pressed = false
179
+ gesture.end()
180
+ window.removeEventListener('pointerup', release)
181
+ window.removeEventListener('pointercancel', release)
182
+ }
183
+ window.addEventListener('pointerup', release)
184
+ window.addEventListener('pointercancel', release)
185
+ },
186
+ key: () => {
187
+ if (!inactive.value) gesture.pulse('keyboard')
188
+ },
189
+ })
190
+
124
191
  provide(NumberInputKey, {
125
192
  get value() {
126
193
  return props.modelValue
@@ -152,6 +219,9 @@ export const NumberInput = /* @__PURE__ */ defineComponent({
152
219
  get pointerLock() {
153
220
  return props.pointerLock
154
221
  },
222
+ get dragCursor() {
223
+ return props.dragCursor
224
+ },
155
225
  get selectOnFocus() {
156
226
  return props.selectOnFocus
157
227
  },
@@ -188,7 +258,12 @@ export const NumberInput = /* @__PURE__ */ defineComponent({
188
258
  // Deliberately unclamped: clamping here would make "1500" impossible
189
259
  // to type into an input whose max is 100.
190
260
  const parsed = props.parse(next)
191
- if (Number.isFinite(parsed)) emit('update:modelValue', parsed)
261
+ if (!Number.isFinite(parsed)) return
262
+ if (parsed !== props.modelValue) {
263
+ gesture.pulse('keyboard')
264
+ gesture.changed(parsed)
265
+ }
266
+ emit('update:modelValue', parsed)
192
267
  },
193
268
  commitDraft: () => {
194
269
  if (draft.value === null || inactive.value) return
@@ -199,8 +274,15 @@ export const NumberInput = /* @__PURE__ */ defineComponent({
199
274
  })
200
275
  // Text with no number is not a value: the field goes back to what it
201
276
  // was showing.
202
- if (committed === null) draft.value = null
203
- else changeValue(committed)
277
+ if (committed === null) {
278
+ draft.value = null
279
+ return
280
+ }
281
+ // The typing was a gesture of its own, and committing is where it
282
+ // ends.
283
+ if (committed !== props.modelValue) gesture.pulse('keyboard')
284
+ changeValue(committed)
285
+ gesture.end()
204
286
  },
205
287
  changeValue,
206
288
  nudge: (direction, option, modifiers) =>
@@ -224,6 +306,7 @@ export const NumberInput = /* @__PURE__ */ defineComponent({
224
306
  const direction = wheelDirection(event)
225
307
  if (!props.wheel || inactive.value || direction === null) return
226
308
  event.preventDefault()
309
+ gesture.pulse('wheel')
227
310
  changeValue(
228
311
  nudgeNumberInput(
229
312
  props.modelValue,
@@ -2,6 +2,7 @@ import {
2
2
  computed,
3
3
  defineComponent,
4
4
  h,
5
+ inject,
5
6
  nextTick,
6
7
  onBeforeUnmount,
7
8
  ref,
@@ -12,9 +13,9 @@ import {
12
13
  caretAtDecimalOffset,
13
14
  caretDecimalOffset,
14
15
  numberSpan,
15
- } from '@tremolo-ui/dom'
16
+ } from '@tremolo-ui/dom/internal'
16
17
 
17
- import { useNumberInputContext } from './context'
18
+ import { NumberInputGestureKey, useNumberInputContext } from './context'
18
19
 
19
20
  /**
20
21
  * The text field, and the only place the value can be typed. While the user
@@ -24,6 +25,7 @@ export const NumberInputField = /* @__PURE__ */ defineComponent({
24
25
  name: 'NumberInputField',
25
26
  setup(_, { expose }) {
26
27
  const field = useNumberInputContext()
28
+ const gesture = inject(NumberInputGestureKey, null)
27
29
  const input = ref<HTMLInputElement | null>(null)
28
30
  const focused = ref(false)
29
31
  watchEffect(() => field.setInput(input.value))
@@ -58,6 +60,7 @@ export const NumberInputField = /* @__PURE__ */ defineComponent({
58
60
  field.keepCaretOnStep && element.selectionStart !== null
59
61
  ? caretDecimalOffset(from, element.selectionStart)
60
62
  : null
63
+ gesture?.key()
61
64
  field.nudge(direction, field.keyboard!, event)
62
65
  if (offset === null) return
63
66
  await nextTick()
@@ -1,8 +1,15 @@
1
- import { computed, defineComponent, h, provide, ref, watch } from 'vue'
1
+ import { computed, defineComponent, h, inject, provide, ref, watch } from 'vue'
2
2
 
3
- import { createStepperDrag, type StepperDragInstance } from '@tremolo-ui/dom'
3
+ import {
4
+ createStepperDrag,
5
+ type StepperDragInstance,
6
+ } from '@tremolo-ui/dom/internal'
4
7
 
5
- import { StepperKey, useNumberInputContext } from './context'
8
+ import {
9
+ NumberInputGestureKey,
10
+ StepperKey,
11
+ useNumberInputContext,
12
+ } from './context'
6
13
 
7
14
  /**
8
15
  * The area the steppers sit in, and a drag handle in its own right: dragging
@@ -12,6 +19,7 @@ export const NumberInputStepper = /* @__PURE__ */ defineComponent({
12
19
  name: 'NumberInputStepper',
13
20
  setup(_, { slots }) {
14
21
  const field = useNumberInputContext()
22
+ const gesture = inject(NumberInputGestureKey, null)
15
23
  const el = ref<HTMLDivElement | null>(null)
16
24
  let instance: StepperDragInstance | null = null
17
25
 
@@ -26,6 +34,7 @@ export const NumberInputStepper = /* @__PURE__ */ defineComponent({
26
34
  pixels: field.drag ?? 1,
27
35
  sensitivity: field.dragSensitivity,
28
36
  pointerLock: field.pointerLock,
37
+ cursor: field.dragCursor,
29
38
  })
30
39
 
31
40
  watch(
@@ -49,6 +58,18 @@ export const NumberInputStepper = /* @__PURE__ */ defineComponent({
49
58
 
50
59
  provide(StepperKey, { moved: () => instance?.moved() ?? false })
51
60
 
52
- return () => h('div', { ref: el }, slots.default?.())
61
+ return () =>
62
+ h(
63
+ 'div',
64
+ {
65
+ ref: el,
66
+ // A drag on the stepper is a press like any other: held from here
67
+ // until the pointer is released.
68
+ onPointerdown: () => {
69
+ if (enabled.value) gesture?.press()
70
+ },
71
+ },
72
+ slots.default?.(),
73
+ )
53
74
  },
54
75
  })
@@ -21,6 +21,7 @@ export interface NumberInputContextValue {
21
21
  readonly drag: number | null
22
22
  readonly dragSensitivity: ModifierValue<number>
23
23
  readonly pointerLock: boolean
24
+ readonly dragCursor: string
24
25
  readonly selectOnFocus: 'all' | 'number' | 'none'
25
26
  readonly unformatOnFocus: boolean
26
27
  readonly keepCaretOnStep: boolean
@@ -58,6 +59,17 @@ export function useNumberInputContext(): NumberInputContextValue {
58
59
  return injectContext(NumberInputKey, 'NumberInput')
59
60
  }
60
61
 
62
+ /** @internal The change gesture of `NumberInput`, for the parts that change the value. */
63
+ export interface NumberInputGestureValue {
64
+ /** A press on a stepper, held until the pointer is released anywhere. */
65
+ press: () => void
66
+ /** An arrow key in `NumberInputField`. */
67
+ key: () => void
68
+ }
69
+
70
+ export const NumberInputGestureKey: InjectionKey<NumberInputGestureValue> =
71
+ Symbol('NumberInputGesture')
72
+
61
73
  /** @internal Whether the drag on `NumberInputStepper` has moved the value. */
62
74
  export const StepperKey: InjectionKey<{ moved: () => boolean }> =
63
75
  Symbol('NumberInputStepper')
@@ -1,5 +1,5 @@
1
1
  export { useNumberInputContext, type NumberInputContextValue } from './context'
2
- export { NumberInput } from './NumberInput'
2
+ export { NumberInput, type NumberInputProps } from './NumberInput'
3
3
  export { NumberInputDecrementStepper } from './NumberInputDecrementStepper'
4
4
  export { NumberInputField } from './NumberInputField'
5
5
  export { NumberInputIncrementStepper } from './NumberInputIncrementStepper'
@@ -2,7 +2,11 @@ import { computed, defineComponent, h, inject } from 'vue'
2
2
 
3
3
  import { useLongPress } from '../../composables/useLongPress'
4
4
 
5
- import { StepperKey, useNumberInputContext } from './context'
5
+ import {
6
+ NumberInputGestureKey,
7
+ StepperKey,
8
+ useNumberInputContext,
9
+ } from './context'
6
10
 
7
11
  /** The body of `NumberInputIncrementStepper` and `NumberInputDecrementStepper`. */
8
12
  export function stepperButton(name: string, direction: 1 | -1) {
@@ -11,12 +15,15 @@ export function stepperButton(name: string, direction: 1 | -1) {
11
15
  setup(_, { slots }) {
12
16
  const field = useNumberInputContext()
13
17
  const stepper = inject(StepperKey, null)
18
+ const gesture = inject(NumberInputGestureKey, null)
14
19
  const blocked = computed(
15
20
  () => field.disabled || (direction > 0 ? field.atMax : field.atMin),
16
21
  )
17
22
  const press = useLongPress({
18
23
  onPress: () => {
19
24
  if (field.disabled || field.readonly) return
25
+ // On every press and repeat; the gesture is held from the first.
26
+ gesture?.press()
20
27
  // Once the pointer has actually travelled, the drag on the stepper
21
28
  // owns the value; repeating on top of it would move it twice.
22
29
  if (stepper?.moved()) return
@@ -1,27 +1,31 @@
1
1
  import {
2
+ type ComponentObjectPropsOptions,
2
3
  computed,
3
4
  defineComponent,
5
+ type ExtractPublicPropTypes,
4
6
  h,
5
- ref,
6
- watch,
7
7
  type HTMLAttributes,
8
8
  type PropType,
9
+ ref,
9
10
  type SlotsType,
11
+ watch,
10
12
  } from 'vue'
11
13
 
12
14
  import {
13
- blackKeyWidth,
14
15
  createPianoInput,
15
- fitWhiteKeyWidth,
16
- getNoteRangeArray,
17
16
  notePosition,
18
- pianoWidth,
19
17
  type KeyboardShortcuts,
20
18
  type KeyboardShortcutsScope,
21
19
  type NoteRange,
22
20
  type PianoInputInstance,
23
21
  type PianoLayout,
24
22
  } from '@tremolo-ui/dom'
23
+ import {
24
+ blackKeyWidth,
25
+ fitWhiteKeyWidth,
26
+ getNoteRangeArray,
27
+ pianoWidth,
28
+ } from '@tremolo-ui/dom/internal'
25
29
  import { isWhiteKey, noteKey } from '@tremolo-ui/functions'
26
30
 
27
31
  /** What a key is, when the `label` slot or `keyProps` is asked about it. */
@@ -39,49 +43,53 @@ export interface KeyState {
39
43
  export type KeyAttributes = HTMLAttributes &
40
44
  Record<`data-${string}`, string | number | boolean | undefined>
41
45
 
46
+ const pianoProps = {
47
+ /** The notes drawn, from `first` to `last`. */
48
+ noteRange: { type: Object as PropType<NoteRange>, required: true },
49
+ /** Let a pointer slide from one key to the next. @default true */
50
+ glissando: { type: Boolean, default: true },
51
+ /** Highest note that can sound. @default 127 */
52
+ midiMax: { type: Number, default: 127 },
53
+ /** Play notes from the computer keyboard. See `SHORTCUTS` in `@tremolo-ui/dom`. */
54
+ keyboardShortcuts: Object as PropType<KeyboardShortcuts>,
55
+ /** Where keyboard shortcuts listen. @default 'root' */
56
+ keyboardShortcutsScope: {
57
+ type: String as PropType<KeyboardShortcutsScope>,
58
+ default: 'root',
59
+ },
60
+ /** Follow the width of the parent element. */
61
+ resizable: Boolean,
62
+ /** Width of a white key in pixels. @default 40 */
63
+ whiteKeyWidth: { type: Number, default: 40 },
64
+ /** Space between two white keys, in pixels. @default 1 */
65
+ keyGap: { type: Number, default: 1 },
66
+ /** Width of a black key, as a fraction of a white one. @default 0.65 */
67
+ blackKeyWidthRatio: { type: Number, default: 0.65 },
68
+ /** Height of a black key, as a fraction of the keyboard. @default 0.6 */
69
+ blackKeyHeightRatio: { type: Number, default: 0.6 },
70
+ /** Classes for the label inside each key. */
71
+ classes: Object as PropType<{
72
+ keyLabelWrapper?: string
73
+ keyLabel?: string
74
+ }>,
75
+ /**
76
+ * Extra attributes for one key, by note. The geometry of the key is
77
+ * applied after the returned style and cannot be overridden.
78
+ */
79
+ keyProps: Function as PropType<
80
+ (note: number, state: KeyState) => KeyAttributes
81
+ >,
82
+ } satisfies ComponentObjectPropsOptions
83
+
84
+ export type PianoProps = ExtractPublicPropTypes<typeof pianoProps>
85
+
42
86
  /**
43
87
  * Customizable piano. The keys are drawn by the component; what goes inside
44
88
  * one is the `label` slot, which receives the note and its state.
45
89
  */
46
90
  export const Piano = /* @__PURE__ */ defineComponent({
47
91
  name: 'Piano',
48
- props: {
49
- /** The notes drawn, from `first` to `last`. */
50
- noteRange: { type: Object as PropType<NoteRange>, required: true },
51
- /** Let a pointer slide from one key to the next. @default true */
52
- glissando: { type: Boolean, default: true },
53
- /** Highest note that can sound. @default 127 */
54
- midiMax: { type: Number, default: 127 },
55
- /** Play notes from the computer keyboard. See `SHORTCUTS` in `@tremolo-ui/dom`. */
56
- keyboardShortcuts: Object as PropType<KeyboardShortcuts>,
57
- /** Where keyboard shortcuts listen. @default 'root' */
58
- keyboardShortcutsScope: {
59
- type: String as PropType<KeyboardShortcutsScope>,
60
- default: 'root',
61
- },
62
- /** Follow the width of the parent element. */
63
- resizable: Boolean,
64
- /** Width of a white key in pixels. @default 40 */
65
- whiteKeyWidth: { type: Number, default: 40 },
66
- /** Space between two white keys, in pixels. @default 1 */
67
- keyGap: { type: Number, default: 1 },
68
- /** Width of a black key, as a fraction of a white one. @default 0.65 */
69
- blackKeyWidthRatio: { type: Number, default: 0.65 },
70
- /** Height of a black key, as a fraction of the keyboard. @default 0.6 */
71
- blackKeyHeightRatio: { type: Number, default: 0.6 },
72
- /** Classes for the label inside each key. */
73
- classes: Object as PropType<{
74
- keyLabelWrapper?: string
75
- keyLabel?: string
76
- }>,
77
- /**
78
- * Extra attributes for one key, by note. The geometry of the key is
79
- * applied after the returned style and cannot be overridden.
80
- */
81
- keyProps: Function as PropType<
82
- (note: number, state: KeyState) => KeyAttributes
83
- >,
84
- },
92
+ props: pianoProps,
85
93
  emits: {
86
94
  /** A note started sounding. */
87
95
  playNote: (note: number, _velocity?: number) => typeof note === 'number',
@@ -1 +1,6 @@
1
- export { Piano, type KeyAttributes, type KeyState } from './Piano'
1
+ export {
2
+ Piano,
3
+ type KeyAttributes,
4
+ type KeyState,
5
+ type PianoProps,
6
+ } from './Piano'
@@ -1,64 +1,72 @@
1
1
  import {
2
+ type ComponentObjectPropsOptions,
2
3
  computed,
3
4
  defineComponent,
5
+ type ExtractPublicPropTypes,
4
6
  h,
5
7
  onScopeDispose,
8
+ type PropType,
6
9
  provide,
7
10
  shallowRef,
8
11
  watchEffect,
9
- type PropType,
10
12
  } from 'vue'
11
13
 
12
14
  import {
13
15
  createPointsEditor,
14
16
  DEFAULT_DRAG_SENSITIVITY,
15
- POINTS_EDITOR_DEFAULT_KEYBOARD,
16
- POINTS_EDITOR_DEFAULT_WHEEL,
17
17
  type InputEventOption,
18
18
  type ModifierValue,
19
19
  type SelectionBoxRect,
20
20
  } from '@tremolo-ui/dom'
21
+ import {
22
+ POINTS_EDITOR_DEFAULT_KEYBOARD,
23
+ POINTS_EDITOR_DEFAULT_WHEEL,
24
+ } from '@tremolo-ui/dom/internal'
21
25
 
22
26
  import { PointsEditorKey } from './context'
23
27
 
24
28
  const EMPTY: readonly string[] = []
25
29
 
30
+ const pointsEditorProps = {
31
+ /** Make the points unchangeable and remove them from the tab order. */
32
+ disabled: Boolean,
33
+ /** Make the points unmovable. */
34
+ readonly: Boolean,
35
+ /** The cursor to show while dragging a point. @default 'grabbing' */
36
+ dragCursor: { type: String, default: 'grabbing' },
37
+ /** How much one wheel notch moves the focused point. */
38
+ wheel: {
39
+ type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
40
+ default: () => POINTS_EDITOR_DEFAULT_WHEEL,
41
+ },
42
+ /** How much one arrow key press moves a point. */
43
+ keyboard: {
44
+ type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
45
+ default: () => POINTS_EDITOR_DEFAULT_KEYBOARD,
46
+ },
47
+ /** How much a drag moves a point, per modifier key. */
48
+ dragSensitivity: {
49
+ type: [Number, Object] as PropType<ModifierValue<number>>,
50
+ default: () => DEFAULT_DRAG_SENSITIVITY,
51
+ },
52
+ /**
53
+ * Let points be selected, and a selection be moved as one. A selection
54
+ * updates several points in the same tick.
55
+ */
56
+ selectable: Boolean,
57
+ /** Ids of the selected points. Bind it with `v-model:selection`. */
58
+ selection: Array as PropType<string[]>,
59
+ } satisfies ComponentObjectPropsOptions
60
+
61
+ export type PointsEditorProps = ExtractPublicPropTypes<typeof pointsEditorProps>
62
+
26
63
  /**
27
64
  * Multiple point controller. Bind the selection with `v-model:selection` to
28
65
  * hold it yourself; the editor keeps its own otherwise.
29
66
  */
30
67
  export const PointsEditor = /* @__PURE__ */ defineComponent({
31
68
  name: 'PointsEditor',
32
- props: {
33
- /** Make the points unchangeable and remove them from the tab order. */
34
- disabled: Boolean,
35
- /** Make the points unmovable. */
36
- readonly: Boolean,
37
- /** The cursor to show while dragging a point. @default { cursor: 'grabbing' } */
38
- externalStyles: Object as PropType<{ cursor?: string }>,
39
- /** How much one wheel notch moves the focused point. */
40
- wheel: {
41
- type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
42
- default: () => POINTS_EDITOR_DEFAULT_WHEEL,
43
- },
44
- /** How much one arrow key press moves a point. */
45
- keyboard: {
46
- type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
47
- default: () => POINTS_EDITOR_DEFAULT_KEYBOARD,
48
- },
49
- /** How much a drag moves a point, per modifier key. */
50
- dragSensitivity: {
51
- type: [Number, Object] as PropType<ModifierValue<number>>,
52
- default: () => DEFAULT_DRAG_SENSITIVITY,
53
- },
54
- /**
55
- * Let points be selected, and a selection be moved as one. A selection
56
- * updates several points in the same tick.
57
- */
58
- selectable: Boolean,
59
- /** Ids of the selected points. Bind it with `v-model:selection`. */
60
- selection: Array as PropType<string[]>,
61
- },
69
+ props: pointsEditorProps,
62
70
  emits: {
63
71
  'update:selection': (selection: string[]) => Array.isArray(selection),
64
72
  },
@@ -110,8 +118,8 @@ export const PointsEditor = /* @__PURE__ */ defineComponent({
110
118
  get dragSensitivity() {
111
119
  return props.dragSensitivity
112
120
  },
113
- get cursor() {
114
- return props.externalStyles?.cursor ?? 'grabbing'
121
+ get dragCursor() {
122
+ return props.dragCursor
115
123
  },
116
124
  get selectable() {
117
125
  return props.selectable
@@ -1,11 +1,7 @@
1
1
  import { computed, defineComponent, h, ref, watch, watchEffect } from 'vue'
2
2
 
3
- import {
4
- createDragValue,
5
- elementMapping,
6
- POINT_AXIS,
7
- wheelMove,
8
- } from '@tremolo-ui/dom'
3
+ import { createDragValue, elementMapping } from '@tremolo-ui/dom'
4
+ import { POINT_AXIS, wheelMove } from '@tremolo-ui/dom/internal'
9
5
 
10
6
  import { useWheel } from '../../composables/useWheel'
11
7
  import { providePlacement } from '../_util/placement'