@tremolo-ui/react 0.4.0 → 0.6.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 (90) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +2272 -1650
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1461 -475
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1461 -475
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2258 -1648
  16. package/dist/index.js.map +1 -1
  17. package/package.json +32 -31
  18. package/src/components/AnimationCanvas/index.tsx +162 -194
  19. package/src/components/Knob/ActiveLine.tsx +9 -8
  20. package/src/components/Knob/InactiveLine.tsx +11 -14
  21. package/src/components/Knob/SVGRoot.tsx +10 -17
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +73 -89
  24. package/src/components/Knob/index.tsx +227 -105
  25. package/src/components/NumberInput/DecrementStepper.tsx +21 -50
  26. package/src/components/NumberInput/IncrementStepper.tsx +22 -50
  27. package/src/components/NumberInput/InputField.tsx +206 -0
  28. package/src/components/NumberInput/Stepper.tsx +150 -21
  29. package/src/components/NumberInput/StepperButton.tsx +95 -0
  30. package/src/components/NumberInput/context.tsx +104 -149
  31. package/src/components/NumberInput/index.tsx +461 -110
  32. package/src/components/Piano/index.tsx +413 -263
  33. package/src/components/Piano/keyboard-shortcuts.ts +61 -0
  34. package/src/components/PointsEditor/Background.tsx +19 -4
  35. package/src/components/PointsEditor/Container.tsx +92 -22
  36. package/src/components/PointsEditor/Point.tsx +293 -53
  37. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  38. package/src/components/PointsEditor/context.tsx +97 -75
  39. package/src/components/PointsEditor/index.tsx +497 -62
  40. package/src/components/Slider/Marks.tsx +84 -0
  41. package/src/components/Slider/MarksOption.tsx +115 -0
  42. package/src/components/Slider/Thumb.tsx +99 -54
  43. package/src/components/Slider/Track.tsx +73 -62
  44. package/src/components/Slider/context.tsx +40 -53
  45. package/src/components/Slider/index.tsx +237 -190
  46. package/src/components/Slider/type.ts +27 -9
  47. package/src/components/XYPad/Area.tsx +52 -33
  48. package/src/components/XYPad/Thumb.tsx +121 -60
  49. package/src/components/XYPad/context.tsx +49 -0
  50. package/src/components/XYPad/index.tsx +338 -246
  51. package/src/components/_util/Placement.tsx +68 -0
  52. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  53. package/src/components/_util/css-length.ts +13 -0
  54. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  55. package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
  56. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  57. package/src/hooks/useAnimationFrame.ts +17 -7
  58. package/src/hooks/useDrag.ts +39 -9
  59. package/src/hooks/useDragValue.ts +194 -0
  60. package/src/hooks/useEventListener.ts +11 -7
  61. package/src/hooks/useLongPress.ts +42 -8
  62. package/src/hooks/useMIDIAccess.ts +82 -11
  63. package/src/hooks/useMIDIInput.ts +45 -13
  64. package/src/hooks/useMIDIMessage.ts +23 -5
  65. package/src/hooks/useWheel.ts +29 -9
  66. package/src/index.ts +50 -27
  67. package/src/input-event.ts +32 -0
  68. package/dist/index.css +0 -536
  69. package/src/Globals.d.ts +0 -3
  70. package/src/components/AnimationCanvas/canvas.ts +0 -46
  71. package/src/components/Knob/index.css +0 -77
  72. package/src/components/NumberInput/InternalInput.tsx +0 -216
  73. package/src/components/NumberInput/index.css +0 -185
  74. package/src/components/NumberInput/type.ts +0 -58
  75. package/src/components/Piano/KeyLabel.tsx +0 -51
  76. package/src/components/Piano/context.tsx +0 -73
  77. package/src/components/Piano/index.css +0 -90
  78. package/src/components/Piano/key.tsx +0 -205
  79. package/src/components/Piano/keyboardShortcuts.ts +0 -31
  80. package/src/components/PointsEditor/index.css +0 -30
  81. package/src/components/Slider/Scale.tsx +0 -52
  82. package/src/components/Slider/ScaleOption.tsx +0 -103
  83. package/src/components/Slider/index.css +0 -112
  84. package/src/components/XYPad/index.css +0 -48
  85. package/src/components/_util/index.ts +0 -28
  86. package/src/components/_util/type.ts +0 -1
  87. package/src/hooks/useDragWithElement.ts +0 -100
  88. package/src/hooks/usePianoDrag.ts +0 -75
  89. package/src/hooks/useRefCallbackEvent.ts +0 -34
  90. package/src/styles/global.css +0 -19
