@tremolo-ui/react 0.6.0 → 0.8.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/index.cjs +522 -997
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +601 -329
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +601 -329
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +487 -953
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/DropZone/index.tsx +101 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +139 -0
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +3 -2
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +27 -21
- package/src/components/NumberInput/DecrementStepper.tsx +2 -13
- package/src/components/NumberInput/IncrementStepper.tsx +2 -14
- package/src/components/NumberInput/InputField.tsx +17 -38
- package/src/components/NumberInput/Stepper.tsx +44 -107
- package/src/components/NumberInput/StepperButton.tsx +16 -12
- package/src/components/NumberInput/context.tsx +8 -8
- package/src/components/NumberInput/index.tsx +41 -81
- package/src/components/Piano/index.tsx +39 -149
- package/src/components/PointsEditor/Background.tsx +2 -8
- package/src/components/PointsEditor/Container.tsx +17 -34
- package/src/components/PointsEditor/Point.tsx +41 -86
- package/src/components/PointsEditor/SelectionBox.tsx +2 -8
- package/src/components/PointsEditor/context.tsx +10 -61
- package/src/components/PointsEditor/index.tsx +31 -315
- package/src/components/Slider/Marks.tsx +8 -13
- package/src/components/Slider/MarksOption.tsx +11 -17
- package/src/components/Slider/Thumb.tsx +9 -15
- package/src/components/Slider/Track.tsx +7 -13
- package/src/components/Slider/index.tsx +33 -39
- package/src/components/XYPad/Area.tsx +26 -53
- package/src/components/XYPad/Thumb.tsx +8 -12
- package/src/components/XYPad/context.tsx +1 -3
- package/src/components/XYPad/index.tsx +35 -50
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -147
- package/src/hooks/useDrag.ts +13 -4
- package/src/hooks/useDragValue.ts +17 -2
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +14 -36
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -44
- package/src/components/_util/css-length.ts +0 -13
- package/src/input-event.ts +0 -32
|
@@ -1,163 +1,25 @@
|
|
|
1
1
|
import { useEffect } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
applyDelta,
|
|
5
|
-
linearScale,
|
|
6
|
-
type InputEventOption,
|
|
7
|
-
type ModifierState,
|
|
8
|
-
type ModifierValue,
|
|
9
|
-
type ValueRange,
|
|
10
|
-
} from '@tremolo-ui/functions'
|
|
11
|
-
|
|
12
|
-
/** Positions probed across the travel. The ends are left out so that the
|
|
13
|
-
* clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
|
|
14
|
-
const PROBES = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
|
15
|
-
|
|
16
|
-
const NONE: ModifierState = {
|
|
17
|
-
shiftKey: false,
|
|
18
|
-
altKey: false,
|
|
19
|
-
ctrlKey: false,
|
|
20
|
-
metaKey: false,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const MODIFIER_STATE: Record<string, ModifierState> = {
|
|
24
|
-
shift: { ...NONE, shiftKey: true },
|
|
25
|
-
alt: { ...NONE, altKey: true },
|
|
26
|
-
ctrl: { ...NONE, ctrlKey: true },
|
|
27
|
-
meta: { ...NONE, metaKey: true },
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Each entry of a modifier-aware input option, as a name and held keys. */
|
|
31
|
-
function entries(
|
|
32
|
-
options: ModifierValue<InputEventOption>,
|
|
33
|
-
): { name: string; modifiers: ModifierState }[] {
|
|
34
|
-
if (Array.isArray(options)) return [{ name: '', modifiers: NONE }]
|
|
35
|
-
return Object.keys(options).map((key) => ({
|
|
36
|
-
name: key === 'default' ? '' : key,
|
|
37
|
-
modifiers: MODIFIER_STATE[key] ?? NONE,
|
|
38
|
-
}))
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
interface Outcome {
|
|
42
|
-
/** The press changed the value at least once across the travel. */
|
|
43
|
-
moved: boolean
|
|
44
|
-
/** The change reached the displayed text at least once. */
|
|
45
|
-
visible: boolean
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Press every entry of `options` at nine points along the travel and report
|
|
50
|
-
* whether anything came of it.
|
|
51
|
-
*
|
|
52
|
-
* Run against `applyDelta` itself rather than against a reading of `step`:
|
|
53
|
-
* the whole point is that the amount, the step and the scale interact, and
|
|
54
|
-
* the pipeline is the only thing that knows how.
|
|
55
|
-
*/
|
|
56
|
-
function probe(
|
|
57
|
-
options: ModifierValue<InputEventOption>,
|
|
58
|
-
range: ValueRange,
|
|
59
|
-
format?: (value: number) => string,
|
|
60
|
-
): Outcome {
|
|
61
|
-
const { min, max, scale = linearScale } = range
|
|
62
|
-
const outcome: Outcome = { moved: false, visible: false }
|
|
63
|
-
|
|
64
|
-
for (const { modifiers } of entries(options)) {
|
|
65
|
-
for (const position of PROBES) {
|
|
66
|
-
const value = scale.denormalize(position, min, max)
|
|
67
|
-
const up = applyDelta(value, 1, options, range, modifiers)
|
|
68
|
-
const down = applyDelta(value, -1, options, range, modifiers)
|
|
69
|
-
if (up !== value || down !== value) outcome.moved = true
|
|
70
|
-
if (!format) continue
|
|
71
|
-
const shown = format(value)
|
|
72
|
-
if (format(up) !== shown || format(down) !== shown) outcome.visible = true
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return outcome
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export interface CheckStepsOptions {
|
|
80
|
-
/** The component, for the message. */
|
|
81
|
-
component: string
|
|
82
|
-
/** The axis, for a component that has more than one. */
|
|
83
|
-
axis?: string
|
|
84
|
-
/**
|
|
85
|
-
* The range to probe, or `null` to check nothing. An unbounded input has no
|
|
86
|
-
* travel to sample, so `NumberInput` passes `null` when `min` and `max` are
|
|
87
|
-
* not both there.
|
|
88
|
-
*/
|
|
89
|
-
range: ValueRange | null
|
|
90
|
-
keyboard?: ModifierValue<InputEventOption> | null
|
|
91
|
-
wheel?: ModifierValue<InputEventOption> | null
|
|
92
|
-
/**
|
|
93
|
-
* How the value is displayed, where the component shows one. Called with
|
|
94
|
-
* probe values only, and only in development.
|
|
95
|
-
*/
|
|
96
|
-
format?: (value: number) => string
|
|
97
|
-
}
|
|
3
|
+
import { checkSteps, type CheckStepsOptions } from '@tremolo-ui/dom'
|
|
98
4
|
|
|
99
5
|
/**
|
|
100
6
|
* Warn, in development only, when a key press or a wheel notch cannot produce
|
|
101
|
-
* a change the user can see.
|
|
102
|
-
*
|
|
103
|
-
* Two settings that are each fine on their own can cancel out, and nothing
|
|
104
|
-
* fails when they do — the control simply sits there:
|
|
105
|
-
*
|
|
106
|
-
* - **`step` coarser than the amount.** `keyboard={['raw', 0.1]}` with
|
|
107
|
-
* `step={1}` rounds every press straight back to where it started
|
|
108
|
-
* - **the display coarser than the amount.** A `format` showing two decimals
|
|
109
|
-
* of a kHz value cannot show a press worth 1 Hz
|
|
110
|
-
*
|
|
111
|
-
* The second is only reported when the press is invisible at *every* point
|
|
112
|
-
* along the travel. A display that rounds is a deliberate choice and is
|
|
113
|
-
* normally right — it is being too coarse everywhere that makes it a mistake.
|
|
7
|
+
* a change the user can see. The probing is `checkSteps` in the core.
|
|
114
8
|
*
|
|
115
9
|
* @internal
|
|
116
10
|
*/
|
|
117
|
-
export function useCheckSteps({
|
|
118
|
-
component,
|
|
119
|
-
axis,
|
|
120
|
-
range,
|
|
121
|
-
keyboard,
|
|
122
|
-
wheel,
|
|
123
|
-
format,
|
|
124
|
-
}: CheckStepsOptions) {
|
|
11
|
+
export function useCheckSteps(options: CheckStepsOptions) {
|
|
12
|
+
const { component, axis, range, keyboard, wheel } = options
|
|
125
13
|
const { min, max, step, scale } = range ?? {}
|
|
126
|
-
const where = axis ? `${component} (${axis})` : component
|
|
127
14
|
|
|
128
15
|
useEffect(() => {
|
|
129
16
|
try {
|
|
130
17
|
// Inline and first, so a bundler folds the comparison and drops the
|
|
131
|
-
// whole block —
|
|
132
|
-
//
|
|
133
|
-
// it is wrapped.
|
|
18
|
+
// whole block — and with it the only call to `checkSteps`, which then
|
|
19
|
+
// falls out of the bundle too. See `useCheckPlacement` for why it
|
|
20
|
+
// cannot go through a helper, and why it is wrapped.
|
|
134
21
|
if (process.env.NODE_ENV === 'production') return
|
|
135
|
-
|
|
136
|
-
return
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
for (const [name, options] of [
|
|
140
|
-
['keyboard', keyboard],
|
|
141
|
-
['wheel', wheel],
|
|
142
|
-
] as const) {
|
|
143
|
-
if (!options) continue
|
|
144
|
-
const { moved, visible } = probe(options, range, format)
|
|
145
|
-
if (!moved) {
|
|
146
|
-
console.warn(
|
|
147
|
-
`[tremolo-ui] ${where}: \`${name}\` cannot move the value.` +
|
|
148
|
-
(step !== undefined
|
|
149
|
-
? ` Each press is smaller than \`step\` (${step}), so it rounds`
|
|
150
|
-
: ' Each press rounds') +
|
|
151
|
-
' straight back to where it started.',
|
|
152
|
-
)
|
|
153
|
-
} else if (format && !visible) {
|
|
154
|
-
console.warn(
|
|
155
|
-
`[tremolo-ui] ${where}: \`${name}\` moves the value, but \`format\`` +
|
|
156
|
-
' shows the same text before and after, everywhere in the range.' +
|
|
157
|
-
' The display is too coarse for it to be seen.',
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
22
|
+
for (const warning of checkSteps(options)) console.warn(warning)
|
|
161
23
|
} catch {
|
|
162
24
|
// No bundler substituted NODE_ENV, so a production build cannot be told
|
|
163
25
|
// from a development one. Say nothing rather than break.
|
|
@@ -166,5 +28,5 @@ export function useCheckSteps({
|
|
|
166
28
|
// on what actually decides the outcome. `format` is left out on purpose:
|
|
167
29
|
// an inline arrow function would make this run every render.
|
|
168
30
|
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
169
|
-
}, [
|
|
31
|
+
}, [component, axis, min, max, step, scale, keyboard, wheel])
|
|
170
32
|
}
|
package/src/hooks/useDrag.ts
CHANGED
|
@@ -4,10 +4,14 @@ import { createDrag, type DragState } from '@tremolo-ui/dom'
|
|
|
4
4
|
|
|
5
5
|
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
6
|
|
|
7
|
-
interface
|
|
7
|
+
export interface UseDragOptions {
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* Pixels the pointer has to travel before the drag counts as one.
|
|
10
|
+
*
|
|
11
|
+
* A click moves the pointer by a pixel or two, so without a threshold a
|
|
12
|
+
* double click reports a drag between the two presses.
|
|
13
|
+
*
|
|
14
|
+
* @default 1
|
|
11
15
|
*/
|
|
12
16
|
threshold?: number
|
|
13
17
|
|
|
@@ -24,6 +28,9 @@ interface UseDragProps {
|
|
|
24
28
|
pointerLock?: boolean
|
|
25
29
|
|
|
26
30
|
/**
|
|
31
|
+
* Called on every move once the drag has started, with the pointer position
|
|
32
|
+
* relative to the element and how far it moved since the last call.
|
|
33
|
+
*
|
|
27
34
|
* @param state the whole drag, for anything the four numbers leave out —
|
|
28
35
|
* the pointer event and its modifier keys, most of all.
|
|
29
36
|
*/
|
|
@@ -34,7 +41,9 @@ interface UseDragProps {
|
|
|
34
41
|
deltaY: number,
|
|
35
42
|
state: DragState,
|
|
36
43
|
) => void
|
|
44
|
+
/** Called once the pointer has moved past `threshold`, not on pointerdown. */
|
|
37
45
|
onDragStart?: (state: DragState) => void
|
|
46
|
+
/** Called when the pointer is released, only if the drag ever started. */
|
|
38
47
|
onDragEnd?: (state: DragState) => void
|
|
39
48
|
}
|
|
40
49
|
|
|
@@ -50,7 +59,7 @@ export function useDrag<T extends Element>({
|
|
|
50
59
|
onDrag,
|
|
51
60
|
onDragStart,
|
|
52
61
|
onDragEnd,
|
|
53
|
-
}:
|
|
62
|
+
}: UseDragOptions): (node: T | null) => void {
|
|
54
63
|
const dragHandler = useCallbackRef(onDrag)
|
|
55
64
|
const dragStartHandler = useCallbackRef(onDragStart)
|
|
56
65
|
const dragEndHandler = useCallbackRef(onDragEnd)
|
|
@@ -51,9 +51,19 @@ export interface UseDragValueOptions {
|
|
|
51
51
|
*/
|
|
52
52
|
sensitivity?: (state: DragState) => number
|
|
53
53
|
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Move the value on pointerdown, before the pointer has moved at all. With
|
|
56
|
+
* `baseElementRef` that makes a press jump the value to where it landed.
|
|
57
|
+
*
|
|
58
|
+
* @see DragValueOptions.updateOnPointerDown
|
|
59
|
+
*/
|
|
55
60
|
updateOnPointerDown?: boolean
|
|
56
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Pixels the pointer has to travel before the drag counts as one. The
|
|
63
|
+
* default is 0, so the value follows the very first move.
|
|
64
|
+
*
|
|
65
|
+
* @default 0
|
|
66
|
+
*/
|
|
57
67
|
threshold?: number
|
|
58
68
|
/** CSS cursor to show while dragging. Applied to the element itself. */
|
|
59
69
|
cursor?: string
|
|
@@ -75,11 +85,16 @@ export interface UseDragValueOptions {
|
|
|
75
85
|
shouldStart?: (event: PointerEvent) => boolean
|
|
76
86
|
|
|
77
87
|
/**
|
|
88
|
+
* Called with the new value whenever the drag moves it. The hook holds no
|
|
89
|
+
* value of its own, so this is where it is stored.
|
|
90
|
+
*
|
|
78
91
|
* @param state the whole drag, for anything the value leaves out — the
|
|
79
92
|
* pointer event and its modifier keys, most of all.
|
|
80
93
|
*/
|
|
81
94
|
onChange?: (value: XY<number>, state: DragState) => void
|
|
95
|
+
/** Called once the drag starts, with the value it starts from. */
|
|
82
96
|
onDragStart?: (value: XY<number>, state: DragState) => void
|
|
97
|
+
/** Called when the pointer is released, with the value it ends on. */
|
|
83
98
|
onDragEnd?: (value: XY<number>, state: DragState) => void
|
|
84
99
|
}
|
|
85
100
|
|
|
@@ -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,65 +1,49 @@
|
|
|
1
|
-
import { useCallback,
|
|
1
|
+
import { useCallback, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useInterval } from './useInterval'
|
|
3
|
+
import { createLongPress, type LongPressInstance } from '@tremolo-ui/dom'
|
|
5
4
|
|
|
5
|
+
import { useCallbackRef } from './_internal/useCallbackRef'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Repeat `callback` while a pointer is held down: once on the press, then
|
|
9
|
+
* every `interval` after `initialDelay`. The repeat itself is
|
|
10
|
+
* `createLongPress` in `@tremolo-ui/dom`.
|
|
11
|
+
*
|
|
12
|
+
* Returns the function that starts a press, for an `onPointerDown`.
|
|
13
|
+
*/
|
|
6
14
|
export function useLongPress(
|
|
7
15
|
callback: () => void,
|
|
8
16
|
initialDelay = 500,
|
|
9
17
|
interval = 40,
|
|
10
18
|
) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
useInterval(
|
|
17
|
-
() => {
|
|
18
|
-
callback()
|
|
19
|
-
setDelay(interval)
|
|
20
|
-
},
|
|
21
|
-
pressed ? delay : null,
|
|
22
|
-
)
|
|
23
|
-
|
|
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
|
-
)
|
|
19
|
+
// Updated before layout effects run, so that a press started from one
|
|
20
|
+
// right after a re-render already calls the new callback.
|
|
21
|
+
const onPress = useCallbackRef(callback)
|
|
22
|
+
const instanceRef = useRef<LongPressInstance | null>(null)
|
|
41
23
|
|
|
42
|
-
|
|
43
|
-
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
instanceRef.current?.update({ delay: initialDelay, interval })
|
|
44
26
|
})
|
|
45
27
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const instance = createLongPress({
|
|
30
|
+
onPress: () => onPress(),
|
|
31
|
+
delay: initialDelay,
|
|
32
|
+
interval,
|
|
33
|
+
})
|
|
34
|
+
instanceRef.current = instance
|
|
35
|
+
return () => {
|
|
36
|
+
instanceRef.current = null
|
|
37
|
+
instance.destroy()
|
|
38
|
+
}
|
|
39
|
+
// Created once: the options are pushed with update() above, so that a new
|
|
40
|
+
// delay does not end a press in progress.
|
|
41
|
+
// oxlint-disable-next-line react-hooks/exhaustive-deps
|
|
42
|
+
}, [])
|
|
53
43
|
|
|
54
44
|
return useCallback(
|
|
55
|
-
(event?: Pick<PointerEvent, 'button' | 'pointerId'>) =>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
isPressed.current = true
|
|
59
|
-
activePointerId.current = event?.pointerId ?? null
|
|
60
|
-
callback()
|
|
61
|
-
setPressed(true)
|
|
62
|
-
},
|
|
63
|
-
[callback],
|
|
45
|
+
(event?: Pick<PointerEvent, 'button' | 'pointerId'>) =>
|
|
46
|
+
instanceRef.current?.start(event),
|
|
47
|
+
[],
|
|
64
48
|
)
|
|
65
49
|
}
|
|
@@ -7,14 +7,6 @@ import {
|
|
|
7
7
|
type MIDIAccessState,
|
|
8
8
|
} from '@tremolo-ui/dom'
|
|
9
9
|
|
|
10
|
-
export {
|
|
11
|
-
NOT_SUPPORTED,
|
|
12
|
-
PERMISSION_DENIED,
|
|
13
|
-
UNAVAILABLE,
|
|
14
|
-
type MIDIAccessError,
|
|
15
|
-
type MIDIAccessOptions,
|
|
16
|
-
} from '@tremolo-ui/dom'
|
|
17
|
-
|
|
18
10
|
const INITIAL_STATE: MIDIAccessState = {
|
|
19
11
|
midiAccess: null,
|
|
20
12
|
error: null,
|
|
@@ -2,13 +2,10 @@ import { useEffect, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
createMIDIInput,
|
|
5
|
-
PITCH_BEND_CENTER,
|
|
6
5
|
type MIDIInputHandlers,
|
|
7
6
|
type MIDIInputInstance,
|
|
8
7
|
} from '@tremolo-ui/dom'
|
|
9
8
|
|
|
10
|
-
export { PITCH_BEND_CENTER, type MIDIInputHandlers }
|
|
11
|
-
|
|
12
9
|
/**
|
|
13
10
|
* Handle MIDI input events. To be used with {@link useMIDIAccess}.
|
|
14
11
|
*
|
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
|
@@ -7,6 +7,14 @@ export {
|
|
|
7
7
|
type DrawFunction,
|
|
8
8
|
type InitFunction,
|
|
9
9
|
} from './components/AnimationCanvas'
|
|
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'
|
|
10
18
|
export {
|
|
11
19
|
Knob,
|
|
12
20
|
useKnobContext,
|
|
@@ -22,39 +30,26 @@ export {
|
|
|
22
30
|
type NumberInputContextValue,
|
|
23
31
|
type NumberInputProps,
|
|
24
32
|
type NumberInputMethods,
|
|
25
|
-
type NumberInputInputFieldProps,
|
|
26
|
-
type NumberInputStepperProps,
|
|
27
|
-
type NumberInputIncrementStepperProps,
|
|
28
|
-
type NumberInputDecrementStepperProps,
|
|
29
33
|
} from './components/NumberInput'
|
|
30
34
|
export {
|
|
31
35
|
Piano,
|
|
32
|
-
SHORTCUTS,
|
|
33
36
|
type PianoProps,
|
|
34
37
|
type PianoMethods,
|
|
35
38
|
type KeyState,
|
|
36
39
|
type KeyAttributes,
|
|
37
|
-
type CSSVariables,
|
|
38
|
-
type KeyboardShortcuts,
|
|
39
40
|
} from './components/Piano'
|
|
40
41
|
export {
|
|
41
42
|
PointsEditor,
|
|
42
43
|
usePointsEditorContext,
|
|
43
|
-
clampPoint,
|
|
44
44
|
type PointsEditorProps,
|
|
45
45
|
type PointsEditorContextValue,
|
|
46
|
-
type PointsEditorBackgroundProps,
|
|
47
|
-
type PointsEditorContainerProps,
|
|
48
|
-
type PointsEditorSelectionBoxProps,
|
|
49
46
|
type PointsEditorPointProps,
|
|
50
|
-
type PointBaseType,
|
|
51
47
|
} from './components/PointsEditor'
|
|
52
48
|
export {
|
|
53
49
|
Slider,
|
|
54
50
|
useSliderContext,
|
|
55
51
|
type SliderContextValue,
|
|
56
52
|
type SliderMarksOptionProps,
|
|
57
|
-
type MarksOptions,
|
|
58
53
|
type SliderMarksProps,
|
|
59
54
|
type SliderMethods,
|
|
60
55
|
type SliderProps,
|
|
@@ -68,38 +63,21 @@ export {
|
|
|
68
63
|
type XYPadContextValue,
|
|
69
64
|
type XYPadProps,
|
|
70
65
|
type XYPadMethods,
|
|
71
|
-
type XYPadAreaProps,
|
|
72
66
|
type XYPadThumbProps,
|
|
73
67
|
type XYPadThumbMethods,
|
|
74
|
-
type XY,
|
|
75
|
-
type XYInput,
|
|
76
68
|
} from './components/XYPad'
|
|
77
69
|
|
|
70
|
+
export { type CSSVariables } from './css-variables'
|
|
71
|
+
|
|
78
72
|
// hooks
|
|
79
73
|
export { useAnimationFrame } from './hooks/useAnimationFrame'
|
|
80
|
-
export { useDrag } from './hooks/useDrag'
|
|
74
|
+
export { useDrag, type UseDragOptions } from './hooks/useDrag'
|
|
81
75
|
export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
|
|
76
|
+
export { useDropZone, type UseDropZoneOptions } from './hooks/useDropZone'
|
|
82
77
|
export { useEventListener } from './hooks/useEventListener'
|
|
83
78
|
export { useInterval } from './hooks/useInterval'
|
|
84
79
|
export { useLongPress } from './hooks/useLongPress'
|
|
85
|
-
export {
|
|
86
|
-
|
|
87
|
-
NOT_SUPPORTED,
|
|
88
|
-
PERMISSION_DENIED,
|
|
89
|
-
UNAVAILABLE,
|
|
90
|
-
type MIDIAccessError,
|
|
91
|
-
type MIDIAccessOptions,
|
|
92
|
-
} from './hooks/useMIDIAccess'
|
|
93
|
-
export {
|
|
94
|
-
useMIDIInput,
|
|
95
|
-
PITCH_BEND_CENTER,
|
|
96
|
-
type MIDIInputHandlers,
|
|
97
|
-
} from './hooks/useMIDIInput'
|
|
80
|
+
export { useMIDIAccess } from './hooks/useMIDIAccess'
|
|
81
|
+
export { useMIDIInput } from './hooks/useMIDIInput'
|
|
98
82
|
export { useMIDIMessage } from './hooks/useMIDIMessage'
|
|
99
83
|
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'
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
export type KeyboardShortcuts = {
|
|
2
|
-
/**
|
|
3
|
-
* Keys laid out from `noteRange.first`, one entry per semitone.
|
|
4
|
-
*
|
|
5
|
-
* An empty string leaves that note without a shortcut: `KeyboardEvent.key` is
|
|
6
|
-
* never empty, so the entry can never match. Use it to skip the black keys
|
|
7
|
-
* (see {@link SHORTCUTS.HOME_ROW_NATURAL}) and keep the remaining entries
|
|
8
|
-
* lined up with the notes.
|
|
9
|
-
*/
|
|
10
|
-
keys: string[]
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Ready-made keyboard layouts. Both assume `noteRange.first` is a C.
|
|
15
|
-
*/
|
|
16
|
-
export const SHORTCUTS = {
|
|
17
|
-
/** Every semitone from C, over the two rows of a QWERTY keyboard. */
|
|
18
|
-
HOME_ROW: {
|
|
19
|
-
keys: [
|
|
20
|
-
'a',
|
|
21
|
-
'w',
|
|
22
|
-
's',
|
|
23
|
-
'e',
|
|
24
|
-
'd',
|
|
25
|
-
'f',
|
|
26
|
-
't',
|
|
27
|
-
'g',
|
|
28
|
-
'y',
|
|
29
|
-
'h',
|
|
30
|
-
'u',
|
|
31
|
-
'j',
|
|
32
|
-
'k',
|
|
33
|
-
'o',
|
|
34
|
-
'l',
|
|
35
|
-
'p',
|
|
36
|
-
';',
|
|
37
|
-
],
|
|
38
|
-
},
|
|
39
|
-
/** The white keys only, on the home row. Black keys have no shortcut. */
|
|
40
|
-
HOME_ROW_NATURAL: {
|
|
41
|
-
keys: [
|
|
42
|
-
'a',
|
|
43
|
-
'',
|
|
44
|
-
's',
|
|
45
|
-
'',
|
|
46
|
-
'd',
|
|
47
|
-
'f',
|
|
48
|
-
'',
|
|
49
|
-
'g',
|
|
50
|
-
'',
|
|
51
|
-
'h',
|
|
52
|
-
'',
|
|
53
|
-
'j',
|
|
54
|
-
'k',
|
|
55
|
-
'',
|
|
56
|
-
'l',
|
|
57
|
-
'',
|
|
58
|
-
';',
|
|
59
|
-
],
|
|
60
|
-
},
|
|
61
|
-
}
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { decimalPart, toFixed } from '@tremolo-ui/functions'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* How `Slider.Marks` fills itself in when it is given no children: one option
|
|
5
|
-
* every `per`, or every `step` of the slider. The object form turns off the
|
|
6
|
-
* mark or the label for the whole set; a single option is customized by
|
|
7
|
-
* writing `Slider.MarksOption` out instead.
|
|
8
|
-
*/
|
|
9
|
-
export type MarksOptions =
|
|
10
|
-
| 'step'
|
|
11
|
-
| number
|
|
12
|
-
| {
|
|
13
|
-
per: 'step' | number
|
|
14
|
-
mark?: boolean
|
|
15
|
-
label?: boolean
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function generateOptionsList(
|
|
19
|
-
options: MarksOptions,
|
|
20
|
-
min: number,
|
|
21
|
-
max: number,
|
|
22
|
-
step: number,
|
|
23
|
-
) {
|
|
24
|
-
const {
|
|
25
|
-
per,
|
|
26
|
-
mark = true,
|
|
27
|
-
label = true,
|
|
28
|
-
} = typeof options === 'object' ? options : { per: options }
|
|
29
|
-
const optionsList: {
|
|
30
|
-
value: number
|
|
31
|
-
mark: boolean
|
|
32
|
-
label: boolean
|
|
33
|
-
}[] = []
|
|
34
|
-
const interval = per === 'step' ? step : per
|
|
35
|
-
const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1
|
|
36
|
-
for (let i = 0; i < count; i++) {
|
|
37
|
-
const value = toFixed(
|
|
38
|
-
interval * (Math.ceil(min / interval) + i),
|
|
39
|
-
decimalPart(interval)?.length,
|
|
40
|
-
)
|
|
41
|
-
optionsList.push({ value: value, mark: mark, label: label })
|
|
42
|
-
}
|
|
43
|
-
return optionsList
|
|
44
|
-
}
|