@tremolo-ui/react 0.3.0 → 0.5.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 (64) hide show
  1. package/dist/index.cjs +1162 -1554
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +58 -93
  4. package/dist/index.d.cts +592 -278
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +592 -278
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1163 -1555
  9. package/dist/index.js.map +1 -1
  10. package/package.json +12 -13
  11. package/src/components/AnimationCanvas/index.tsx +84 -155
  12. package/src/components/Knob/ActiveLine.tsx +6 -6
  13. package/src/components/Knob/InactiveLine.tsx +9 -11
  14. package/src/components/Knob/SVGRoot.tsx +9 -18
  15. package/src/components/Knob/context.tsx +57 -89
  16. package/src/components/Knob/index.css +6 -2
  17. package/src/components/Knob/index.tsx +95 -83
  18. package/src/components/NumberInput/DecrementStepper.tsx +17 -50
  19. package/src/components/NumberInput/IncrementStepper.tsx +17 -50
  20. package/src/components/NumberInput/InputField.tsx +123 -0
  21. package/src/components/NumberInput/Stepper.tsx +90 -12
  22. package/src/components/NumberInput/context.tsx +74 -146
  23. package/src/components/NumberInput/index.css +38 -102
  24. package/src/components/NumberInput/index.tsx +252 -98
  25. package/src/components/NumberInput/stepperButton.tsx +90 -0
  26. package/src/components/Piano/index.tsx +272 -288
  27. package/src/components/Piano/keyboardShortcuts.ts +34 -4
  28. package/src/components/PointsEditor/Background.tsx +8 -2
  29. package/src/components/PointsEditor/Container.tsx +19 -9
  30. package/src/components/PointsEditor/Point.tsx +117 -52
  31. package/src/components/PointsEditor/context.tsx +28 -74
  32. package/src/components/PointsEditor/index.css +5 -0
  33. package/src/components/PointsEditor/index.tsx +77 -43
  34. package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
  35. package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
  36. package/src/components/Slider/Thumb.tsx +55 -68
  37. package/src/components/Slider/Track.tsx +15 -15
  38. package/src/components/Slider/context.tsx +36 -51
  39. package/src/components/Slider/index.css +18 -5
  40. package/src/components/Slider/index.tsx +136 -170
  41. package/src/components/Slider/type.ts +4 -4
  42. package/src/components/XYPad/Area.tsx +16 -7
  43. package/src/components/XYPad/Thumb.tsx +62 -74
  44. package/src/components/XYPad/context.tsx +46 -0
  45. package/src/components/XYPad/index.css +12 -0
  46. package/src/components/XYPad/index.tsx +193 -250
  47. package/src/hooks/useDrag.ts +41 -76
  48. package/src/hooks/useDragValue.ts +149 -0
  49. package/src/hooks/useEventListener.ts +1 -1
  50. package/src/hooks/useWheel.ts +51 -0
  51. package/src/index.ts +15 -13
  52. package/src/components/AnimationCanvas/canvas.ts +0 -46
  53. package/src/components/DragObserver/index.tsx +0 -58
  54. package/src/components/NumberInput/InternalInput.tsx +0 -221
  55. package/src/components/NumberInput/type.ts +0 -58
  56. package/src/components/Piano/KeyLabel.tsx +0 -51
  57. package/src/components/Piano/context.tsx +0 -73
  58. package/src/components/Piano/key.tsx +0 -205
  59. package/src/components/WheelObserver/index.tsx +0 -48
  60. package/src/components/_util/type.ts +0 -1
  61. package/src/hooks/useDragWithElement.ts +0 -72
  62. package/src/hooks/usePianoDrag.ts +0 -75
  63. package/src/hooks/useRefCallbackEvent.ts +0 -34
  64. /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
@@ -1,93 +1,58 @@
1
- import { useRef, useCallback } from 'react'
1
+ import { useEffect, useState } from 'react'
2
2
 