@@ -1,6 +1,6 @@
1
- import { useEffect } from 'react'
1
+ import { useCallback, useEffect, useRef } from 'react'
2
2
 
3
- import { useCallbackRef } from './useCallbackRef'
3
+ import { useCallbackRef } from './_internal/useCallbackRef'
4
4
 
5
5
  type Target = EventTarget | null | (() => EventTarget | null)
6
6
  type Options = boolean | AddEventListenerOptions
@@ -30,6 +30,7 @@ export function useEventListener(
30
30
  options?: Options,
31
31
  ) {
32
32
  const listener = useCallbackRef(handler)
33
+ const cleanupRef = useRef<VoidFunction>(() => {})
33
34
 
34
35
  useEffect(() => {
35
36
  const node = typeof target === 'function' ? target() : (target ?? document)
@@ -37,13 +38,16 @@ export function useEventListener(
37
38
  if (!node) return
38
39
 
39
40
  node.addEventListener(event, listener, options)
40
- return () => {
41
+ const cleanup = () => {
41
42
  node.removeEventListener(event, listener, options)
42
43
  }
44
+ cleanupRef.current = cleanup
45
+
46
+ return () => {
47
+ cleanup()
48
+ if (cleanupRef.current === cleanup) cleanupRef.current = () => {}
49
+ }
43
50
  }, [event, target, options, listener])
44
51
 
45
- return () => {
46
- const node = typeof target === 'function' ? target() : (target ?? document)
47
- node?.removeEventListener(event, listener, options)
48
- }
52
+ return useCallback(() => cleanupRef.current(), [])
49
53
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useState } from 'react'
1
+ import { useCallback, useRef, useState } from 'react'
2
2
 
3
3
  import { useEventListener } from './useEventListener'
4
4
  import { useInterval } from './useInterval'
@@ -10,6 +10,8 @@ export function useLongPress(
10
10
  ) {
11
11
  const [pressed, setPressed] = useState(false)
12
12
  const [delay, setDelay] = useState(initialDelay)
13
+ const activePointerId = useRef<number | null>(null)
14
+ const isPressed = useRef(false)
13
15
 
14
16
  useInterval(
15
17
  () => {
@@ -19,13 +21,45 @@ export function useLongPress(
19
21
  pressed ? delay : null,
20
22
  )
21
23
 
22
- useEventListener(globalThis.window, 'pointerup', () => {
23
- setPressed(false)
24
- setDelay(initialDelay)
24
+ const stop = useCallback(
25
+ (pointerId?: number) => {
26
+ if (!isPressed.current) return
27
+ if (
28
+ pointerId !== undefined &&
29
+ activePointerId.current !== null &&
30
+ activePointerId.current !== pointerId
31
+ )
32
+ return
33
+
34
+ isPressed.current = false
35
+ activePointerId.current = null
36
+ setPressed(false)
37
+ setDelay(initialDelay)
38
+ },
39
+ [initialDelay],
40
+ )
41
+
42
+ useEventListener(globalThis.window, 'pointerup', (event) => {
43
+ stop(event.pointerId)
44
+ })
45
+
46
+ useEventListener(globalThis.window, 'pointercancel', (event) => {
47
+ stop(event.pointerId)
25
48
  })
26
49
 
27
- return useCallback(() => {
28
- callback()
29
- setPressed(true)
30
- }, [callback])
50
+ useEventListener(globalThis.window, 'blur', () => {
51
+ stop()
52
+ })
53
+
54
+ return useCallback(
55
+ (event?: Pick<PointerEvent, 'button' | 'pointerId'>) => {
56
+ if (isPressed.current || (event && event.button !== 0)) return
57
+
58
+ isPressed.current = true
59
+ activePointerId.current = event?.pointerId ?? null
60
+ callback()
61
+ setPressed(true)
62
+ },
63
+ [callback],
64
+ )
31
65
  }
@@ -1,32 +1,103 @@
1
1
  import { useEffect, useMemo, useSyncExternalStore } from 'react'
2
2
 
3
- import { createMIDIAccess } from '@tremolo-ui/dom'
3
+ import {
4
+ createMIDIAccess,
5
+ type MIDIAccessInstance,
6
+ type MIDIAccessOptions,
7
+ type MIDIAccessState,
8
+ } from '@tremolo-ui/dom'
4
9
 
5
10
  export {
6
11
  NOT_SUPPORTED,
7
12
  PERMISSION_DENIED,
13
+ UNAVAILABLE,
8
14
  type MIDIAccessError,
15
+ type MIDIAccessOptions,
9
16
  } from '@tremolo-ui/dom'
10
17
 
18
+ const INITIAL_STATE: MIDIAccessState = {
19
+ midiAccess: null,
20
+ error: null,
21
+ inputs: [],
22
+ }
23
+
24
+ function createMIDIAccessStore() {
25
+ let instance: MIDIAccessInstance | null = null
26
+ let unsubscribe: VoidFunction | null = null
27
+ let state = INITIAL_STATE
28
+ const listeners = new Set<VoidFunction>()
29
+
30
+ function emit() {
31
+ for (const listener of listeners) listener()
32
+ }
33
+
34
+ function connect() {
35
+ const nextInstance = createMIDIAccess()
36
+ instance = nextInstance
37
+ state = nextInstance.getState()
38
+ unsubscribe = nextInstance.subscribe(() => {
39
+ state = nextInstance.getState()
40
+ emit()
41
+ })
42
+ emit()
43
+ return nextInstance
44
+ }
45
+
46
+ function disconnect(currentInstance: MIDIAccessInstance) {
47
+ if (instance !== currentInstance) return
48
+
49
+ unsubscribe?.()
50
+ unsubscribe = null
51
+ instance = null
52
+ state = INITIAL_STATE
53
+ currentInstance.destroy()
54
+ emit()
55
+ }
56
+
57
+ function request(options?: MIDIAccessOptions) {
58
+ instance?.request(options)
59
+ }
60
+
61
+ return {
62
+ connect,
63
+ disconnect,
64
+ request,
65
+ getState: () => state,
66
+ getServerState: () => INITIAL_STATE,
67
+ subscribe: (listener: VoidFunction) => {
68
+ listeners.add(listener)
69
+ return () => listeners.delete(listener)
70
+ },
71
+ }
72
+ }
73
+
11
74
  /**
12
75
  * Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
76
+ *
77
+ * `inputs` follows the devices: it changes as one is plugged in or unplugged,
78
+ * so a list of devices needs no `statechange` listener of its own.
79
+ *
80
+ * `request` takes `{ sysex: true }` when the app needs system exclusive
81
+ * messages. Browsers treat that as a separate, more sensitive permission, so
82
+ * ask for it only when it is actually used.
13
83
  */
14
84
  export function useMIDIAccess(requestOnMount = true) {
15
- const instance = useMemo(() => createMIDIAccess(), [])
85
+ const store = useMemo(() => createMIDIAccessStore(), [])
16
86
 
17
- const { midiAccess, error } = useSyncExternalStore(
18
- instance.subscribe,
19
- instance.getState,
20
- instance.getServerState,
87
+ const { midiAccess, error, inputs } = useSyncExternalStore(
88
+ store.subscribe,
89
+ store.getState,
90
+ store.getServerState,
21
91
  )
22
92
 
23
93
  useEffect(() => {
24
- if (requestOnMount) instance.request()
25
- }, [instance, requestOnMount])
94
+ const instance = store.connect()
95
+ return () => store.disconnect(instance)
96
+ }, [store])
26
97
 
27
98
  useEffect(() => {
28
- return () => instance.destroy()
29
- }, [instance])
99
+ if (requestOnMount) store.request()
100
+ }, [store, requestOnMount])
30
101
 
31
- return { request: instance.request, midiAccess, error }
102
+ return { request: store.request, midiAccess, error, inputs }
32
103
  }
@@ -1,22 +1,54 @@
1
- import { useEffect } from 'react'
1
+ import { useEffect, useRef } from 'react'
2
2
 
3
- import { createMIDIInput } from '@tremolo-ui/dom'
3
+ import {
4
+ createMIDIInput,
5
+ PITCH_BEND_CENTER,
6
+ type MIDIInputHandlers,
7
+ type MIDIInputInstance,
8
+ } from '@tremolo-ui/dom'
9
+
10
+ export { PITCH_BEND_CENTER, type MIDIInputHandlers }
4
11
 
5
12
  /**
6
- * Hooks for handling note on/off events. To be used with useMIDIAccess. Internally uses useMIDIMessage.
13
+ * Handle MIDI input events. To be used with {@link useMIDIAccess}.
14
+ *
15
+ * The handlers are read fresh on every event, so writing them inline is fine:
16
+ * the listeners are attached once per `midiAccess` and stay attached. Devices
17
+ * plugged in later are picked up without anything having to be rebuilt.
18
+ *
19
+ * @example
20
+ * const { midiAccess } = useMIDIAccess()
21
+ * useMIDIInput(midiAccess, {
22
+ * onNoteOnEvent: (note, velocity) => play(note, velocity / 127),
23
+ * onNoteOffEvent: (note) => stop(note),
24
+ * onControlChangeEvent: (controller, value) => {
25
+ * if (controller === 1) setModulation(value / 127)
26
+ * },
27
+ * })
7
28
  */
8
29
  export function useMIDIInput(
9
30
  midiAccess: MIDIAccess | null,
10
- onNoteOnEvent?: (note: number, velocity: number) => void,
11
- onNoteOffEvent?: (note: number) => void,
12
- onPitchBendEvent?: (msb: number, lsb: number) => void,
31
+ handlers: MIDIInputHandlers,
13
32
  ) {
33
+ // Read when the instance is created. The effect below keeps it current, and
34
+ // runs right after, so a stale handler is replaced within the same commit.
35
+ const latest = useRef(handlers)
36
+ const instanceRef = useRef<MIDIInputInstance | null>(null)
37
+
38
+ useEffect(() => {
39
+ const instance = createMIDIInput(midiAccess, latest.current)
40
+ instanceRef.current = instance
41
+
42
+ return () => {
43
+ instanceRef.current = null
44
+ instance.destroy()
45
+ }
46
+ }, [midiAccess])
47
+
48
+ // Runs after every render: handlers come from props and are cheap to push,
49
+ // and updating in place leaves the listeners untouched.
14
50
  useEffect(() => {
15
- const instance = createMIDIInput(midiAccess, {
16
- onNoteOnEvent,
17
- onNoteOffEvent,
18
- onPitchBendEvent,
19
- })
20
- return () => instance.destroy()
21
- }, [midiAccess, onNoteOnEvent, onNoteOffEvent, onPitchBendEvent])
51
+ latest.current = handlers
52
+ instanceRef.current?.update(handlers)
53
+ })
22
54
  }
@@ -1,16 +1,34 @@
1
- import { useEffect } from 'react'
1
+ import { useEffect, useRef } from 'react'
2
2
 
3
- import { createMIDIMessage } from '@tremolo-ui/dom'
3
+ import { createMIDIMessage, type MIDIMessageInstance } from '@tremolo-ui/dom'
4
4
 
5
5
  /**
6
6
  * Hooks for when you want to process MIDI events in more detail than useMIDIInput.
7
+ *
8
+ * The handler is read fresh on every event, so writing it inline is fine: the
9
+ * listeners are attached once per `midiAccess` and stay attached, and devices
10
+ * plugged in later are picked up.
7
11
  */
8
12
  export function useMIDIMessage(
9
13
  midiAccess: MIDIAccess | null,
10
14
  onMIDIMessage: (event: MIDIMessageEvent) => void,
11
15
  ) {
16
+ const latest = useRef(onMIDIMessage)
17
+ const instanceRef = useRef<MIDIMessageInstance | null>(null)
18
+
19
+ useEffect(() => {
20
+ const instance = createMIDIMessage(midiAccess, (event) =>
21
+ latest.current(event),
22
+ )
23
+ instanceRef.current = instance
24
+
25
+ return () => {
26
+ instanceRef.current = null
27
+ instance.destroy()
28
+ }
29
+ }, [midiAccess])
30
+
12
31
  useEffect(() => {
13
- const instance = createMIDIMessage(midiAccess, onMIDIMessage)
14
- return () => instance.destroy()
15
- }, [midiAccess, onMIDIMessage])
32
+ latest.current = onMIDIMessage
33
+ })
16
34
  }
