@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
@@ -0,0 +1,36 @@
1
+ import { inject, provide, type InjectionKey } from 'vue'
2
+
3
+ const PLACEMENT: InjectionKey<string> = Symbol('tremolo-ui placement')
4
+
5
+ /**
6
+ * Mark the children of this component as being inside `name`, for
7
+ * {@link checkPlacement}. Call it during `setup`.
8
+ */
9
+ export function providePlacement(name: string) {
10
+ provide(PLACEMENT, name)
11
+ }
12
+
13
+ /**
14
+ * Warn, in development only, when `child` is rendered outside `parent`. A
15
+ * part positioned against its parent still renders anywhere else, so nothing
16
+ * fails when it is misplaced — its position just comes out wrong. The names
17
+ * are the ones written in templates (`'KnobThumb'`), since they are what the
18
+ * warning shows. Call it during `setup`.
19
+ */
20
+ export function checkPlacement(child: string, parent: string) {
21
+ try {
22
+ // Inline and first, so that a bundler folds the comparison and drops the
23
+ // message with it. Wrapped because `process` may not exist at all.
24
+ if (process.env.NODE_ENV === 'production') return
25
+ const found = inject(PLACEMENT, null)
26
+ if (found === parent) return
27
+ console.warn(
28
+ `[tremolo-ui] ${child} has to be rendered inside ${parent}` +
29
+ (found === null ? '.' : `, but it is inside ${found}.`) +
30
+ ' It renders either way, so nothing fails — its position just' +
31
+ ' comes out wrong.',
32
+ )
33
+ } catch {
34
+ // No bundler substituted NODE_ENV. Say nothing rather than break.
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ import { type PropType } from 'vue'
2
+
3
+ import {
4
+ DEFAULT_DRAG_SENSITIVITY,
5
+ DEFAULT_KEYBOARD_OPTIONS,
6
+ DEFAULT_WHEEL_OPTIONS,
7
+ type InputEventOption,
8
+ type ModifierValue,
9
+ } from '@tremolo-ui/dom'
10
+
11
+ // The input props every value control shares, with the core's defaults. One
12
+ // constant each rather than an object to spread: a spread inside
13
+ // `defineComponent(...)` counts as a possible side effect to a bundler, which
14
+ // then keeps every component that uses it even when none is imported.
15
+
16
+ /** How much one notch of the wheel moves the value. `null` turns it off. */
17
+ export const wheelProp = {
18
+ type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
19
+ default: () => DEFAULT_WHEEL_OPTIONS,
20
+ }
21
+
22
+ /** How much one arrow key press moves the value. `null` turns it off. */
23
+ export const keyboardProp = {
24
+ type: [Array, Object] as PropType<ModifierValue<InputEventOption> | null>,
25
+ default: () => DEFAULT_KEYBOARD_OPTIONS,
26
+ }
27
+
28
+ /** How much a drag moves the value, per modifier key. */
29
+ export const dragSensitivityProp = {
30
+ type: [Number, Object] as PropType<ModifierValue<number>>,
31
+ default: () => DEFAULT_DRAG_SENSITIVITY,
32
+ }
@@ -0,0 +1,22 @@
1
+ import { watchEffect } from 'vue'
2
+
3
+ import { checkSteps, type CheckStepsOptions } from '@tremolo-ui/dom'
4
+
5
+ /**
6
+ * Warn, in development only, when a key press or a wheel notch cannot produce
7
+ * a change the user can see. The probing is `checkSteps` in the core.
8
+ *
9
+ * Takes a getter, so that the check runs again when the settings change.
10
+ */
11
+ export function useCheckSteps(options: () => CheckStepsOptions) {
12
+ try {
13
+ // Inline and first, so that a bundler folds the comparison and drops the
14
+ // only call to `checkSteps`, which then falls out of the bundle too.
15
+ if (process.env.NODE_ENV === 'production') return
16
+ watchEffect(() => {
17
+ for (const warning of checkSteps(options())) console.warn(warning)
18
+ })
19
+ } catch {
20
+ // No bundler substituted NODE_ENV. Say nothing rather than break.
21
+ }
22
+ }
@@ -0,0 +1,16 @@
1
+ import { h, type VNodeProps } from 'vue'
2
+
3
+ import { visuallyHiddenStyle } from '@tremolo-ui/dom'
4
+
5
+ /**
6
+ * The control underneath a headless part: out of sight, but in the
7
+ * accessibility tree and the tab order, carrying the ARIA and the keyboard.
8
+ *
9
+ * A plain element rather than a component, so that a `ref` on it reaches the
10
+ * `<input>` itself.
11
+ */
12
+ export function visuallyHiddenRangeInput(
13
+ props: VNodeProps & Record<string, unknown>,
14
+ ) {
15
+ return h('input', { ...props, type: 'range', style: visuallyHiddenStyle })
16
+ }
@@ -0,0 +1,22 @@
1
+ import { type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { createDrag, type DragOptions } from '@tremolo-ui/dom'
4
+
5
+ import { useInstance } from './useInstance'
6
+
7
+ /**
8
+ * Track a pointer drag on an element. See `createDrag` in `@tremolo-ui/dom`
9
+ * for the options.
10
+ *
11
+ * @example
12
+ * const el = useTemplateRef('el')
13
+ * useDrag(el, { onDrag: (state) => (x.value += state.deltaX) })
14
+ */
15
+ export function useDrag(
16
+ target: MaybeRefOrGetter<Element | null | undefined>,
17
+ options: MaybeRefOrGetter<DragOptions> = {},
18
+ ) {
19
+ useInstance(target, options, createDrag, (instance, next) =>
20
+ instance.update(next),
21
+ )
22
+ }
@@ -0,0 +1,20 @@
1
+ import { type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { createDragValue, type DragValueOptions } from '@tremolo-ui/dom'
4
+
5
+ import { useInstance } from './useInstance'
6
+
7
+ /**
8
+ * Drive a value with a pointer drag. See `createDragValue` in
9
+ * `@tremolo-ui/dom` for the options; `elementMapping` and `relativeMapping`
10
+ * from there choose how the pointer becomes a value. `mapping` is read when
11
+ * the element is attached.
12
+ */
13
+ export function useDragValue(
14
+ target: MaybeRefOrGetter<Element | null | undefined>,
15
+ options: MaybeRefOrGetter<DragValueOptions>,
16
+ ) {
17
+ useInstance(target, options, createDragValue, (instance, next) =>
18
+ instance.update(next),
19
+ )
20
+ }
@@ -0,0 +1,32 @@
1
+ import { shallowRef, toValue, type MaybeRefOrGetter } from 'vue'
2
+
3
+ import {
4
+ createDropZone,
5
+ type DropZoneOptions,
6
+ type DropZoneState,
7
+ } from '@tremolo-ui/dom'
8
+
9
+ import { useInstance } from './useInstance'
10
+
11
+ /**
12
+ * Take files dropped on an element. See `createDropZone` in
13
+ * `@tremolo-ui/dom`. Returns whether files are over it, and whether they
14
+ * would be refused, for styling it while they are.
15
+ */
16
+ export function useDropZone(
17
+ target: MaybeRefOrGetter<Element | null | undefined>,
18
+ options: MaybeRefOrGetter<DropZoneOptions> = {},
19
+ ) {
20
+ const state = shallowRef<DropZoneState>({ over: false, invalid: false })
21
+ const withState = () => ({
22
+ ...toValue(options),
23
+ onStateChange: (next: DropZoneState) => {
24
+ state.value = next
25
+ toValue(options).onStateChange?.(next)
26
+ },
27
+ })
28
+ useInstance(target, withState, createDropZone, (instance, next) =>
29
+ instance.update(next),
30
+ )
31
+ return state
32
+ }
@@ -0,0 +1,52 @@
1
+ import { toValue, watch, type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { replaceOptions } from '@tremolo-ui/dom'
4
+
5
+ /**
6
+ * Create an instance for the element `target` holds, and destroy it when the
7
+ * element goes away or changes. Options are pushed to the instance with
8
+ * `update` as they change, so that changing them does not interrupt a drag in
9
+ * progress.
10
+ *
11
+ * Call it during `setup`: the watchers stop with the component.
12
+ */
13
+ export function useInstance<
14
+ E extends Element,
15
+ O extends object,
16
+ I extends { destroy: () => void },
17
+ >(
18
+ target: MaybeRefOrGetter<E | null | undefined>,
19
+ options: MaybeRefOrGetter<O>,
20
+ create: (element: E, options: O) => I,
21
+ update: (instance: I, options: Partial<O>) => void,
22
+ ): { current: () => I | null } {
23
+ let instance: I | null = null
24
+
25
+ watch(
26
+ () => toValue(target),
27
+ (element, _, onCleanup) => {
28
+ if (!element) return
29
+ const current = create(element, toValue(options))
30
+ instance = current
31
+ onCleanup(() => {
32
+ current.destroy()
33
+ if (instance === current) instance = null
34
+ })
35
+ },
36
+ { immediate: true, flush: 'post' },
37
+ )
38
+
39
+ // A copy, so that the watch reads every property: a `reactive()` object
40
+ // whose `requireFocus` changes is then seen, not only a new object. The
41
+ // copy is what the instance is left with — what it no longer carries is
42
+ // cleared rather than kept.
43
+ watch(
44
+ () => ({ ...toValue(options) }),
45
+ (next, previous) => {
46
+ if (instance) update(instance, replaceOptions(previous, next))
47
+ },
48
+ { flush: 'post' },
49
+ )
50
+
51
+ return { current: () => instance }
52
+ }
@@ -0,0 +1,20 @@
1
+ import { onScopeDispose, toValue, watch, type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { createLongPress, type LongPressOptions } from '@tremolo-ui/dom'
4
+
5
+ /**
6
+ * Repeat `onPress` while a pointer is held down: once on the press, then
7
+ * every `interval` after `delay`. See `createLongPress` in `@tremolo-ui/dom`.
8
+ *
9
+ * Returns the function that starts a press, for `@pointerdown`.
10
+ */
11
+ export function useLongPress(options: MaybeRefOrGetter<LongPressOptions>) {
12
+ const instance = createLongPress(toValue(options))
13
+ watch(
14
+ () => toValue(options),
15
+ (next) => instance.update(next),
16
+ )
17
+ onScopeDispose(() => instance.destroy())
18
+ return (event?: Pick<PointerEvent, 'button' | 'pointerId'>) =>
19
+ instance.start(event)
20
+ }
@@ -0,0 +1,58 @@
1
+ import {
2
+ computed,
3
+ onMounted,
4
+ onScopeDispose,
5
+ shallowRef,
6
+ toValue,
7
+ watch,
8
+ type MaybeRefOrGetter,
9
+ } from 'vue'
10
+
11
+ import {
12
+ createMIDIAccess,
13
+ type MIDIAccessOptions,
14
+ type MIDIAccessState,
15
+ } from '@tremolo-ui/dom'
16
+
17
+ /**
18
+ * Request MIDI access in the browser. Call it during `setup`.
19
+ *
20
+ * `state` holds the access, the error and the connected inputs, and follows
21
+ * devices as they are plugged in and unplugged.
22
+ *
23
+ * @param requestOnMount ask for access once the component is mounted — never
24
+ * during server rendering, where there is no MIDI to ask for. Leave it off to
25
+ * ask from a click instead, so that opening a page does not bring up a
26
+ * permission prompt. A ref or a getter asks once it turns true.
27
+ */
28
+ export function useMIDIAccess(
29
+ requestOnMount: MaybeRefOrGetter<boolean> = true,
30
+ ) {
31
+ const instance = createMIDIAccess()
32
+ const state = shallowRef<MIDIAccessState>(instance.getState())
33
+ const unsubscribe = instance.subscribe(() => {
34
+ state.value = instance.getState()
35
+ })
36
+ onMounted(() => {
37
+ watch(
38
+ () => toValue(requestOnMount),
39
+ (request) => {
40
+ if (request) instance.request()
41
+ },
42
+ { immediate: true },
43
+ )
44
+ })
45
+ onScopeDispose(() => {
46
+ unsubscribe()
47
+ instance.destroy()
48
+ })
49
+ return {
50
+ // Read-only, and without wrapping MIDIAccess in a proxy.
51
+ state: computed(() => state.value),
52
+ /**
53
+ * Ask for access. Takes `{ sysex: true }` when the app needs system
54
+ * exclusive messages, which browsers treat as a separate permission.
55
+ */
56
+ request: (options?: MIDIAccessOptions) => instance.request(options),
57
+ }
58
+ }
@@ -0,0 +1,33 @@
1
+ import { toValue, watch, type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { createMIDIInput, type MIDIInputHandlers } from '@tremolo-ui/dom'
4
+
5
+ /**
6
+ * Handle the channel voice messages of every connected input. Call it during
7
+ * `setup`. See `createMIDIInput` in `@tremolo-ui/dom` for the handlers.
8
+ *
9
+ * The listeners follow `midiAccess` as it is granted, and new handlers
10
+ * replace the old ones without re-subscribing.
11
+ */
12
+ export function useMIDIInput(
13
+ midiAccess: MaybeRefOrGetter<MIDIAccess | null>,
14
+ handlers: MaybeRefOrGetter<MIDIInputHandlers>,
15
+ ) {
16
+ let instance: ReturnType<typeof createMIDIInput> | null = null
17
+ watch(
18
+ () => toValue(midiAccess),
19
+ (access, _, onCleanup) => {
20
+ const current = createMIDIInput(access, toValue(handlers))
21
+ instance = current
22
+ onCleanup(() => {
23
+ current.destroy()
24
+ if (instance === current) instance = null
25
+ })
26
+ },
27
+ { immediate: true },
28
+ )
29
+ watch(
30
+ () => toValue(handlers),
31
+ (next) => instance?.update(next),
32
+ )
33
+ }
@@ -0,0 +1,30 @@
1
+ import { toValue, watch, type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { createMIDIMessage } from '@tremolo-ui/dom'
4
+
5
+ /**
6
+ * Listen to raw `midimessage` events on every input. Call it during `setup`.
7
+ * For note and controller events, `useMIDIInput` decodes the message for you.
8
+ */
9
+ export function useMIDIMessage(
10
+ midiAccess: MaybeRefOrGetter<MIDIAccess | null>,
11
+ onMIDIMessage: MaybeRefOrGetter<(event: MIDIMessageEvent) => void>,
12
+ ) {
13
+ let instance: ReturnType<typeof createMIDIMessage> | null = null
14
+ watch(
15
+ () => toValue(midiAccess),
16
+ (access, _, onCleanup) => {
17
+ const current = createMIDIMessage(access, toValue(onMIDIMessage))
18
+ instance = current
19
+ onCleanup(() => {
20
+ current.destroy()
21
+ if (instance === current) instance = null
22
+ })
23
+ },
24
+ { immediate: true },
25
+ )
26
+ watch(
27
+ () => toValue(onMIDIMessage),
28
+ (next) => instance?.update(next),
29
+ )
30
+ }
@@ -0,0 +1,23 @@
1
+ import { type MaybeRefOrGetter } from 'vue'
2
+
3
+ import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
4
+
5
+ import { useInstance } from './useInstance'
6
+
7
+ /**
8
+ * Listen to the wheel on an element, as a non-passive listener so that
9
+ * `preventDefault()` can keep the page from scrolling. See `createWheel` in
10
+ * `@tremolo-ui/dom`.
11
+ */
12
+ export function useWheel(
13
+ target: MaybeRefOrGetter<Element | null | undefined>,
14
+ onWheel: (event: WheelEvent) => void,
15
+ options: MaybeRefOrGetter<WheelOptions> = {},
16
+ ) {
17
+ useInstance(
18
+ target,
19
+ options,
20
+ (element, opts) => createWheel(element, (event) => onWheel(event), opts),
21
+ (instance, next) => instance.update(next),
22
+ )
23
+ }
package/src/index.ts ADDED
@@ -0,0 +1,67 @@
1
+ // components
2
+ export {
3
+ AnimationCanvas,
4
+ type DrawFunction,
5
+ type InitFunction,
6
+ } from './components/AnimationCanvas'
7
+ export { DropZone } from './components/DropZone'
8
+ export {
9
+ FileInput,
10
+ FileInputTrigger,
11
+ useFileInputContext,
12
+ type FileInputContextValue,
13
+ } from './components/FileInput'
14
+ export {
15
+ Knob,
16
+ KnobActiveLine,
17
+ KnobInactiveLine,
18
+ KnobSVGRoot,
19
+ KnobThumb,
20
+ useKnobContext,
21
+ type KnobContextValue,
22
+ } from './components/Knob'
23
+ export {
24
+ NumberInput,
25
+ NumberInputDecrementStepper,
26
+ NumberInputField,
27
+ NumberInputIncrementStepper,
28
+ NumberInputStepper,
29
+ useNumberInputContext,
30
+ type NumberInputContextValue,
31
+ } from './components/NumberInput'
32
+ export { Piano, type KeyAttributes, type KeyState } from './components/Piano'
33
+ export {
34
+ PointsEditor,
35
+ PointsEditorBackground,
36
+ PointsEditorContainer,
37
+ PointsEditorPoint,
38
+ PointsEditorSelectionBox,
39
+ usePointsEditorContext,
40
+ type PointsEditorContextValue,
41
+ } from './components/PointsEditor'
42
+ export {
43
+ Slider,
44
+ SliderMarks,
45
+ SliderMarksOption,
46
+ SliderThumb,
47
+ SliderTrack,
48
+ useSliderContext,
49
+ type SliderContextValue,
50
+ } from './components/Slider'
51
+ export {
52
+ XYPad,
53
+ XYPadArea,
54
+ XYPadThumb,
55
+ useXYPadContext,
56
+ type XYPadContextValue,
57
+ } from './components/XYPad'
58
+
59
+ // composables
60
+ export { useDrag } from './composables/useDrag'
61
+ export { useDragValue } from './composables/useDragValue'
62
+ export { useDropZone } from './composables/useDropZone'
63
+ export { useLongPress } from './composables/useLongPress'
64
+ export { useMIDIAccess } from './composables/useMIDIAccess'
65
+ export { useMIDIInput } from './composables/useMIDIInput'
66
+ export { useMIDIMessage } from './composables/useMIDIMessage'
67
+ export { useWheel } from './composables/useWheel'