3
- import { useEventListener } from './useEventListener'
4
- import { useRefCallbackEvent } from './useRefCallbackEvent'
3
+ import { createDrag } from '@tremolo-ui/dom'
4
+
5
+ import { useCallbackRef } from './_internal/useCallbackRef'
5
6
 
6
7
  interface UseDragProps {
8
+ /**
9
+ * Threshold at which the onDrag event fires.
10
+ * Prevents onDrag events from firing, for example, when double-clicking.
11
+ */
7
12
  threshold?: number
8
13
 
14
+ /** CSS cursor to show while dragging. Applied to the element itself. */
15
+ cursor?: string
16
+
9
17
  onDrag?: (x: number, y: number, deltaX: number, deltaY: number) => void
10
18
  onDragStart?: () => void
11
19
  onDragEnd?: () => void
12
20
  }
13
21
 
14
22
  /**
15
- * @returns [refCallback, pointerDownHandler]
23
+ * Track a pointer drag on an element.
24
+ *
25
+ * @returns a ref callback to attach to the element being dragged
16
26
  */
17
27
  export function useDrag<T extends Element>({
18
- threshold: _threshold = 1,
28
+ threshold = 1,
29
+ cursor,
19
30
  onDrag,
20
31
  onDragStart,
21
32
  onDragEnd,
22
- }: UseDragProps): [
23
- (div: EventTarget | null) => void,
24
- (event: React.PointerEvent<T>) => void,
25
- ] {
26
- const dragOffsetX = useRef<number | undefined>(undefined)
27
- const dragOffsetY = useRef<number | undefined>(undefined)
28
- const dragStartX = useRef(0)
29
- const dragStartY = useRef(0)
30
-
31
- const threshold = Math.max(_threshold, 1)
32
-
33
- const handleDrag = useCallback(
34
- (
35
- event: MouseEvent | React.MouseEvent<T, MouseEvent> | TouchEvent,
36
- first = false,
37
- ) => {
38
- const isTouch = event instanceof TouchEvent
39
- if (isTouch && event.cancelable) event.preventDefault()
40
- const screenX = isTouch ? event.touches[0].screenX : event.screenX
41
- const screenY = isTouch ? event.touches[0].screenY : event.screenY
42
- let deltaX = 0
43
- let deltaY = 0
44
- if (first) {
45
- dragStartX.current = screenX
46
- dragStartY.current = screenY
47
- }
48
- if (dragOffsetX.current) {
49
- deltaX = screenX - dragOffsetX.current
50
- dragOffsetX.current = screenX
51
- }
52
- if (dragOffsetY.current) {
53
- deltaY = screenY - dragOffsetY.current
54
- dragOffsetY.current = screenY
55
- }
56
- if (Math.abs(deltaX) < threshold && Math.abs(deltaY) < threshold) return
57
- onDrag?.(
58
- screenX - dragStartX.current,
59
- screenY - dragStartY.current,
60
- deltaX,
61
- deltaY,
62
- )
63
- },
64
- [threshold, onDrag],
65
- )
66
-
67
- const pointerDownHandler = useCallback(
68
- (event: React.PointerEvent<T>) => {
69
- dragOffsetX.current = event.screenX
70
- dragOffsetY.current = event.screenY
71
- handleDrag(event, true)
72
- onDragStart?.()
73
- },
74
- [handleDrag, onDragStart],
75
- )
76
-
77
- const refHandler = useRefCallbackEvent(
78
- 'touchmove',
79
- handleDrag,
80
- { passive: false },
81
- [onDrag],
82
- )
83
-
84
- useEventListener(globalThis.window, 'mousemove', handleDrag)
85
-
86
- useEventListener(globalThis.window, 'pointerup', () => {
87
- dragOffsetX.current = undefined
88
- dragOffsetY.current = undefined
89
- onDragEnd?.()
90
- })
91
-
92
- return [refHandler, pointerDownHandler]
33
+ }: UseDragProps): (node: T | null) => void {
34
+ const dragHandler = useCallbackRef(onDrag)
35
+ const dragStartHandler = useCallbackRef(onDragStart)
36
+ const dragEndHandler = useCallbackRef(onDragEnd)
37
+
38
+ // The node is held in state rather than bound in the ref callback itself, so
39
+ // that re-attaching the ref with the same node does not restart the drag.
40
+ // React re-attaches on every render when the caller passes an inline ref.
41
+ const [node, setNode] = useState<T | null>(null)
42
+
43
+ useEffect(() => {
44
+ if (!node) return
45
+
46
+ const instance = createDrag(node, {
47
+ threshold,
48
+ cursor,
49
+ onDragStart: () => dragStartHandler(),
50
+ onDrag: ({ x, y, deltaX, deltaY }) => dragHandler(x, y, deltaX, deltaY),
51
+ onDragEnd: () => dragEndHandler(),
52
+ })
53
+
54
+ return () => instance.destroy()
55
+ }, [node, threshold, cursor, dragHandler, dragStartHandler, dragEndHandler])
56
+
57
+ return setNode
93
58
  }