@@ -1,18 +1,35 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { createWheel } from '@tremolo-ui/dom'
4
-
5
- import { useCallbackRef } from './useCallbackRef'
1
+ import { RefObject, useEffect, useState } from 'react'
2
+
3
+ import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
4
+
5
+ import { useCallbackRef } from './_internal/useCallbackRef'
6
+
7
+ export interface UseWheelOptions extends WheelOptions {
8
+ /**
9
+ * Listen on this element rather than on the one the returned ref callback is
10
+ * attached to, and ignore that callback.
11
+ *
12
+ * For a control made of several movable parts: a wheel event only reaches
13
+ * what the cursor is over, so a listener per part responds only while the
14
+ * cursor is over that same part. Listening on the element they share lets
15
+ * every part see the event, and each one decide whether it is the one to act.
16
+ *
17
+ * The element is read when the listener is attached, so a ref filled in by a
18
+ * parent is fine as long as the parent is above in the tree.
19
+ */
20
+ target?: RefObject<Element | null>
21
+ }
6
22
 
7
23
  /**
8
24
  * Listen to wheel events on an element.
9
25
  *
10
26
  * The listener is not passive, so the handler may call `preventDefault()`.
11
27
  *
12
- * @returns a ref callback to attach to the element
28
+ * @returns a ref callback to attach to the element, unused when `target` is given
13
29
  */
