@tremolo-ui/react 0.7.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.
Files changed (47) hide show
  1. package/dist/index.cjs +346 -1015
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +76 -221
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +76 -221
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +333 -978
  8. package/dist/index.js.map +1 -1
  9. package/package.json +3 -3
  10. package/src/components/DropZone/index.tsx +2 -1
  11. package/src/components/FileInput/index.tsx +8 -13
  12. package/src/components/Knob/ActiveLine.tsx +5 -3
  13. package/src/components/Knob/InactiveLine.tsx +6 -4
  14. package/src/components/Knob/SVGRoot.tsx +3 -3
  15. package/src/components/Knob/Thumb.tsx +3 -2
  16. package/src/components/Knob/context.tsx +3 -74
  17. package/src/components/Knob/index.tsx +19 -16
  18. package/src/components/NumberInput/InputField.tsx +12 -27
  19. package/src/components/NumberInput/Stepper.tsx +49 -97
  20. package/src/components/NumberInput/StepperButton.tsx +3 -4
  21. package/src/components/NumberInput/context.tsx +2 -1
  22. package/src/components/NumberInput/index.tsx +40 -71
  23. package/src/components/Piano/index.tsx +35 -142
  24. package/src/components/PointsEditor/Container.tsx +15 -26
  25. package/src/components/PointsEditor/Point.tsx +38 -85
  26. package/src/components/PointsEditor/context.tsx +5 -56
  27. package/src/components/PointsEditor/index.tsx +29 -311
  28. package/src/components/Slider/Marks.tsx +3 -5
  29. package/src/components/Slider/MarksOption.tsx +7 -18
  30. package/src/components/Slider/Track.tsx +2 -1
  31. package/src/components/Slider/index.tsx +29 -28
  32. package/src/components/XYPad/Area.tsx +27 -49
  33. package/src/components/XYPad/Thumb.tsx +3 -1
  34. package/src/components/XYPad/context.tsx +1 -3
  35. package/src/components/XYPad/index.tsx +33 -46
  36. package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
  37. package/src/hooks/_internal/useCheckSteps.ts +9 -146
  38. package/src/hooks/useLongPress.ts +35 -51
  39. package/src/hooks/useMIDIAccess.ts +0 -8
  40. package/src/hooks/useMIDIInput.ts +0 -3
  41. package/src/index.ts +2 -27
  42. package/src/components/Piano/keyboard-shortcuts.ts +0 -61
  43. package/src/components/Slider/type.ts +0 -46
  44. package/src/components/_util/css-length.ts +0 -13
  45. package/src/components/_util/decimal-digits.ts +0 -23
  46. package/src/components/_util/visually-hidden.ts +0 -25
  47. package/src/input-event.ts +0 -32
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  ComponentPropsWithoutRef,
3
3
  CSSProperties,
4
- useCallback,
5
4
  useEffect,
6
5
  useId,
7
6
  useRef,
@@ -9,38 +8,26 @@ import {
9
8
  } from 'react'
10
9
 
11
10
  import {
12
- applyDelta,
11
+ arrowKeyMove,
12
+ clampPoint,
13
13
  type InputEventOption,
14
- type ModifierState,
15
14
  type ModifierValue,
15
+ POINT_AXIS,
16
+ type PointPosition,
17
+ type PointsEditorPoint,
16
18
  selectModifier,
17
19
  } from '@tremolo-ui/dom'
18
- import { clamp } from '@tremolo-ui/functions'
19
20
 
20
21
  import { useComposedRefs } from '../../compose-refs'
21
22
  import { useDragValue } from '../../hooks/useDragValue'
22
- import { cssLength } from '../_util/css-length'
23
23
  import { useCheckPlacement } from '../_util/Placement'
24
24
  import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
25
25
 
26
- import { type PointRegistration, usePointsEditorContext } from './context'
26
+ import { usePointsEditorContext } from './context'
27
27
 
28
28
  import type { CSSVariables } from '../../css-variables'
29
29
 