@@ -0,0 +1,149 @@
1
+ import { RefObject, useEffect, useRef, useState } from 'react'
2
+
3
+ import {
4
+ createDragValue,
5
+ elementMapping,
6
+ relativeMapping,
7
+ type AxisOptions,
8
+ type DragValueInstance,
9
+ type XY,
10
+ type XYInput,
11
+ } from '@tremolo-ui/dom'
12
+
13
+ import { useCallbackRef } from './_internal/useCallbackRef'
14
+
15
+ export interface UseDragValueOptions {
16
+ /** Scaling of each axis; a single value applies to both. */
17
+ axis: XYInput<AxisOptions>
18
+
19
+ /**
20
+ * Normalize the pointer against the bounding rect of this element, so that
21
+ * the value *is* the position pointed at.
22
+ *
23
+ * Give either this or `getValue`.
24
+ */
25
+ baseElementRef?: RefObject<Element | null>
26
+
27
+ /**
28
+ * Move the value away from where it stood when the drag started, by the
29
+ * distance dragged. The pointer position itself carries no meaning.
30
+ *
31
+ * Give either this or `baseElementRef`.
32
+ */
33
+ getValue?: () => XY<number>
34
+ /**
35
+ * Pixels of movement that span the whole range, with `getValue`.
36
+ * @default 100
37
+ */
38
+ pixelRange?: XYInput<number>
39
+
40
+ /** @see DragValueOptions.updateOnPointerDown */
41
+ updateOnPointerDown?: boolean
42
+ /** @default 0 */
43
+ threshold?: number
44
+ /** CSS cursor to show while dragging. Applied to the element itself. */
45
+ cursor?: string
46
+
47
+ onChange?: (value: XY<number>) => void
48
+ onDragStart?: (value: XY<number>) => void
49
+ onDragEnd?: (value: XY<number>) => void
50
+ }
51
+
52
+ /**
53
+ * Drive a value with a pointer drag.
54
+ *
55
+ * @returns a ref callback for the element that starts the drag, and whether a drag is in progress
56
+ */
57
+ export function useDragValue<T extends Element>(
58
+ options: UseDragValueOptions,
59
+ ): { refCallback: (node: T | null) => void; dragging: boolean } {
60
+ const {
61
+ axis,
62
+ baseElementRef,
63
+ pixelRange,
64
+ updateOnPointerDown,
65
+ threshold,
66
+ cursor,
67
+ } = options
68
+
69
+ if (!baseElementRef && !options.getValue) {
70
+ throw new Error(
71
+ 'useDragValue: give either baseElementRef or getValue, so that the drag has something to move',
72
+ )
73
+ }
74
+
75
+ const [dragging, setDragging] = useState(false)
76
+
77
+ const valueGetter = useCallbackRef(options.getValue)
78
+ const changeHandler = useCallbackRef(options.onChange)
79
+ const dragStartHandler = useCallbackRef(options.onDragStart)
80
+ const dragEndHandler = useCallbackRef(options.onDragEnd)
81
+
82
+ // Read when the instance is created. The effect below keeps it current, and
83
+ // runs right after, so a stale setting is replaced within the same commit.
84
+ const latest = useRef(options)
85
+
86
+ const instanceRef = useRef<DragValueInstance | null>(null)
87
+
88
+ // See useDrag for why the node is held in state.
89
+ const [node, setNode] = useState<T | null>(null)
90
+
91
+ const [pixelRangeX, pixelRangeY] = Array.isArray(pixelRange)
92
+ ? pixelRange
93
+ : [pixelRange, pixelRange]
94
+
95
+ useEffect(() => {
96
+ if (!node) return
97
+
98
+ const instance = createDragValue(node, {
99
+ axis: latest.current.axis,
100
+ mapping: baseElementRef
101
+ ? elementMapping(() => baseElementRef.current)
102
+ : relativeMapping({
103
+ pixelRange: [pixelRangeX ?? 100, pixelRangeY ?? 100],
104
+ }),
105
+ getValue: () => valueGetter() ?? [0, 0],
106
+ updateOnPointerDown: latest.current.updateOnPointerDown,
107
+ threshold: latest.current.threshold,
108
+ cursor: latest.current.cursor,
109
+ onChange: (value) => changeHandler(value),
110
+ onDragStart: (value) => {
111
+ setDragging(true)
112
+ dragStartHandler(value)
113
+ },
114
+ onDragEnd: (value) => {
115
+ setDragging(false)
116
+ dragEndHandler(value)
117
+ },
118
+ })
119
+ instanceRef.current = instance
120
+
121
+ return () => {
122
+ instanceRef.current = null
123
+ instance.destroy()
124
+ }
125
+ }, [
126
+ node,
127
+ baseElementRef,
128
+ pixelRangeX,
129
+ pixelRangeY,
130
+ valueGetter,
131
+ changeHandler,
132
+ dragStartHandler,
133
+ dragEndHandler,
134
+ ])
135
+
136
+ // Runs after every render: the settings come from props and are cheap to
137
+ // push, and updating in place leaves a drag in progress untouched.
138
+ useEffect(() => {
139
+ latest.current = options
140
+ instanceRef.current?.update({
141
+ axis,
142
+ updateOnPointerDown,
143
+ threshold,
144
+ cursor,
145
+ })
146
+ })
147
+
148
+ return { refCallback: setNode, dragging }
149
+ }
@@ -1,6 +1,6 @@
1
1
  import { useEffect } from 'react'