14
30
  export function useWheel<T extends Element>(
15
31
  onWheel: (event: WheelEvent) => void,
32
+ { requireFocus = false, target }: UseWheelOptions = {},
16
33
  ): (node: T | null) => void {
17
34
  const wheelHandler = useCallbackRef(onWheel)
18
35
 
@@ -20,12 +37,15 @@ export function useWheel<T extends Element>(
20
37
  const [node, setNode] = useState<T | null>(null)
21
38
 
22
39
  useEffect(() => {
23
- if (!node) return
40
+ const element = target ? target.current : node
41
+ if (!element) return
24
42
 
25
- const instance = createWheel(node, (event) => wheelHandler(event))
43
+ const instance = createWheel(element, (event) => wheelHandler(event), {
44
+ requireFocus,
45
+ })
26
46
 
27
47
  return () => instance.destroy()
28
- }, [node, wheelHandler])
48
+ }, [node, target, wheelHandler, requireFocus])
29
49
 
30
50
  return setNode
31
51
  }
package/src/index.ts CHANGED
@@ -1,53 +1,61 @@
1
- // For bundled CSS (@tremolo-ui/react/styles/index.css)
2
- import './styles/global.css'
3
- import './components/Knob/index.css'
4
- import './components/NumberInput/index.css'
5
- import './components/Piano/index.css'
6
- import './components/PointsEditor/index.css'
7
- import './components/Slider/index.css'
8
- import './components/XYPad/index.css'
9
-
10
1
  export {
11
2
  AnimationCanvas,
12
- type AbsoluteSizingProps,
3
+ type AnimationCanvasCommonProps,
4
+ type AnimationCanvasFixedProps,
13
5
  type AnimationCanvasProps,
14
- type CommonProps,
6
+ type AnimationCanvasResizableProps,
15
7
  type DrawFunction,
16
8
  type InitFunction,
17
- type RelativeSizingProps,
18
9
  } from './components/AnimationCanvas'
