@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.
- package/dist/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +1468 -534
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1103 -435
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1103 -435
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1457 -534
- package/dist/index.js.map +1 -1
- package/package.json +28 -26
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/Knob/ActiveLine.tsx +7 -6
- package/src/components/Knob/InactiveLine.tsx +9 -10
- package/src/components/Knob/SVGRoot.tsx +5 -3
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +17 -1
- package/src/components/Knob/index.tsx +148 -55
- package/src/components/NumberInput/DecrementStepper.tsx +8 -4
- package/src/components/NumberInput/IncrementStepper.tsx +9 -4
- package/src/components/NumberInput/InputField.tsx +188 -105
- package/src/components/NumberInput/Stepper.tsx +123 -72
- package/src/components/NumberInput/{stepperButton.tsx → StepperButton.tsx} +27 -22
- package/src/components/NumberInput/context.tsx +35 -8
- package/src/components/NumberInput/index.tsx +272 -75
- package/src/components/Piano/index.tsx +354 -188
- package/src/components/PointsEditor/Background.tsx +11 -2
- package/src/components/PointsEditor/Container.tsx +87 -27
- package/src/components/PointsEditor/Point.tsx +239 -79
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -39
- package/src/components/Slider/Marks.tsx +48 -16
- package/src/components/Slider/MarksOption.tsx +57 -43
- package/src/components/Slider/Thumb.tsx +108 -50
- package/src/components/Slider/Track.tsx +69 -58
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +131 -65
- package/src/components/Slider/type.ts +26 -8
- package/src/components/XYPad/Area.tsx +45 -35
- package/src/components/XYPad/Thumb.tsx +135 -62
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +180 -48
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/hooks/_internal/useCallbackRef.ts +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +170 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +38 -8
- package/src/hooks/useDragValue.ts +54 -9
- package/src/hooks/useEventListener.ts +10 -6
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/index.ts +38 -21
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -501
- package/src/Globals.d.ts +0 -3
- package/src/components/Knob/index.css +0 -81
- package/src/components/NumberInput/index.css +0 -121
- package/src/components/Piano/index.css +0 -90
- package/src/components/PointsEditor/index.css +0 -35
- package/src/components/Slider/index.css +0 -125
- package/src/components/XYPad/index.css +0 -60
- package/src/components/_util/index.ts +0 -28
- package/src/styles/global.css +0 -19
- /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 {
|
|
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
|
|
85
|
+
const store = useMemo(() => createMIDIAccessStore(), [])
|
|
16
86
|
|
|
17
|
-
const { midiAccess, error } = useSyncExternalStore(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
87
|
+
const { midiAccess, error, inputs } = useSyncExternalStore(
|
|
88
|
+
store.subscribe,
|
|
89
|
+
store.getState,
|
|
90
|
+
store.getServerState,
|
|
21
91
|
)
|
|
22
92
|
|
|
23
93
|
useEffect(() => {
|
|
24
|
-
|
|
25
|
-
|
|
94
|
+
const instance = store.connect()
|
|
95
|
+
return () => store.disconnect(instance)
|
|
96
|
+
}, [store])
|
|
26
97
|
|
|
27
98
|
useEffect(() => {
|
|
28
|
-
|
|
29
|
-
}, [
|
|
99
|
+
if (requestOnMount) store.request()
|
|
100
|
+
}, [store, requestOnMount])
|
|
30
101
|
|
|
31
|
-
return { request:
|
|
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 {
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
3
|
+
type AnimationCanvasCommonProps,
|
|
4
|
+
type AnimationCanvasFixedProps,
|
|
13
5
|
type AnimationCanvasProps,
|
|
14
|
-
type
|
|
6
|
+
type AnimationCanvasResizableProps,
|
|
15
7
|
type DrawFunction,
|
|
16
8
|
type InitFunction,
|
|
17
|
-
type RelativeSizingProps,
|
|
18
9
|
} from './components/AnimationCanvas'
|
|
19
|
-
export {
|
|
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
|
|
26
|
-
type
|
|
27
|
-
type
|
|
28
|
-
type
|
|
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
|
|
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
|
|
55
|
-
type
|
|
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 {
|
|
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
|
+
}
|