@tremolo-ui/react 0.4.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 (62) hide show
  1. package/dist/index.cjs +1125 -1437
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +58 -93
  4. package/dist/index.d.cts +561 -243
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +561 -243
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1125 -1438
  9. package/dist/index.js.map +1 -1
  10. package/package.json +9 -10
  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 +90 -61
  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 +104 -24
  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 +127 -146
  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 +190 -230
  47. package/src/hooks/useDrag.ts +1 -1
  48. package/src/hooks/useDragValue.ts +149 -0
  49. package/src/hooks/useEventListener.ts +1 -1
  50. package/src/hooks/useWheel.ts +29 -9
  51. package/src/index.ts +15 -9
  52. package/src/components/AnimationCanvas/canvas.ts +0 -46
  53. package/src/components/NumberInput/InternalInput.tsx +0 -216
  54. package/src/components/NumberInput/type.ts +0 -58
  55. package/src/components/Piano/KeyLabel.tsx +0 -51
  56. package/src/components/Piano/context.tsx +0 -73
  57. package/src/components/Piano/key.tsx +0 -205
  58. package/src/components/_util/type.ts +0 -1
  59. package/src/hooks/useDragWithElement.ts +0 -100
  60. package/src/hooks/usePianoDrag.ts +0 -75
  61. package/src/hooks/useRefCallbackEvent.ts +0 -34
  62. /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