19
- export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
10
+ export {
11
+ Knob,
12
+ useKnobContext,
13
+ type KnobContextValue,
14
+ type KnobProps,
15
+ type KnobMethods,
16
+ type KnobSVGRootProps,
17
+ type KnobThumbProps,
18
+ } from './components/Knob'
20
19
  export {
21
20
  NumberInput,
21
+ useNumberInputContext,
22
+ type NumberInputContextValue,
22
23
  type NumberInputProps,
23
24
  type NumberInputMethods,
24
- type StepperProps,
25
- type IncrementStepperProps,
26
- type DecrementStepperProps,
25
+ type NumberInputInputFieldProps,
26
+ type NumberInputStepperProps,
27
+ type NumberInputIncrementStepperProps,
28
+ type NumberInputDecrementStepperProps,
27
29
  } from './components/NumberInput'
28
30
  export {
29
31
  Piano,
30
- getNoteRangeArray,
31
32
  SHORTCUTS,
32
33
  type PianoProps,
33
34
  type PianoMethods,
34
- type KeyProps,
35
- type KeyMethods,
36
- type KeyLabelProps,
35
+ type KeyState,
36
+ type KeyAttributes,
37
+ type CSSVariables,
37
38
  type KeyboardShortcuts,
38
39
  } from './components/Piano'