2
2
 
3
- import { useCallbackRef } from './useCallbackRef'
3
+ import { useCallbackRef } from './_internal/useCallbackRef'
4
4
 
5
5
  type Target = EventTarget | null | (() => EventTarget | null)
6
6
  type Options = boolean | AddEventListenerOptions
@@ -0,0 +1,51 @@
1
+ import { RefObject, useEffect, useState } from 'react'
2
+
3
+ import { createWheel, type WheelOptions } from '@tremolo-ui/dom'
4
+
5
+ import { useCallbackRef } from './_internal/useCallbackRef'
6
+
7
+ export interface UseWheelOptions extends WheelOptions {
8
+ /**
9
+ * Listen on this element rather than on the one the returned ref callback is
10
+ * attached to, and ignore that callback.
11
+ *
12
+ * For a control made of several movable parts: a wheel event only reaches
13
+ * what the cursor is over, so a listener per part responds only while the
14
+ * cursor is over that same part. Listening on the element they share lets
15
+ * every part see the event, and each one decide whether it is the one to act.
16
+ *
17
+ * The element is read when the listener is attached, so a ref filled in by a
18
+ * parent is fine as long as the parent is above in the tree.
19
+ */
20
+ target?: RefObject<Element | null>
21
+ }
22
+
23
+ /**
24
+ * Listen to wheel events on an element.
25
+ *
26
+ * The listener is not passive, so the handler may call `preventDefault()`.
27
+ *
28
+ * @returns a ref callback to attach to the element, unused when `target` is given
29
+ */
30
+ export function useWheel<T extends Element>(
31
+ onWheel: (event: WheelEvent) => void,
32
+ { requireFocus = false, target }: UseWheelOptions = {},
33
+ ): (node: T | null) => void {
34
+ const wheelHandler = useCallbackRef(onWheel)
35
+
36
+ // See useDrag for why the node is held in state.
37
+ const [node, setNode] = useState<T | null>(null)
38
+
39
+ useEffect(() => {
40
+ const element = target ? target.current : node
41
+ if (!element) return
42
+
43
+ const instance = createWheel(element, (event) => wheelHandler(event), {
44
+ requireFocus,
45
+ })
46
+
47
+ return () => instance.destroy()
48
+ }, [node, target, wheelHandler, requireFocus])
49
+
50
+ return setNode
51
+ }
package/src/index.ts CHANGED
@@ -16,49 +16,49 @@ export {
16
16
  type InitFunction,
17
17
  type RelativeSizingProps,
18
18
  } from './components/AnimationCanvas'