30
- export type PointBaseType = { x: number; y: number }
31
-
32
- export function clampPoint(
33
- point: PointBaseType,
34
- min?: Partial<PointBaseType>,
35
- max?: Partial<PointBaseType>,
36
- ) {
37
- const { x, y } = point
38
- const newX = clamp(x, min?.x ?? 0, max?.x ?? 1)
39
- const newY = clamp(y, min?.y ?? 0, max?.y ?? 1)
40
- return { x: newX, y: newY }
41
- }
42
-
43
- export interface PointsEditorPointProps<T extends PointBaseType> {
30
+ export interface PointsEditorPointProps<T extends PointPosition> {
44
31
  /**
45
32
  * Where the point is, as `{ x, y }` from 0 to 1 on each axis, with `y`
46
33
  * growing downwards.
@@ -53,16 +40,10 @@ export interface PointsEditorPointProps<T extends PointBaseType> {
53
40
  */
54
41
  id?: string
55
42
  /** The lowest position the point can take, per axis. An axis left out is 0. */
56
- min?: Partial<PointBaseType>
43
+ min?: Partial<PointPosition>
57
44
  /** The highest position the point can take, per axis. An axis left out is 1. */
58
- max?: Partial<PointBaseType>
45
+ max?: Partial<PointPosition>
59
46
 
60
- /** Width and height at once. Sets both `--width` and `--height`. */
61
- size?: number | string
62
- /** Sets `--width`; the size the theme gives it stands when omitted. */
63
- width?: number | string
64
- /** Sets `--height`; the size the theme gives it stands when omitted. */
65
- height?: number | string
66
47
  /** Sets `--color`, for the theme to colour the point with. */
67
48
  color?: string
68
49
 
@@ -88,22 +69,15 @@ export interface PointsEditorPointProps<T extends PointBaseType> {
88
69
  * Called with the new position when the point is dragged or moved by the
89
70
  * arrow keys or the wheel, including when it moves along with a selection.
90
71
  */
91
- onChange?: (value: PointBaseType) => void
72
+ onChange?: (value: PointPosition) => void
92
73
  /** Called when a drag on this point starts, with where the point is. */
93
- onDragStart?: (value: PointBaseType) => void
74
+ onDragStart?: (value: PointPosition) => void
94
75
  /** Called when that drag ends, with where the point is. */
95
- onDragEnd?: (value: PointBaseType) => void
76
+ onDragEnd?: (value: PointPosition) => void
96
77
 
97
- style?: CSSProperties &
98
- CSSVariables<'width' | 'height' | 'color' | 'translate'>
78
+ style?: CSSProperties & CSSVariables<'color' | 'translate'>
99
79
  }
100
80
 
101
- /**
102
- * A point is placed by its position within the container, so its value is a
103
- * position: 0..1 on each axis, with y growing downwards.
104
- */
105
- export const AXIS = { min: 0, max: 1 }
106
-
107
81
  /**
108
82
  * One setting for both axes, or one per axis. The point holds a range input
109
83
  * for each, so anything that names or describes it comes in a pair.
@@ -115,15 +89,12 @@ function perAxis(
115
89
  return typeof value === 'string' ? value : value?.[axis]
116
90
  }
117
91
 
118
- export function Point<T extends PointBaseType>({
92
+ export function Point<T extends PointPosition>({
119
93
  value,
120
94
  children,
121
95
  id: idProp,
122
96
  min,
123
97
  max,
124
- size,
125
- width,
126
- height,
127
98
  color,
128
99
 
129
100
  disabled: _disabled,
@@ -154,10 +125,7 @@ export function Point<T extends PointBaseType>({
154
125
  keyboard: rootKeyboard,
155
126
  dragSensitivity,
156
127
  selection,
157
- registerPoint,
158
- beginPointDrag,
159
- movePointDrag,
160
- nudgeSelection,
128
+ editor,
161
129
  } = usePointsEditorContext()
162
130
 
163
131
  const generatedId = useId()
@@ -182,7 +150,7 @@ export function Point<T extends PointBaseType>({
182
150
  // What the editor needs to move this point along with the rest of a
183
151
  // selection. Rewritten after every render rather than kept in the registry
184
152
  // itself: the value changes on every frame of a drag.
185
- const registration = useRef<PointRegistration>({
153
+ const registration = useRef<PointsEditorPoint>({
186
154
  value,
187
155
  min,
188
156
  max,
@@ -203,15 +171,18 @@ export function Point<T extends PointBaseType>({
203
171
  }
204
172
  })
205
173
 
206
- useEffect(() => registerPoint(id, registration), [id, registerPoint])
174
+ useEffect(
175
+ () => editor.registerPoint(id, () => registration.current),
176
+ [id, editor],
177
+ )
207
178
 
208
179
  /** Where the pointer was when the drag started, to measure the move from. */
209
- const pointerOrigin = useRef<PointBaseType | null>(null)
180
+ const pointerOrigin = useRef<PointPosition | null>(null)
210
181
 
211
182
  // The value is the position itself: no scaling, and no rounding to a step.
212
183
  const { refCallback: dragRefCallback, dragging } =
213
184
  useDragValue<HTMLDivElement>({
214
- axis: AXIS,
185
+ axis: POINT_AXIS,
215
186
  baseElementRef: containerRef,
216
187
  sensitivity: (state) =>
217
188
  selectModifier(dragSensitivity, state.event).value,
@@ -223,13 +194,13 @@ export function Point<T extends PointBaseType>({
223
194
  onChange: ([x, y]) => {
224
195
  const origin = pointerOrigin.current
225
196
  if (!origin) return
226
- movePointDrag({ x: x - origin.x, y: y - origin.y })
197
+ editor.movePointDrag({ x: x - origin.x, y: y - origin.y })
227
198
  },
228
199
  onDragStart: ([x, y], state) => {
229
200
  // The press decides the selection before the snapshot is taken, so it
230
201
  // happens here rather than in an onPointerDown: a native listener on
231
202
  // the element runs before React's, and the two would disagree.
232
- beginPointDrag(id, state.event)
203
+ editor.beginPointDrag(id, state.event)
233
204
  pointerOrigin.current = { x, y }
234
205
  xInputRef.current?.focus()
235
206
 
@@ -244,43 +215,27 @@ export function Point<T extends PointBaseType>({
244
215
  },
245
216
  })
246
217
 
247
- const nudge = useCallback(
248
- (
249
- axis: 'x' | 'y',
250
- direction: number,
251
- option: ModifierValue<InputEventOption>,
252
- modifiers: ModifierState,
253
- ) => {
254
- const next = applyDelta(value[axis], direction, option, AXIS, modifiers)
255
- // As a move, so that the rest of the selection comes along and the whole
256
- // group stops together at the edge.
257
- nudgeSelection(id, {
258
- x: axis === 'x' ? next - value.x : 0,
259
- y: axis === 'y' ? next - value.y : 0,
260
- })
261
- },
262
- [value, id, nudgeSelection],
263
- )
264
-
265
218
  const refCallback = useComposedRefs<HTMLDivElement>(
266
219
  dragRefCallback,
267
220
  setElement,
268
221
  )
269
222
 
270
223
  const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
271
- const key = event.key
272
- if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
273
- return
274
- // The key picks the axis, whichever of the two inputs holds the focus: a
275
- // point is one control to the person moving it, and the focus lands on the
276
- // x input, so reading the axis off the input would leave the y axis with
277
- // no keys at all.
278
- const axis = key === 'ArrowRight' || key === 'ArrowLeft' ? 'x' : 'y'
224
+ // The key picks the axis, whichever of the two inputs holds the focus:
225
+ // the focus lands on the x input, so reading the axis off the input would
226
+ // leave the y axis with no keys at all. y grows downwards, so ArrowUp
227
+ // moves the point towards 0.
228
+ const move = arrowKeyMove(event.key)
229
+ if (!move) return
279
230
  event.preventDefault()
280
231
  if (!onChange || inactive || !keyboard) return
281
- // y grows downwards, so ArrowUp moves the point towards 0.
282
- const direction = key === 'ArrowLeft' || key === 'ArrowUp' ? -1 : 1
283
- nudge(axis, direction, keyboard, event)
232
+ editor.nudgePoint(
233
+ id,
234
+ move.axis === 0 ? 'x' : 'y',
235
+ move.direction,
236
+ keyboard,
237
+ event,
238
+ )
284
239
  }
285
240
 
286
241
  const current = clampPoint(value, min, max)
@@ -304,8 +259,6 @@ export function Point<T extends PointBaseType>({
304
259
  style={
305
260
  {
306
261
  '--color': color,
307
- '--width': cssLength(size ?? width),
308
- '--height': cssLength(size ?? height),
309
262
  // The mechanics of the position below: a point is placed by its
310
263
  // position in the container, measured from its own centre.
311
264
  position: 'absolute',
@@ -351,7 +304,7 @@ export function Point<T extends PointBaseType>({
351
304
  return
352
305
  }
353
306
  const delta = event.currentTarget.valueAsNumber - value[axis]
354
- nudgeSelection(id, {
307
+ editor.nudgeSelection(id, {
355
308
  x: axis === 'x' ? delta : 0,
356
309
  y: axis === 'y' ? delta : 0,
357
310
  })
@@ -2,32 +2,11 @@ import { createContext, CSSProperties, RefObject, useContext } from 'react'
2
2
 
3
3
  import {
4
4
  type InputEventOption,
5
- type ModifierState,
6
5
  type ModifierValue,
6
+ type PointsEditorInstance,
7
7
  type SelectionBoxRect,
8
8
  } from '@tremolo-ui/dom'
9
9
 
10
- import type { PointBaseType } from './Point'
11
-
12
- /**
13
- * What a `Point` tells the editor about itself, so that a selection can be
14
- * moved without the editor knowing how the points are stored.
15
- *
16
- * Held behind a ref and rewritten on every render: the value changes on every
17
- * frame of a drag, and a registry keyed on it would be rebuilt just as often.
18
- */
19
- export interface PointRegistration {
20
- value: PointBaseType
21
- min?: Partial<PointBaseType>
22
- max?: Partial<PointBaseType>
23
- readonly: boolean
24
- onChange?: (value: PointBaseType) => void
25
- /** The point's own element, to match the focus against. */
26
- element: HTMLElement | null
27
- /** The wheel option this point resolved, root inheritance applied. */
28
- wheel: ModifierValue<InputEventOption> | null
29
- }
30
-
31
10
  export type PointsEditorContextValue = {
32
11
  disabled: boolean
33
12
  readonly: boolean
@@ -40,53 +19,23 @@ export type PointsEditorContextValue = {
40
19
  externalStyles: {
41
20
  cursor?: CSSProperties['cursor']
42
21
  }
43
-
44
22
  /**
45
23
  * `Container` registers its element here; `Point` normalizes the pointer
46
24
  * against it, so a point is placed by its position within the container.
47
25
  */
48
26
  containerRef: RefObject<HTMLDivElement | null>
49
-
50
27
  /** Whether points can be selected at all. See `PointsEditorProps.selectable`. */
51
28
  selectable: boolean
52
29
  /** Ids of the points currently selected. Always empty while `selectable` is off. */
53
30
  selection: readonly string[]
54
- /** Register a point so that a selection can move it with the rest. */
55
- registerPoint: (id: string, entry: RefObject<PointRegistration>) => () => void
56
- /**
57
- * Whether the element is a point, or inside one. The registry knows which
58
- * elements the points are, so nothing has to be recognised by a class name.
59
- */
60
- isPointElement: (element: Element | null | undefined) => boolean
61
- /**
62
- * A pointer went down on a point: works out the new selection and takes the
63
- * snapshot the move will be measured against.
64
- */
65
- beginPointDrag: (id: string, modifiers: ModifierState) => void
66
- /** Move everything the drag picked up, by one amount, clamped as one. */
67
- movePointDrag: (delta: PointBaseType) => void
68
- /**
69
- * Move the selection by an amount that did not come from a drag — an arrow
70
- * key or a wheel notch. The current values are the starting point.
71
- */
72
- nudgeSelection: (id: string, delta: PointBaseType) => void
73
31
  /**
74
- * Move the point that currently holds focus by one notch. The wheel listener
75
- * belongs to `Container` rather than to every `Point`, so the lookup of who
76
- * has focus happens here, where the registry is. Returns whether a point
77
- * took it, so the caller knows whether to consume the event.
32
+ * The selection and the moves, from `createPointsEditor` in
33
+ * `@tremolo-ui/dom`: every `Point` registers with it, and a press, a drag,
34
+ * a key or the wheel goes through it so that a selection moves as one.
78
35
  */
79
- nudgeFocusedPoint: (
80
- axis: 'x' | 'y',
81
- direction: number,
82
- modifiers: ModifierState,
83
- ) => boolean
84
-
36
+ editor: PointsEditorInstance
85
37
  /** The selection box, while one is being dragged. */
86
38
  selectionBox: SelectionBoxRect | null
87
- beginSelectionBox: (at: PointBaseType, modifiers: ModifierState) => void
88
- moveSelectionBox: (to: PointBaseType) => void
89
- endSelectionBox: () => void
90
39
  }
91
40
 
92
41
  const PointsEditorContext =