@tremolo-ui/react 0.5.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 (76) 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 +1468 -534
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1103 -435
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1103 -435
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1457 -534
  16. package/dist/index.js.map +1 -1
  17. package/package.json +28 -26
  18. package/src/components/AnimationCanvas/index.tsx +115 -76
  19. package/src/components/Knob/ActiveLine.tsx +7 -6
  20. package/src/components/Knob/InactiveLine.tsx +9 -10
  21. package/src/components/Knob/SVGRoot.tsx +5 -3
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +17 -1
  24. package/src/components/Knob/index.tsx +148 -55
  25. package/src/components/NumberInput/DecrementStepper.tsx +8 -4
  26. package/src/components/NumberInput/IncrementStepper.tsx +9 -4
  27. package/src/components/NumberInput/InputField.tsx +188 -105
  28. package/src/components/NumberInput/Stepper.tsx +123 -72
  29. package/src/components/NumberInput/{stepperButton.tsx → StepperButton.tsx} +27 -22
  30. package/src/components/NumberInput/context.tsx +35 -8
  31. package/src/components/NumberInput/index.tsx +272 -75
  32. package/src/components/Piano/index.tsx +354 -188
  33. package/src/components/PointsEditor/Background.tsx +11 -2
  34. package/src/components/PointsEditor/Container.tsx +87 -27
  35. package/src/components/PointsEditor/Point.tsx +239 -79
  36. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  37. package/src/components/PointsEditor/context.tsx +78 -10
  38. package/src/components/PointsEditor/index.tsx +440 -39
  39. package/src/components/Slider/Marks.tsx +48 -16
  40. package/src/components/Slider/MarksOption.tsx +57 -43
  41. package/src/components/Slider/Thumb.tsx +108 -50
  42. package/src/components/Slider/Track.tsx +69 -58
  43. package/src/components/Slider/context.tsx +4 -2
  44. package/src/components/Slider/index.tsx +131 -65
  45. package/src/components/Slider/type.ts +26 -8
  46. package/src/components/XYPad/Area.tsx +45 -35
  47. package/src/components/XYPad/Thumb.tsx +135 -62
  48. package/src/components/XYPad/context.tsx +4 -1
  49. package/src/components/XYPad/index.tsx +180 -48
  50. package/src/components/_util/Placement.tsx +68 -0
  51. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  52. package/src/components/_util/css-length.ts +13 -0
  53. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  54. package/src/hooks/_internal/useCallbackRef.ts +3 -1
  55. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  56. package/src/hooks/useAnimationFrame.ts +17 -7
  57. package/src/hooks/useDrag.ts +38 -8
  58. package/src/hooks/useDragValue.ts +54 -9
  59. package/src/hooks/useEventListener.ts +10 -6
  60. package/src/hooks/useLongPress.ts +42 -8
  61. package/src/hooks/useMIDIAccess.ts +82 -11
  62. package/src/hooks/useMIDIInput.ts +45 -13
  63. package/src/hooks/useMIDIMessage.ts +23 -5
  64. package/src/index.ts +38 -21
  65. package/src/input-event.ts +32 -0
  66. package/dist/index.css +0 -501
  67. package/src/Globals.d.ts +0 -3
  68. package/src/components/Knob/index.css +0 -81
  69. package/src/components/NumberInput/index.css +0 -121
  70. package/src/components/Piano/index.css +0 -90
  71. package/src/components/PointsEditor/index.css +0 -35
  72. package/src/components/Slider/index.css +0 -125
  73. package/src/components/XYPad/index.css +0 -60
  74. package/src/components/_util/index.ts +0 -28
  75. package/src/styles/global.css +0 -19
  76. /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
@@ -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
  }
package/src/index.ts CHANGED
@@ -1,31 +1,31 @@
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,
22
21
  useNumberInputContext,
22
+ type NumberInputContextValue,
23
23
  type NumberInputProps,
24
24
  type NumberInputMethods,
25
- type NumberInputFieldProps,
26
- type StepperProps,
27
- type IncrementStepperProps,
28
- type DecrementStepperProps,
25
+ type NumberInputInputFieldProps,
26
+ type NumberInputStepperProps,
27
+ type NumberInputIncrementStepperProps,
28
+ type NumberInputDecrementStepperProps,
29
29
  } from './components/NumberInput'
30
30
  export {
31
31
  Piano,
@@ -45,14 +45,17 @@ export {
45
45
  type PointsEditorContextValue,
46
46
  type PointsEditorBackgroundProps,
47
47
  type PointsEditorContainerProps,
48
- type PointProps,
48
+ type PointsEditorSelectionBoxProps,
49
+ type PointsEditorPointProps,
49
50
  type PointBaseType,
50
51
  } from './components/PointsEditor'
51
52
  export {
52
53
  Slider,
53
54
  useSliderContext,
54
- type MarksOptionProps,
55
- type MarksProps,
55
+ type SliderContextValue,
56
+ type SliderMarksOptionProps,
57
+ type MarksOptions,
58
+ type SliderMarksProps,
56
59
  type SliderMethods,
57
60
  type SliderProps,
58
61
  type SliderThumbMethods,
@@ -61,6 +64,8 @@ export {
61
64
  } from './components/Slider'
62
65
  export {
63
66
  XYPad,
67
+ useXYPadContext,
68
+ type XYPadContextValue,
64
69
  type XYPadProps,
65
70
  type XYPadMethods,
66
71
  type XYPadAreaProps,
@@ -81,8 +86,20 @@ export {
81
86
  useMIDIAccess,
82
87
  NOT_SUPPORTED,
83
88
  PERMISSION_DENIED,
89
+ UNAVAILABLE,
84
90
  type MIDIAccessError,
91
+ type MIDIAccessOptions,
85
92
  } from './hooks/useMIDIAccess'
86
- export { useMIDIInput } from './hooks/useMIDIInput'
93
+ export {
94
+ useMIDIInput,
95
+ PITCH_BEND_CENTER,
96
+ type MIDIInputHandlers,
97
+ } from './hooks/useMIDIInput'
87
98
  export { useMIDIMessage } from './hooks/useMIDIMessage'
88
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
+ }