19
- export { DragObserver, type DragObserverProps } from './components/DragObserver'
20
19
  export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
21
20
  export {
22
21
  NumberInput,
22
+ useNumberInputContext,
23
23
  type NumberInputProps,
24
24
  type NumberInputMethods,
25
+ type NumberInputFieldProps,
25
26
  type StepperProps,
26
27
  type IncrementStepperProps,
27
28
  type DecrementStepperProps,
28
29
  } from './components/NumberInput'
29
30
  export {
30
31
  Piano,
31
- getNoteRangeArray,
32
32
  SHORTCUTS,
33
33
  type PianoProps,
34
34
  type PianoMethods,
35
- type KeyProps,
36
- type KeyMethods,
37
- type KeyLabelProps,
35
+ type KeyState,
36
+ type KeyAttributes,
37
+ type CSSVariables,
38
38
  type KeyboardShortcuts,
39
39
  } from './components/Piano'
40
40
  export {
41
41
  PointsEditor,
42
+ usePointsEditorContext,
42
43
  clampPoint,
43
44
  type PointsEditorProps,
45
+ type PointsEditorContextValue,
46
+ type PointsEditorBackgroundProps,
47
+ type PointsEditorContainerProps,
44
48
  type PointProps,
45
49
  type PointBaseType,
46
50
  } from './components/PointsEditor'
47
51
  export {
48
52
  Slider,
49
53
  useSliderContext,
50
- type ScaleOptionProps,
51
- type ScaleProps,
54
+ type MarksOptionProps,
55
+ type MarksProps,
52
56
  type SliderMethods,
53
57
  type SliderProps,
54
58
  type SliderThumbMethods,
55
59
  type SliderThumbProps,
56
60
  type SliderTrackProps,
57
61
  } from './components/Slider'
