@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,6 +1,6 @@
1
- import clsx from 'clsx'
2
1
  import {
3
2
  ComponentPropsWithoutRef,
3
+ CSSProperties,
4
4
  forwardRef,
5
5
  ReactNode,
6
6
  useCallback,
@@ -12,16 +12,24 @@ import {
12
12
  import type { AxisOptions, XY } from '@tremolo-ui/dom'
13
13
  import {
14
14
  applyDelta,
15
- InputEventOption,
15
+ type InputEventOption,
16
+ type ModifierValue,
17
+ selectModifier,
16
18
  linearScale,
17
19
  type Scale,
18
20
  type ValueRange,
19
21
  } from '@tremolo-ui/functions'
20
22
 
23
+ import { useComposedRefs } from '../../compose-refs'
24
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
21
25
  import { useDragValue } from '../../hooks/useDragValue'
22
26
  import { useWheel } from '../../hooks/useWheel'
23
- import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
24
- import { useComposedRefs } from '../_util/composeRefs'
27
+ import {
28
+ DEFAULT_DRAG_SENSITIVITY,
29
+ DEFAULT_KEYBOARD_OPTIONS,
30
+ DEFAULT_WHEEL_OPTIONS,
31
+ } from '../../input-event'
32
+ import { cssLength } from '../_util/css-length'
25
33
 
26
34
  import { ActiveLine } from './ActiveLine'
27
35
  import { calcAngles, KnobProvider } from './context'
@@ -30,17 +38,23 @@ import { SVGRoot } from './SVGRoot'
30
38
  import { Thumb } from './Thumb'
31
39
 
32
40
  const defaultExternalStyles: KnobProps['externalStyles'] = {
33
- userSelectNone: true,
34
41
  cursor: 'grabbing',
35
42
  }
36
43
 
37
44
  export interface KnobProps {
38
- // required
45
+ /** The current value. The knob shows only this, so update it from `onChange`. */
39
46
  value: number
47
+ /** The value with the knob turned all the way down. */
40
48
  min: number
49
+ /** The value with the knob turned all the way up. */
41
50
  max: number
42
51
 
43
- // optional
52
+ /**
53
+ * Granularity of the value. A drag, the wheel and the arrow keys snap it to
54
+ * multiples of `step`.
55
+ *
56
+ * @default 1
57
+ */
44
58
  step?: number
45
59
  /**
46
60
  * How the value is distributed across the travel.
@@ -53,52 +67,123 @@ export interface KnobProps {
53
67
  */
54
68
  scale?: Scale
55
69
  /**
56
- * value set when double-clicking
57
- * restriction: enableDoubleClickDefault = true
70
+ * The value a double click restores, while `enableDoubleClickDefault` is on.
58
71
  * @default min
59
- * @see enableDoubleClickDefault
60
72
  */
61
73
  defaultValue?: number
62
74
 
63
75
  /**
64
- * Value to be used as the starting point of the line when drawing.
76
+ * Where the active arc starts. Put it at the centre of a bipolar control,
77
+ * such as a pan knob, so that the arc grows from there either way.
65
78
  * @default min
66
79
  */
67
80
  startValue?: number
68
81
 
69
- /** width and height */
70
82
  /**
71
- * Width and height of the knob.
72
- * Defaults to the `--knob-size` CSS variable (50px).
83
+ * Width and height of the knob. Sets `--knob-size`; the size the theme
84
+ * gives it stands when this is omitted.
73
85
  */
74
86
  size?: number | string
75
87
 
76
88
  /**
77
- * Global style to apply when dragged
78
- * @default defaultExternalStyles
89
+ * The cursor to show while dragging. It is set on the dragged element, so it
90
+ * stays while the pointer is outside the knob.
91
+ *
92
+ * @default { cursor: 'grabbing' }
79
93
  */
80
94
  externalStyles?: {
81
- userSelectNone?: boolean
82
- cursor?: Cursor
95
+ cursor?: CSSProperties['cursor']
83
96
  }
84
97
  /**
85
- * wheel control option
86
- * If null, no event will be triggered
98
+ * How much one notch of the wheel moves the value. It only acts while the
99
+ * focus is inside, so that scrolling the page past the knob leaves it alone.
100
+ *
101
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
102
+ * range between `min` and `max`. The result is snapped to `step`, except for
103
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
104
+ * the wheel off.
105
+ *
106
+ * @default ['raw', 1]
107
+ */
108
+ wheel?: ModifierValue<InputEventOption> | null
109
+ /**
110
+ * How much a drag moves the value, per modifier key.
111
+ *
112
+ * `1` is the normal travel of 100px for the whole range; `0.1` makes the
113
+ * same movement cover a tenth of it. Shift is bound to `0.1` by default, to
114
+ * match what it does on the arrow keys.
115
+ *
116
+ * Pressing or releasing the key mid-drag does not disturb the value: the
117
+ * travel so far is kept and the new sensitivity applies from there. It takes
118
+ * effect on the next movement, since a key on its own produces no pointer
119
+ * event.
120
+ *
121
+ * @default { default: 1, shift: 0.1 }
122
+ */
123
+ dragSensitivity?: ModifierValue<number>
124
+
125
+ /**
126
+ * Hide the cursor while dragging and read the pointer movement directly,
127
+ * rather than letting it wander off across the screen.
128
+ *
129
+ * A knob does not care where the pointer is, only how far it moved, and
130
+ * letting it wander costs twice: the cursor ends up far from the knob it is
131
+ * holding, and **the drag stops at the edge of the screen**, where the
132
+ * operating system pins the pointer and the coordinates stop changing. A
133
+ * `dragSensitivity` below 1 reaches that edge quickly.
134
+ *
135
+ * Off by default because it is not free: the browser shows its own notice,
136
+ * Esc takes the lock back, and the request needs a user gesture and can be
137
+ * refused. A refused request is not an error — the drag simply carries on as
138
+ * an ordinary one.
139
+ *
140
+ * @default false
141
+ */
142
+ pointerLock?: boolean
143
+
144
+ /**
145
+ * How much one arrow key press moves the value.
146
+ *
147
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
148
+ * range between `min` and `max`. The result is snapped to `step`, except for
149
+ * an amount set on a modifier key, which is what lets shift move off the
150
+ * grid. `null` turns the arrow keys off.
151
+ *
152
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
153
+ * the amount to match: 1 would round straight back to where it started, and
154
+ * a development build warns about it.
155
+ *
156
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
87
157
  */
88
- wheel?: InputEventOption | null
158
+ keyboard?: ModifierValue<InputEventOption> | null
89
159
  /**
90
- * keyboard control option
91
- * If null, no event will be triggered
160
+ * Restore `defaultValue` on a double click.
161
+ * @default true
92
162
  */
93
- keyboard?: InputEventOption | null
94
163
  enableDoubleClickDefault?: boolean
95
164
 
165
+ /**
166
+ * Make the knob unchangeable and remove it from the tab order.
167
+ * The parts carry `data-disabled` while it is set.
168
+ */
96
169
  disabled?: boolean
170
+ /**
171
+ * Make the knob unchangeable while leaving it focusable.
172
+ * The parts carry `data-readonly` while it is set.
173
+ */
97
174
  readonly?: boolean
98
175
 
99
- /** angle range [degree] */
176
+ /**
177
+ * How far the knob turns from `min` to `max`, in degrees, centred on the
178
+ * top.
179
+ * @default 270
180
+ */
100
181
  angleRange?: number
101
182
 
183
+ /**
184
+ * Called with the new value when a drag, the wheel, an arrow key or a double
185
+ * click moves it.
186
+ */
102
187
  onChange?: (value: number) => void
103
188
 
104
189
  /**
@@ -130,7 +215,7 @@ type Props = KnobProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KnobProps>
130
215
  */
131
216
  const WHEEL_OPTIONS = { requireFocus: true }
132
217
 
133
- export const Root = forwardRef<KnobMethods, Props>(
218
+ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
134
219
  (
135
220
  {
136
221
  value,
@@ -142,8 +227,10 @@ export const Root = forwardRef<KnobMethods, Props>(
142
227
  startValue = min,
143
228
  size,
144
229
  externalStyles: _externalStyles,
145
- wheel = ['raw', 1],
146
- keyboard = ['raw', 1],
230
+ wheel = DEFAULT_WHEEL_OPTIONS,
231
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
232
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
233
+ pointerLock = false,
147
234
  enableDoubleClickDefault = true,
148
235
  disabled = false,
149
236
  readonly = false,
@@ -162,6 +249,7 @@ export const Root = forwardRef<KnobMethods, Props>(
162
249
  const elmRef = useRef<HTMLElement | SVGElement>(null)
163
250
 
164
251
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
252
+ const inactive = disabled || readonly
165
253
 
166
254
  // --- internal functions ---
167
255
  const range: ValueRange = useMemo(
@@ -169,17 +257,19 @@ export const Root = forwardRef<KnobMethods, Props>(
169
257
  [min, max, step, scale],
170
258
  )
171
259
 
260
+ useCheckSteps({ component: 'Knob', range, keyboard, wheel })
261
+
172
262
  const handleKeyDown = useCallback(
173
263
  (event: React.KeyboardEvent<HTMLOrSVGElement>) => {
174
- if (!keyboard || !onChange || readonly) return
264
+ if (!keyboard || !onChange || inactive) return
175
265
  const key = event.key
176
266
  if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
177
267
  event.preventDefault()
178
- const direction = key == 'ArrowRight' || key == 'ArrowUp' ? 1 : -1
179
- onChange(applyDelta(value, direction, keyboard, range))
268
+ const direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
269
+ onChange(applyDelta(value, direction, keyboard, range, event))
180
270
  }
181
271
  },
182
- [keyboard, onChange, readonly, value, range],
272
+ [keyboard, onChange, inactive, value, range],
183
273
  )
184
274
 
185
275
  // --- hooks ---
@@ -197,25 +287,23 @@ export const Root = forwardRef<KnobMethods, Props>(
197
287
  >({
198
288
  axis,
199
289
  getValue: () => [value, value],
290
+ sensitivity: (state) =>
291
+ selectModifier(dragSensitivity, state.event).value,
200
292
  threshold: 1,
201
- cursor: readonly ? undefined : externalStyles.cursor,
293
+ cursor: inactive ? undefined : externalStyles.cursor,
294
+ pointerLock: inactive ? false : pointerLock,
295
+ shouldStart: () => !inactive,
202
296
  onChange: (v) => {
203
- if (readonly) return
297
+ if (inactive) return
204
298
  onChange?.(v[1])
205
299
  },
206
- onDragStart: () => {
207
- if (externalStyles.userSelectNone) addUserSelectNone()
208
- },
209
- onDragEnd: () => {
210
- if (externalStyles.userSelectNone) removeUserSelectNone()
211
- },
212
300
  })
213
301
 
214
302
  const wheelRefCallback = useWheel<HTMLElement>((event) => {
215
- if (!wheel || readonly) return
303
+ if (!wheel || inactive) return
216
304
  event.preventDefault()
217
- if (!onChange || event.deltaY == 0) return
218
- onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range))
305
+ if (!onChange || event.deltaY === 0) return
306
+ onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range, event))
219
307
  }, WHEEL_OPTIONS)
220
308
 
221
309
  // Composed once, so React attaches the refs a single time instead of
@@ -234,35 +322,38 @@ export const Root = forwardRef<KnobMethods, Props>(
234
322
  useImperativeHandle(forwardedRef, () => {
235
323
  return {
236
324
  focus() {
237
- elmRef.current?.focus()
325
+ if (!disabled) elmRef.current?.focus()
238
326
  },
239
327
  blur() {
240
328
  elmRef.current?.blur()
241
329
  },
242
330
  }
243
- }, [])
331
+ }, [disabled])
244
332
 
245
333
  return (
246
334
  <KnobProvider value={context}>
247
335
  <div
248
336
  ref={rootRefCallback}
249
- className={clsx('tremolo-knob', className)}
250
- tabIndex={0}
337
+ className={className}
338
+ tabIndex={disabled ? -1 : 0}
251
339
  role="slider"
252
340
  aria-valuenow={value}
253
341
  aria-valuemin={min}
254
342
  aria-valuemax={max}
255
343
  aria-disabled={disabled}
256
344
  aria-readonly={readonly}
257
- data-dragging={dragging}
258
- style={{
259
- width: size,
260
- height: size,
261
- ...style,
262
- }}
345
+ data-disabled={disabled ? '' : undefined}
346
+ data-readonly={readonly ? '' : undefined}
347
+ data-dragging={dragging ? '' : undefined}
348
+ style={
349
+ {
350
+ '--knob-size': cssLength(size),
351
+ ...style,
352
+ } as CSSProperties
353
+ }
263
354
  onPointerDown={onPointerDown}
264
355
  onDoubleClick={(event) => {
265
- if (enableDoubleClickDefault && onChange) {
356
+ if (!inactive && enableDoubleClickDefault && onChange) {
266
357
  onChange(defaultValue)
267
358
  }
268
359
  onDoubleClick?.(event)
@@ -291,4 +382,6 @@ export const Knob = {
291
382
  Thumb,
292
383
  }
293
384
 
294
- export { useKnobContext } from './context'
385
+ export { useKnobContext, type KnobContextValue } from './context'
386
+ export { type KnobSVGRootProps } from './SVGRoot'
387
+ export { type KnobThumbProps } from './Thumb'
@@ -4,14 +4,18 @@ import {
4
4
  StepperArrow,
5
5
  StepperButton,
6
6
  StepperButtonProps,
7
- } from './stepperButton'
7
+ } from './StepperButton'
8
8
 
9
- export type DecrementStepperProps = StepperButtonProps
9
+ // An interface rather than an alias; see `NumberInputIncrementStepperProps`.
10
+ export interface NumberInputDecrementStepperProps extends StepperButtonProps {}
10
11
 
11
12
  /** Lowers the value by one `step`, repeating while held. */
12
13
  export function DecrementStepper(
13
- props: DecrementStepperProps &
14
- Omit<ComponentPropsWithoutRef<'div'>, keyof DecrementStepperProps>,
14
+ props: NumberInputDecrementStepperProps &
15
+ Omit<
16
+ ComponentPropsWithoutRef<'div'>,
17
+ keyof NumberInputDecrementStepperProps
18
+ >,
15
19
  ) {
16
20
  return (
17
21
  <StepperButton
@@ -4,14 +4,19 @@ import {
4
4
  StepperArrow,
5
5
  StepperButton,
6
6
  StepperButtonProps,
7
- } from './stepperButton'
7
+ } from './StepperButton'
8
8
 
9
- export type IncrementStepperProps = StepperButtonProps
9
+ // An interface rather than an alias, so that typedoc lists the props: the
10
+ // shared `StepperButtonProps` is not exported for it to follow.
11
+ export interface NumberInputIncrementStepperProps extends StepperButtonProps {}
10
12
 
11
13
  /** Raises the value by one `step`, repeating while held. */
12
14
  export function IncrementStepper(
13
- props: IncrementStepperProps &
14
- Omit<ComponentPropsWithoutRef<'div'>, keyof IncrementStepperProps>,
15
+ props: NumberInputIncrementStepperProps &
16
+ Omit<
17
+ ComponentPropsWithoutRef<'div'>,
18
+ keyof NumberInputIncrementStepperProps
19
+ >,
15
20
  ) {
16
21
  return (
17
22
  <StepperButton