@@ -1,73 +0,0 @@
1
- import { createContext, ReactNode, useContext, useEffect, useRef } from 'react'
2
- import { createStore, useStore } from 'zustand'
3
-
4
- export type NoteRange = {
5
- first: number
6
- last: number
7
- }
8
-
9
- type State = {
10
- noteRange: NoteRange
11
- glissando: boolean
12
- midiMax: number
13
- fill: boolean
14
- onPlayNote: (noteNumber: number, velocity?: number) => void
15
- onStopNote: (noteNumber: number) => void
16
- label: (note: number, index: number) => ReactNode
17
- }
18
-
19
- type Action = {
20
- notePosition: (note: number) => number
21
- }
22
-
23
- type PianoStore = ReturnType<typeof createPianoStore>
24
-
25
- const createPianoStore = (initProps: Partial<State> & Action) => {
26
- // const noteRange = initProps.noteRange || { first: 0, last: 127 }
27
- const DEFAULT_PROPS: State = {
28
- noteRange: { first: 0, last: 127 },
29
- glissando: true,
30
- midiMax: 127,
31
- fill: false,
32
- label: () => undefined,
33
- onPlayNote: () => {},
34
- onStopNote: () => {},
35
- }
36
-
37
- return createStore<State & Action>()(() => ({
38
- ...DEFAULT_PROPS,
39
- ...initProps,
40
- notePosition: initProps.notePosition,
41
- }))
42
- }
43
-
44
- const PianoContext = createContext<PianoStore | null>(null)
45
-
46
- type PianoProviderProps = React.PropsWithChildren<Partial<State> & Action>
47
-
48
- export function PianoProvider({ children, ...props }: PianoProviderProps) {
49
- const storeRef = useRef<PianoStore>(null)
50
- if (!storeRef.current) {
51
- storeRef.current = createPianoStore(props)
52
- }
53
-
54
- useEffect(() => {
55
- if (storeRef.current) {
56
- storeRef.current.setState(props)
57
- } else {
58
- storeRef.current = createPianoStore(props)
59
- }
60
- }, [props])
61
-
62
- return (
63
- <PianoContext.Provider value={storeRef.current}>
64
- {children}
65
- </PianoContext.Provider>
66
- )
67
- }
68
-
69
- export function usePianoContext<T>(selector: (state: State & Action) => T): T {
70
- const store = useContext(PianoContext)
71
- if (!store) throw new Error('Missing PianoContext.Provider in the tree')
72
- return useStore(store, selector)
73
- }
@@ -1,205 +0,0 @@
1
- import clsx from 'clsx'
2
- import React, {
3
- CSSProperties,
4
- ComponentPropsWithoutRef,
5
- forwardRef,
6
- useImperativeHandle,
7
- useState,
8
- ReactElement,
9
- } from 'react'
10
-
11
- import { usePianoContext } from './context'
12
- import { KeyLabel, KeyLabelProps } from './KeyLabel'
13
-
14
- export interface KeyProps {
15
- noteNumber: number
16
-
17
- width?: number
18
- height?: number | `${number}%`
19
-
20
- bg?: string
21
- color?: string
22
- activeBg?: string
23
- activeColor?: string
24
-
25
- style?: CSSProperties
26
-
27
- /**
28
- * \<KeyLabel />
29
- */
30
- children?: ReactElement
31
-
32
- /** @internal */
33
- __width?: number
34
- }
35
-
36
- export interface KeyMethods {
37
- play: (velocity?: number) => void
38
- stop: () => void
39
- played: () => boolean
40
- }
41
-
42
- type Props = KeyProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyProps>
43
- type ImplProps = Props & { keyType: 'black' | 'white' }
44
-
45
- export const defaultWhiteKeyWidth = 40
46
- export const defaultBlackKeyWidth = 26
47
-
48
- const KeyImpl = forwardRef<KeyMethods, ImplProps>(
49
- (
50
- {
51
- keyType,
52
- noteNumber,
53
- width,
54
- height,
55
- bg,
56
- color,
57
- activeBg,
58
- activeColor,
59
- className,
60
- style,
61
- children,
62
- __width,
63
- ...props
64
- }: ImplProps,
65
- ref,
66
- ) => {
67
- const [played, setPlayed] = useState(false)
68
- const midiMax = usePianoContext((s) => s.midiMax)
69
- const fill = usePianoContext((s) => s.fill)
70
- // const glissando = usePianoContext(s => s.glissando)
71
- const onPlayNote = usePianoContext((s) => s.onPlayNote)
72
- const onStopNote = usePianoContext((s) => s.onStopNote)
73
- const label = usePianoContext((s) => s.label)
74
- const notePosition = usePianoContext((s) => s.notePosition)
75
- const position = notePosition(noteNumber)
76
-
77
- const disabled = noteNumber > midiMax
78
- const colors = {
79
- '--bg': bg,
80
- '--color': color,
81
- '--active-bg': activeBg,
82
- '--active-color': activeColor,
83
- }
84
-
85
- useImperativeHandle(ref, () => {
86
- return {
87
- play(velocity) {
88
- if (disabled) return
89
- setPlayed(true)
90
- if (onPlayNote) onPlayNote(noteNumber, velocity)
91
- },
92
- stop() {
93
- setPlayed(false)
94
- if (onStopNote) onStopNote(noteNumber)
95
- },
96
- played() {
97
- return played
98
- },
99
- }
100
- }, [disabled, noteNumber, onPlayNote, onStopNote, played])
101
-
102
- let keyLabelProps: KeyLabelProps = {}
103
- if (children != undefined) {
104
- React.Children.forEach(children, (child) => {
105
- if (React.isValidElement(child)) {
106
- if (child.type == KeyLabel) {
107
- keyLabelProps = child.props as KeyLabelProps
108
- } else {
109
- throw new Error('only <KeyLabel>')
110
- }
111
- } else {
112
- throw new Error('children is an invalid element.')
113
- }
114
- })
115
- }
116
-
117
- return (
118
- <div
119
- className={clsx(`tremolo-piano-${keyType}-key`, className)}
120
- aria-disabled={disabled}
121
- data-active={played}
122
- style={{
123
- ...colors,
124
- left: position,
125
- width: fill ? __width : width,
126
- height: height,
127
- ...style,
128
- }}
129
- // onPointerDown={() => {
130
- // if (disabled) return
131
- // setPlayed(true)
132
- // if (onPlayNote) onPlayNote(noteNumber)
133
- // }}
134
- // onPointerEnter={() => {
135
- // if (disabled) return
136
- // if (glissando) {
137
- // setPlayed(true)
138
- // if (onPlayNote) onPlayNote(noteNumber)
139
- // }
140
- // }}
141
- // onPointerUp={() => {
142
- // if (disabled) return
143
- // setPlayed(false)
144
- // if (onStopNote) onStopNote(noteNumber)
145
- // }}
146
- // onPointerLeave={() => {
147
- // if (disabled) return
148
- // if (glissando) {
149
- // setPlayed(false)
150
- // if (onStopNote) onStopNote(noteNumber)
151
- // }
152
- // }}
153
- {...props}
154
- >
155
- <KeyLabel label={label} __note={noteNumber} {...keyLabelProps} />
156
- </div>
157
- )
158
- },
159
- )
160
-
161
- export const WhiteKey = forwardRef<KeyMethods, Props>(
162
- (
163
- {
164
- width = defaultWhiteKeyWidth,
165
- height = '100%',
166
- onContextMenu = (e) => e.preventDefault(),
167
- ...props
168
- }: Props,
169
- ref,
170
- ) => {
171
- return (
172
- <KeyImpl
173
- ref={ref}
174
- keyType="white"
175
- width={width}
176
- height={height}
177
- onContextMenu={onContextMenu}
178
- {...props}
179
- />
180
- )
181
- },
182
- )
183
-
184
- export const BlackKey = forwardRef<KeyMethods, Props>(
185
- (
186
- {
187
- width = defaultBlackKeyWidth,
188
- height = '60%',
189
- onContextMenu = (e) => e.preventDefault(),
190
- ...props
191
- }: Props,
192
- ref,
193
- ) => {
194
- return (
195
- <KeyImpl
196
- ref={ref}
197
- keyType="black"
198
- width={width}
199
- height={height}
200
- onContextMenu={onContextMenu}
201
- {...props}
202
- />
203
- )
204
- },
205
- )
@@ -1 +0,0 @@
1
- export type Override<T, U> = T & Omit<U, keyof T>
@@ -1,100 +0,0 @@
1
- import { RefObject, useCallback, useEffect, useRef, useState } from 'react'
2
-
3
- import { createDrag } from '@tremolo-ui/dom'
4
- import { normalizeValue } from '@tremolo-ui/functions'
5
-
6
- import { useCallbackRef } from './useCallbackRef'
7
-
8
- interface UseDragWithElement<T extends Element> {
9
- baseElementRef: RefObject<T | null>
10
- /**
11
- * Report the position on pointer down, before any movement.
12
- *
13
- * Enable it where the pointer position *is* the value, so that a plain click
14
- * jumps to it. Leave it off where the element being dragged is an object in
15
- * its own right, so that grabbing its edge does not shift it under the cursor.
16
- *
17
- * @default false
18
- */
19
- updateOnPointerDown?: boolean
20
- /** CSS cursor to show while dragging. Applied to the element itself. */
21
- cursor?: string
22
- onDrag: (normalizedX: number, normalizedY: number) => void
23
- onDragStart?: (normalizedX: number, normalizedY: number) => void
24
- onDragEnd?: (normalizedX: number, normalizedY: number) => void
25
- }
26
-
27
- /**
28
- * Track a pointer drag, reporting the position normalized against
29
- * the bounding rect of `baseElementRef`.
30
- *
31
- * @returns a ref callback for the element that starts the drag, and whether a drag is in progress
32
- */
33
- export function useDragWithElement<T extends Element>({
34
- baseElementRef,
35
- updateOnPointerDown = false,
36
- cursor,
37
- onDrag,
38
- onDragStart,
39
- onDragEnd,
40
- }: UseDragWithElement<T>) {
41
- const [dragging, setDragging] = useState(false)
42
- const normalizedX = useRef(0)
43
- const normalizedY = useRef(0)
44
-
45
- const dragHandler = useCallbackRef(onDrag)
46
- const dragStartHandler = useCallbackRef(onDragStart)
47
- const dragEndHandler = useCallbackRef(onDragEnd)
48
-
49
- const update = useCallback(
50
- (clientX: number, clientY: number) => {
51
- const base = baseElementRef.current
52
- if (!base) return false
53
- const { left, top, right, bottom } = base.getBoundingClientRect()
54
- normalizedX.current = normalizeValue(clientX, left, right)
55
- normalizedY.current = normalizeValue(clientY, top, bottom)
56
- return true
57
- },
58
- [baseElementRef],
59
- )
60
-
61
- // See useDrag for why the node is held in state.
62
- const [node, setNode] = useState<Element | null>(null)
63
-
64
- useEffect(() => {
65
- if (!node) return
66
-
67
- const instance = createDrag(node, {
68
- cursor,
69
- onDragStart: ({ clientX, clientY }) => {
70
- setDragging(true)
71
- const updated = update(clientX, clientY)
72
- if (updated && updateOnPointerDown) {
73
- dragHandler(normalizedX.current, normalizedY.current)
74
- }
75
- dragStartHandler(normalizedX.current, normalizedY.current)
76
- },
77
- onDrag: ({ clientX, clientY }) => {
78
- if (update(clientX, clientY)) {
79
- dragHandler(normalizedX.current, normalizedY.current)
80
- }
81
- },
82
- onDragEnd: () => {
83
- setDragging(false)
84
- dragEndHandler(normalizedX.current, normalizedY.current)
85
- },
86
- })
87
-
88
- return () => instance.destroy()
89
- }, [
90
- node,
91
- update,
92
- updateOnPointerDown,
93
- cursor,
94
- dragHandler,
95
- dragStartHandler,
96
- dragEndHandler,
97
- ])
98
-
99
- return { refCallback: setNode, dragging }
100
- }
@@ -1,75 +0,0 @@
1
- import { useRef, useCallback, RefObject } from 'react'
2
-
3
- import { normalizeValue } from '@tremolo-ui/functions'
4
-
5
- import { useEventListener } from './useEventListener'
6
- import { useRefCallbackEvent } from './useRefCallbackEvent'
7
-
8
- interface UsePianoDrag<T extends Element> {
9
- baseElementRef: RefObject<T | null>
10
- onDrag: (normalizedX: number, normalizedY: number) => void
11
- onDragStart?: (normalizedX: number, normalizedY: number) => void
12
- onDragEnd?: (normalizedX: number, normalizedY: number) => void
13
- }
14
-
15
- /**
16
- * Internal
17
- * @returns [refCallback, pointerDownHandler]
18
- * @private
19
- */
20
- export function usePianoDrag<T extends Element>({
21
- baseElementRef,
22
- onDrag,
23
- onDragStart,
24
- onDragEnd,
25
- }: UsePianoDrag<T>) {
26
- const dragged = useRef(false)
27
- const normalizedX = useRef(0)
28
- const normalizedY = useRef(0)
29
-
30
- const handleDrag = useCallback(
31
- (event: MouseEvent | React.PointerEvent<T> | TouchEvent) => {
32
- if (!baseElementRef.current || !dragged.current) {
33
- return
34
- }
35
- const isTouch = event instanceof TouchEvent
36
- if (isTouch && event.cancelable) event.preventDefault()
37
- const { left, top, right, bottom } =
38
- baseElementRef.current.getBoundingClientRect()
39
- const mouseX = isTouch ? event.touches[0].clientX : event.clientX
40
- const mouseY = isTouch ? event.touches[0].clientY : event.clientY
41
- const nx = normalizeValue(mouseX, left, right)
42
- const ny = normalizeValue(mouseY, top, bottom)
43
- normalizedX.current = nx
44
- normalizedY.current = ny
45
- onDrag(nx, ny)
46
- },
47
- [onDrag, baseElementRef],
48
- )
49
-
50
- const refHandler = useRefCallbackEvent(
51
- 'touchmove',
52
- handleDrag,
53
- { passive: false },
54
- [onDrag],
55
- )
56
-
57
- const pointerDownHandler = useCallback(
58
- (event: React.PointerEvent<T>) => {
59
- dragged.current = true
60
- handleDrag(event)
61
- onDragStart?.(normalizedX.current, normalizedY.current)
62
- },
63
- [handleDrag, onDragStart],
64
- )
65
-
66
- useEventListener(globalThis.window, 'pointermove', handleDrag)
67
-
68
- useEventListener(globalThis.window, 'pointerup', () => {
69
- if (!dragged.current) return
70
- dragged.current = false
71
- onDragEnd?.(normalizedX.current, normalizedY.current)
72
- })
73
-
74
- return { refHandler, pointerDownHandler }
75
- }
@@ -1,34 +0,0 @@
1
- import { useMemo } from 'react'
2
-
3
- /**
4
- * Internal
5
- * @private
6
- */
7
- export function useRefCallbackEvent<K extends keyof DocumentEventMap>(
8
- event: K,
9
- handler: (event: DocumentEventMap[K]) => void,
10
- options?: boolean | AddEventListenerOptions,
11
- deps?: React.DependencyList,
12
- ): (div: EventTarget | null) => void
13
- export function useRefCallbackEvent(
14
- event: string,
15
- handler: (event: Event) => void,
16
- options?: boolean | AddEventListenerOptions,
17
- deps: React.DependencyList = [],
18
- ): (div: EventTarget | null) => void {
19
- return useMemo(() => {
20
- let cleanup: (() => void) | undefined
21
- return (node: EventTarget | null) => {
22
- if (!node) {
23
- cleanup?.()
24
- return
25
- }
26
- node.addEventListener(event, handler, options)
27
-
28
- cleanup = () => {
29
- node.removeEventListener(event, handler, options)
30
- }
31
- }
32
- // eslint-disable-next-line react-hooks/exhaustive-deps
33
- }, deps)
34
- }