58
- export {
59
- WheelObserver,
60
- type WheelObserverProps,
61
- } from './components/WheelObserver'
62
62
  export {
63
63
  XYPad,
64
64
  type XYPadProps,
@@ -66,13 +66,14 @@ export {
66
66
  type XYPadAreaProps,
67
67
  type XYPadThumbProps,
68
68
  type XYPadThumbMethods,
69
- type ValueOptions,
69
+ type XY,
70
+ type XYInput,
70
71
  } from './components/XYPad'
71
72
 
72
73
  // hooks
73
74
  export { useAnimationFrame } from './hooks/useAnimationFrame'
74
75
  export { useDrag } from './hooks/useDrag'
75
- export { useDragWithElement } from './hooks/useDragWithElement'
76
+ export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
76
77
  export { useEventListener } from './hooks/useEventListener'
77
78
  export { useInterval } from './hooks/useInterval'
78
79
  export { useLongPress } from './hooks/useLongPress'
@@ -84,3 +85,4 @@ export {
84
85
  } from './hooks/useMIDIAccess'
85
86
  export { useMIDIInput } from './hooks/useMIDIInput'
86
87
  export { useMIDIMessage } from './hooks/useMIDIMessage'
88
+ export { useWheel, type UseWheelOptions } from './hooks/useWheel'
@@ -1,46 +0,0 @@
1
- // https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/save
2
- export const drawingState = [
3
- 'strokeStyle',
4
- 'fillStyle',
5
- 'globalAlpha',
6
- 'lineWidth',
7
- 'lineCap',
8
- 'lineJoin',
9
- 'miterLimit',
10
- 'lineDashOffset',
11
- 'shadowOffsetX',
12
- 'shadowOffsetY',
13
- 'shadowBlur',
14
- 'shadowColor',
15
- 'globalCompositeOperation',
16
- 'font',
17
- 'textAlign',
18
- 'textBaseline',
19
- 'direction',
20
- 'imageSmoothingEnabled',
21
- ] as const
22
-
23
- export type DrawingState = (typeof drawingState)[number]
24
- export type DrawingStateValue = CanvasRenderingContext2D[DrawingState]
25
- export type DrawingContext = Pick<CanvasRenderingContext2D, DrawingState>
26
-
27
- export function isDrawingState(obj: unknown): obj is DrawingState {
28
- const d: string[] = [...drawingState]
29
- return typeof obj == 'string' && d.includes(obj)
30
- }
31
-
32
- export function setDprConfig(
33
- canvas: HTMLCanvasElement,
34
- context: CanvasRenderingContext2D,
35
- width: number,
36
- height: number,
37
- dpr: number,
38
- ) {
39
- canvas.width = width * dpr
40
- canvas.height = height * dpr
41
- // Reset current transformation matrix to the identity matrix
42
- context.setTransform(1, 0, 0, 1, 0, 0)
43
- context.scale(dpr, dpr)
44
- canvas.style.width = `${width}px`
45
- canvas.style.height = `${height}px`
46
- }
@@ -1,58 +0,0 @@
1
- import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
2
-
3
- import { useDrag } from '../../hooks/useDrag'
4
- import { Override } from '../_util/type'
5
-
6
- export interface DragObserverProps<T extends ElementType> {
7
- /**
8
- * React.ElementType
9
- * @default div
10
- */
11
- as?: T
12
-
13
- /**
14
- * Threshold at which the onDrag event fires.
15
- * Prevents onDrag events from firing, for example, when double-clicking.
16
- */
17
- threshold?: number
18
-
19
- children?: ReactNode
20
- onDrag?: (x: number, y: number, deltaX: number, deltaY: number) => void
21
- onDragStart?: () => void
22
- onDragEnd?: () => void
23
- }
24
-
25
- export function DragObserver<T extends ElementType = 'div'>(
26
- props: Override<DragObserverProps<T>, ComponentPropsWithoutRef<T>>,
27
- ) {
28
- const {
29
- as: Component = 'div',
30
- threshold = 1,
31
- onDrag,
32
- onDragStart,
33
- onDragEnd,
34
- children,
35
- onPointerDown,
36
- ...attributes
37
- } = props
38
-
39
- const [refCallback, pointerDownHandler] = useDrag({
40
- threshold: threshold,
41
- onDrag: onDrag,
42
- onDragStart: onDragStart,
43
- onDragEnd: onDragEnd,
44
- })
45
-
46
- return (
47
- <Component
48
- ref={refCallback}
49
- onPointerDown={(e) => {
50
- pointerDownHandler(e)
51
- onPointerDown?.(e)
52
- }}
53
- {...attributes}
54
- >
55
- {children}
56
- </Component>
57
- )
58
- }