39
40
  export {
40
41
  PointsEditor,
42
+ usePointsEditorContext,
41
43
  clampPoint,
42
44
  type PointsEditorProps,
43
- type PointProps,
45
+ type PointsEditorContextValue,
46
+ type PointsEditorBackgroundProps,
47
+ type PointsEditorContainerProps,
48
+ type PointsEditorSelectionBoxProps,
49
+ type PointsEditorPointProps,
44
50
  type PointBaseType,
45
51
  } from './components/PointsEditor'
46
52
  export {
47
53
  Slider,
48
54
  useSliderContext,
49
- type ScaleOptionProps,
50
- type ScaleProps,
55
+ type SliderContextValue,
56
+ type SliderMarksOptionProps,
57
+ type MarksOptions,
58
+ type SliderMarksProps,
51
59
  type SliderMethods,
52
60
  type SliderProps,
53
61
  type SliderThumbMethods,
@@ -56,18 +64,21 @@ export {
56
64
  } from './components/Slider'
57
65
  export {
58
66
  XYPad,
67
+ useXYPadContext,
68
+ type XYPadContextValue,
59
69
  type XYPadProps,
60
70
  type XYPadMethods,
61
71
  type XYPadAreaProps,
62
72
  type XYPadThumbProps,
63
73
  type XYPadThumbMethods,
64
- type ValueOptions,
74
+ type XY,
75
+ type XYInput,
65
76
  } from './components/XYPad'
66
77
 
67
78
  // hooks
68
79
  export { useAnimationFrame } from './hooks/useAnimationFrame'
69
80
  export { useDrag } from './hooks/useDrag'
70
- export { useDragWithElement } from './hooks/useDragWithElement'
81
+ export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
71
82
  export { useEventListener } from './hooks/useEventListener'
72
83
  export { useInterval } from './hooks/useInterval'
73
84
  export { useLongPress } from './hooks/useLongPress'
@@ -75,8 +86,20 @@ export {
75
86
  useMIDIAccess,
76
87
  NOT_SUPPORTED,
77
88
  PERMISSION_DENIED,
89
+ UNAVAILABLE,
78
90
  type MIDIAccessError,
91
+ type MIDIAccessOptions,
79
92
  } from './hooks/useMIDIAccess'
80
- export { useMIDIInput } from './hooks/useMIDIInput'
93
+ export {
94
+ useMIDIInput,
95
+ PITCH_BEND_CENTER,
96
+ type MIDIInputHandlers,
97
+ } from './hooks/useMIDIInput'
81
98
  export { useMIDIMessage } from './hooks/useMIDIMessage'
82
- export { useWheel } from './hooks/useWheel'
99
+ export { useWheel, type UseWheelOptions } from './hooks/useWheel'
100
+
101
+ export {
102
+ DEFAULT_DRAG_SENSITIVITY,
103
+ DEFAULT_KEYBOARD_OPTIONS,
104
+ DEFAULT_WHEEL_OPTIONS,
105
+ } from './input-event'
@@ -0,0 +1,32 @@
1
+ import type { InputEventOption, ModifierValue } from '@tremolo-ui/functions'
2
+
3
+ /**
4
+ * The keyboard amount used by Knob, NumberInput, Slider, and XYPad by default:
5
+ * 1 per press in the units of the value, and 0.1 with shift held. That is one
6
+ * `step` only while `step` is 1; a coarser `step` rounds 1 straight back, which
7
+ * `useCheckSteps` warns about.
8
+ *
9
+ * A modifier entry is not snapped to `step`, which is what lets the finer
10
+ * amount move at all — see `applyDelta` in `@tremolo-ui/functions`.
11
+ */
12
+ export const DEFAULT_KEYBOARD_OPTIONS: ModifierValue<InputEventOption> = {
13
+ default: ['raw', 1],
14
+ shift: ['raw', 0.1],
15
+ }
16
+
17
+ /**
18
+ * The wheel amount used by Knob, NumberInput, Slider, and XYPad by default.
19
+ *
20
+ * Browsers turn shift+wheel into horizontal scrolling, which empties `deltaY`
21
+ * and fills `deltaX`, so no modifier is bound here.
22
+ */
23
+ export const DEFAULT_WHEEL_OPTIONS: ModifierValue<InputEventOption> = ['raw', 1]
24
+
25
+ /**
26
+ * The drag sensitivity used by value controls by default. Shift makes the
27
+ * same movement cover a tenth of the range, matching the arrow keys.
28
+ */
29
+ export const DEFAULT_DRAG_SENSITIVITY: ModifierValue<number> = {
30
+ default: 1,
31
+ shift: 0.1,
32
+ }