@tremolo-ui/react 0.5.0 → 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.
- 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 +1669 -541
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1571 -486
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1571 -486
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1640 -538
- package/dist/index.js.map +1 -1
- package/package.json +29 -27
- package/src/components/AnimationCanvas/index.tsx +115 -76
- package/src/components/DropZone/index.tsx +100 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +144 -0
- 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 +156 -60
- package/src/components/NumberInput/DecrementStepper.tsx +3 -10
- package/src/components/NumberInput/IncrementStepper.tsx +3 -10
- package/src/components/NumberInput/InputField.tsx +185 -108
- package/src/components/NumberInput/Stepper.tsx +120 -84
- package/src/components/NumberInput/StepperButton.tsx +100 -0
- package/src/components/NumberInput/context.tsx +34 -8
- package/src/components/NumberInput/index.tsx +270 -82
- package/src/components/Piano/index.tsx +363 -200
- package/src/components/PointsEditor/Background.tsx +13 -10
- package/src/components/PointsEditor/Container.tsx +83 -29
- package/src/components/PointsEditor/Point.tsx +241 -79
- package/src/components/PointsEditor/SelectionBox.tsx +48 -0
- package/src/components/PointsEditor/context.tsx +78 -10
- package/src/components/PointsEditor/index.tsx +440 -41
- package/src/components/Slider/Marks.tsx +48 -19
- package/src/components/Slider/MarksOption.tsx +64 -45
- package/src/components/Slider/Thumb.tsx +107 -55
- package/src/components/Slider/Track.tsx +66 -62
- package/src/components/Slider/context.tsx +4 -2
- package/src/components/Slider/index.tsx +136 -77
- package/src/components/Slider/type.ts +29 -9
- package/src/components/XYPad/Area.tsx +46 -41
- package/src/components/XYPad/Thumb.tsx +131 -64
- package/src/components/XYPad/context.tsx +4 -1
- package/src/components/XYPad/index.tsx +183 -53
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +12 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/components/_util/decimal-digits.ts +23 -0
- package/src/components/_util/visually-hidden.ts +25 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCallbackRef.ts +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +169 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +51 -12
- package/src/hooks/useDragValue.ts +71 -11
- package/src/hooks/useDropZone.ts +78 -0
- 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/hooks/useWheel.ts +4 -1
- package/src/index.ts +45 -25
- 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/NumberInput/stepperButton.tsx +0 -90
- 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
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createDropZone,
|
|
5
|
+
type DropZoneInstance,
|
|
6
|
+
type DropZoneOptions,
|
|
7
|
+
} from '@tremolo-ui/dom'
|
|
8
|
+
|
|
9
|
+
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
10
|
+
|
|
11
|
+
// `onStateChange` is left out: what it reports comes back from the hook as
|
|
12
|
+
// `over` and `invalid`, which is what a component renders from.
|
|
13
|
+
export interface UseDropZoneOptions extends Omit<
|
|
14
|
+
DropZoneOptions,
|
|
15
|
+
'onStateChange'
|
|
16
|
+
> {}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Take files dropped onto an element.
|
|
20
|
+
*
|
|
21
|
+
* Any element can become a drop target this way — a waveform display, a track
|
|
22
|
+
* lane, a canvas — without wrapping it in anything.
|
|
23
|
+
*
|
|
24
|
+
* @returns a ref callback for the element, and whether a drag is over it
|
|
25
|
+
*/
|
|
26
|
+
export function useDropZone<T extends Element>({
|
|
27
|
+
accept,
|
|
28
|
+
multiple,
|
|
29
|
+
disabled,
|
|
30
|
+
onDrop,
|
|
31
|
+
onReject,
|
|
32
|
+
}: UseDropZoneOptions = {}): {
|
|
33
|
+
refCallback: (node: T | null) => void
|
|
34
|
+
over: boolean
|
|
35
|
+
invalid: boolean
|
|
36
|
+
} {
|
|
37
|
+
const dropHandler = useCallbackRef(onDrop)
|
|
38
|
+
const rejectHandler = useCallbackRef(onReject)
|
|
39
|
+
|
|
40
|
+
const [state, setState] = useState({ over: false, invalid: false })
|
|
41
|
+
|
|
42
|
+
// See useDrag for why the node is held in state.
|
|
43
|
+
const [node, setNode] = useState<T | null>(null)
|
|
44
|
+
|
|
45
|
+
const instanceRef = useRef<DropZoneInstance | null>(null)
|
|
46
|
+
|
|
47
|
+
// Read when the instance is created. The effect below keeps it current, and
|
|
48
|
+
// runs right after, so a stale setting is replaced within the same commit.
|
|
49
|
+
const latest = useRef({ accept, multiple, disabled })
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!node) return
|
|
53
|
+
|
|
54
|
+
const instance = createDropZone(node, {
|
|
55
|
+
...latest.current,
|
|
56
|
+
onDrop: (files, event) => dropHandler(files, event),
|
|
57
|
+
onReject: (files, event) => rejectHandler(files, event),
|
|
58
|
+
onStateChange: (next) => setState(next),
|
|
59
|
+
})
|
|
60
|
+
instanceRef.current = instance
|
|
61
|
+
|
|
62
|
+
return () => {
|
|
63
|
+
instanceRef.current = null
|
|
64
|
+
instance.destroy()
|
|
65
|
+
// The listeners are gone, so no `dragleave` is coming to clear this.
|
|
66
|
+
setState({ over: false, invalid: false })
|
|
67
|
+
}
|
|
68
|
+
}, [node, dropHandler, rejectHandler])
|
|
69
|
+
|
|
70
|
+
// Runs after every render: the settings come from props and are cheap to
|
|
71
|
+
// push, and updating in place leaves a drag in progress untouched.
|
|
72
|
+
useEffect(() => {
|
|
73
|
+
latest.current = { accept, multiple, disabled }
|
|
74
|
+
instanceRef.current?.update({ accept, multiple, disabled })
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
return { refCallback: setNode, over: state.over, invalid: state.invalid }
|
|
78
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from 'react'
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
4
4
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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 {
|
|
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/hooks/useWheel.ts
CHANGED
|
@@ -4,7 +4,10 @@ import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
|
|
|
4
4
|
|
|
5
5
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
// `onWheel` is left out: `createWheel` takes it so that `update()` can replace
|
|
8
|
+
// the callback, but this hook takes the handler as its first argument and the
|
|
9
|
+
// option would be silently ignored.
|
|
10
|
+
export interface UseWheelOptions extends Omit<WheelOptions, 'onWheel'> {
|
|
8
11
|
/**
|
|
9
12
|
* Listen on this element rather than on the one the returned ref callback is
|
|
10
13
|
* attached to, and ignore that callback.
|
package/src/index.ts
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
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 { DropZone, type DropZoneProps } from './components/DropZone'
|
|
11
|
+
export {
|
|
12
|
+
FileInput,
|
|
13
|
+
useFileInputContext,
|
|
14
|
+
type FileInputContextValue,
|
|
15
|
+
type FileInputProps,
|
|
16
|
+
type FileInputTriggerProps,
|
|
17
|
+
} from './components/FileInput'
|
|
18
|
+
export {
|
|
19
|
+
Knob,
|
|
20
|
+
useKnobContext,
|
|
21
|
+
type KnobContextValue,
|
|
22
|
+
type KnobProps,
|
|
23
|
+
type KnobMethods,
|
|
24
|
+
type KnobSVGRootProps,
|
|
25
|
+
type KnobThumbProps,
|
|
26
|
+
} from './components/Knob'
|
|
20
27
|
export {
|
|
21
28
|
NumberInput,
|
|
22
29
|
useNumberInputContext,
|
|
30
|
+
type NumberInputContextValue,
|
|
23
31
|
type NumberInputProps,
|
|
24
32
|
type NumberInputMethods,
|
|
25
|
-
type NumberInputFieldProps,
|
|
26
|
-
type StepperProps,
|
|
27
|
-
type IncrementStepperProps,
|
|
28
|
-
type DecrementStepperProps,
|
|
29
33
|
} from './components/NumberInput'
|
|
30
34
|
export {
|
|
31
35
|
Piano,
|
|
@@ -34,7 +38,6 @@ export {
|
|
|
34
38
|
type PianoMethods,
|
|
35
39
|
type KeyState,
|
|
36
40
|
type KeyAttributes,
|
|
37
|
-
type CSSVariables,
|
|
38
41
|
type KeyboardShortcuts,
|
|
39
42
|
} from './components/Piano'
|
|
40
43
|
export {
|
|
@@ -43,16 +46,16 @@ export {
|
|
|
43
46
|
clampPoint,
|
|
44
47
|
type PointsEditorProps,
|
|
45
48
|
type PointsEditorContextValue,
|
|
46
|
-
type
|
|
47
|
-
type PointsEditorContainerProps,
|
|
48
|
-
type PointProps,
|
|
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,
|
|
@@ -70,10 +75,13 @@ export {
|
|
|
70
75
|
type XYInput,
|
|
71
76
|
} from './components/XYPad'
|
|
72
77
|
|
|
78
|
+
export { type CSSVariables } from './css-variables'
|
|
79
|
+
|
|
73
80
|
// hooks
|
|
74
81
|
export { useAnimationFrame } from './hooks/useAnimationFrame'
|
|
75
|
-
export { useDrag } from './hooks/useDrag'
|
|
82
|
+
export { useDrag, type UseDragOptions } from './hooks/useDrag'
|
|
76
83
|
export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
|
|
84
|
+
export { useDropZone, type UseDropZoneOptions } from './hooks/useDropZone'
|
|
77
85
|
export { useEventListener } from './hooks/useEventListener'
|
|
78
86
|
export { useInterval } from './hooks/useInterval'
|
|
79
87
|
export { useLongPress } from './hooks/useLongPress'
|
|
@@ -81,8 +89,20 @@ export {
|
|
|
81
89
|
useMIDIAccess,
|
|
82
90
|
NOT_SUPPORTED,
|
|
83
91
|
PERMISSION_DENIED,
|
|
92
|
+
UNAVAILABLE,
|
|
84
93
|
type MIDIAccessError,
|
|
94
|
+
type MIDIAccessOptions,
|
|
85
95
|
} from './hooks/useMIDIAccess'
|
|
86
|
-
export {
|
|
96
|
+
export {
|
|
97
|
+
useMIDIInput,
|
|
98
|
+
PITCH_BEND_CENTER,
|
|
99
|
+
type MIDIInputHandlers,
|
|
100
|
+
} from './hooks/useMIDIInput'
|
|
87
101
|
export { useMIDIMessage } from './hooks/useMIDIMessage'
|
|
88
102
|
export { useWheel, type UseWheelOptions } from './hooks/useWheel'
|
|
103
|
+
|
|
104
|
+
export {
|
|
105
|
+
DEFAULT_DRAG_SENSITIVITY,
|
|
106
|
+
DEFAULT_KEYBOARD_OPTIONS,
|
|
107
|
+
DEFAULT_WHEEL_OPTIONS,
|
|
108
|
+
} from './input-event'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type InputEventOption, type ModifierValue } from '@tremolo-ui/dom'
|
|
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/dom`.
|
|
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
|
+
}
|