@tremolo-ui/react 0.1.4 → 0.1.6

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 (43) hide show
  1. package/dist/index.cjs +285 -155
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +80 -32
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +95 -60
  6. package/dist/index.d.cts.map +1 -1
  7. package/dist/index.d.ts +97 -62
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +262 -139
  10. package/dist/index.js.map +1 -1
  11. package/package.json +14 -9
  12. package/src/components/AnimationCanvas/canvas.ts +16 -0
  13. package/src/components/AnimationCanvas/index.tsx +54 -71
  14. package/src/components/Knob/index.css +54 -23
  15. package/src/components/Knob/index.tsx +30 -25
  16. package/src/components/NumberInput/InternalInput.tsx +1 -1
  17. package/src/components/NumberInput/context.tsx +2 -1
  18. package/src/components/NumberInput/index.css +1 -0
  19. package/src/components/NumberInput/index.tsx +5 -5
  20. package/src/components/NumberInput/type.ts +58 -0
  21. package/src/components/Piano/context.tsx +1 -1
  22. package/src/components/Piano/index.css +24 -8
  23. package/src/components/Piano/index.tsx +254 -219
  24. package/src/components/Piano/key.tsx +3 -3
  25. package/src/components/Slider/Scale.tsx +1 -1
  26. package/src/components/Slider/ScaleOption.tsx +1 -1
  27. package/src/components/Slider/Track.tsx +2 -2
  28. package/src/components/Slider/index.tsx +3 -2
  29. package/src/components/Slider/type.ts +26 -0
  30. package/src/components/XYPad/index.tsx +2 -2
  31. package/src/hooks/useAnimationFrame.ts +3 -0
  32. package/src/hooks/useCallbackRef.ts +2 -2
  33. package/src/hooks/useDrag.ts +2 -1
  34. package/src/hooks/useDragWithElement.ts +2 -1
  35. package/src/hooks/useEventListener.ts +3 -0
  36. package/src/hooks/useInterval.ts +3 -0
  37. package/src/hooks/useLongPress.ts +3 -0
  38. package/src/hooks/useMIDIAccess.ts +40 -0
  39. package/src/hooks/useMIDIInput.ts +50 -0
  40. package/src/hooks/useMIDIMessage.ts +24 -0
  41. package/src/hooks/useRefCallbackEvent.ts +4 -0
  42. package/src/index.ts +21 -13
  43. package/src/styles/{index.css → global.css} +1 -1
@@ -112,7 +112,7 @@ export const Slider = forwardRef<SliderMethods, Props>(
112
112
  onKeyDown,
113
113
  ...props
114
114
  }: Props,
115
- ref,
115
+ forwardedRef,
116
116
  ) => {
117
117
  // -- state and ref ---
118
118
  const trackElementRef = useRef<HTMLDivElement>(null)
@@ -245,7 +245,7 @@ export const Slider = forwardRef<SliderMethods, Props>(
245
245
  [wheel, vertical, readonly, onChange, updateValueByEvent],
246
246
  )
247
247
 
248
- useImperativeHandle(ref, () => {
248
+ useImperativeHandle(forwardedRef, () => {
249
249
  return {
250
250
  focus() {
251
251
  thumbRef.current?.focus()
@@ -341,3 +341,4 @@ export {
341
341
  export { SliderTrack, type SliderTrackProps } from './Track'
342
342
  export { Scale, type ScaleProps } from './Scale'
343
343
  export { ScaleOption, type ScaleOptionProps } from './ScaleOption'
344
+ export { type ScaleOptions, type ScaleType } from './type'
@@ -0,0 +1,26 @@
1
+ import { decimalPart, toFixed } from '@tremolo-ui/functions'
2
+
3
+ export type ScaleType = 'mark' | 'mark-number' | 'number'
4
+ export type ScaleOptions = ['step', ScaleType] | [number, ScaleType]
5
+
6
+ export function generateOptionsList(
7
+ options: ScaleOptions,
8
+ min: number,
9
+ max: number,
10
+ step: number,
11
+ ) {
12
+ const optionsList: {
13
+ value: number
14
+ type: ScaleType
15
+ }[] = []
16
+ const per = options[0] == 'step' ? step : options[0]
17
+ const count = Math.floor(max / per) - Math.ceil(min / per) + 1
18
+ for (let i = 0; i < count; i++) {
19
+ const value = toFixed(
20
+ per * (Math.ceil(min / per) + i),
21
+ decimalPart(per)?.length,
22
+ )
23
+ optionsList.push({ value: value, type: options[1] })
24
+ }
25
+ return optionsList
26
+ }
@@ -106,7 +106,7 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
106
106
  children,
107
107
  ...props
108
108
  }: Props,
109
- ref,
109
+ forwardedRef,
110
110
  ) => {
111
111
  const x = useMemo(() => {
112
112
  return { ...defaultValueOptions, ..._x }
@@ -276,7 +276,7 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
276
276
  [x, y, onChange, readonly, updateValueByEvent],
277
277
  )
278
278
 
279
- useImperativeHandle(ref, () => {
279
+ useImperativeHandle(forwardedRef, () => {
280
280
  return {
281
281
  focus() {
282
282
  thumbRef.current?.focus()
@@ -1,5 +1,8 @@
1
1
  import { DependencyList, useCallback, useEffect, useRef } from 'react'
2
2
 
3
+ /**
4
+ * @category hooks
5
+ */
3
6
  export function useAnimationFrame(
4
7
  callback = () => {},
5
8
  deps: DependencyList = [],
@@ -1,8 +1,8 @@
1
1
  import { useCallback, useInsertionEffect, useRef } from 'react'
2
2
 
3
3
  /**
4
- * This hook is user-land implementation of the experimental `useEffectEvent` hook.
5
- * React docs: https://react.dev/learn/separating-events-from-effects#declaring-an-effect-event
4
+ * Internal
5
+ * @private
6
6
  */
7
7
  export function useCallbackRef<Args extends unknown[], Return>(
8
8
  callback: ((...args: Args) => Return) | undefined,
@@ -3,7 +3,7 @@ import { useRef, useCallback } from 'react'
3
3
  import { useEventListener } from './useEventListener'
4
4
  import { useRefCallbackEvent } from './useRefCallbackEvent'
5
5
 
6
- export interface UseDragProps {
6
+ interface UseDragProps {
7
7
  threshold?: number
8
8
 
9
9
  onDrag: (x: number, y: number, deltaX: number, deltaY: number) => void
@@ -12,6 +12,7 @@ export interface UseDragProps {
12
12
  }
13
13
 
14
14
  /**
15
+ * @category hooks
15
16
  * @returns [refCallback, pointerDownHandler]
16
17
  */
17
18
  export function useDrag<T extends Element>({
@@ -5,7 +5,7 @@ import { normalizeValue } from '@tremolo-ui/functions'
5
5
  import { useEventListener } from './useEventListener'
6
6
  import { useRefCallbackEvent } from './useRefCallbackEvent'
7
7
 
8
- export interface UseDragWithElementProps<T extends Element> {
8
+ interface UseDragWithElementProps<T extends Element> {
9
9
  baseElementRef: RefObject<T | null>
10
10
  onDrag: (normalizedX: number, normalizedY: number) => void
11
11
  onDragStart?: (normalizedX: number, normalizedY: number) => void
@@ -13,6 +13,7 @@ export interface UseDragWithElementProps<T extends Element> {
13
13
  }
14
14
 
15
15
  /**
16
+ * @category hooks
16
17
  * @returns [refCallback, pointerDownHandler]
17
18
  */
18
19
  export function useDragWithElement<T extends Element>({
@@ -5,6 +5,9 @@ import { useCallbackRef } from './useCallbackRef'
5
5
  type Target = EventTarget | null | (() => EventTarget | null)
6
6
  type Options = boolean | AddEventListenerOptions
7
7
 
8
+ /**
9
+ * @category hooks
10
+ */
8
11
  export function useEventListener<K extends keyof DocumentEventMap>(
9
12
  target: Target,
10
13
  event: K,
@@ -1,5 +1,8 @@
1
1
  import { useEffect, useRef } from 'react'
2
2
 
3
+ /**
4
+ * @category hooks
5
+ */
3
6
  export function useInterval(callback: () => void, delay: number | null) {
4
7
  const savedCallback = useRef(callback)
5
8
 
@@ -3,6 +3,9 @@ import { useCallback, useState } from 'react'
3
3
  import { useEventListener } from './useEventListener'
4
4
  import { useInterval } from './useInterval'
5
5
 
6
+ /**
7
+ * @category hooks
8
+ */
6
9
  export function useLongPress(
7
10
  callback: () => void,
8
11
  initialDelay = 500,
@@ -0,0 +1,40 @@
1
+ import { useCallback, useEffect, useState } from 'react'
2
+
3
+ /** @private */
4
+ export const PERMISSION_DENIED = 'PERMISSION_DENIED'
5
+ /** @private */
6
+ export const NOT_SUPPORTED = 'NOT_SUPPORTED'
7
+
8
+ /** @private */
9
+ export type MIDIAccessError = typeof PERMISSION_DENIED | typeof NOT_SUPPORTED
10
+
11
+ /**
12
+ * Hooks for requesting MIDI access in the browser. The first argument allows you to choose whether to request access on mount.
13
+ * @category hooks
14
+ */
15
+ export function useMIDIAccess(requestOnMount = true) {
16
+ const [midiAccess, setMidiAccess] = useState<MIDIAccess | null>(null)
17
+ const [error, setError] = useState<MIDIAccessError | null>(null)
18
+
19
+ const request = useCallback(() => {
20
+ if (navigator.requestMIDIAccess) {
21
+ navigator
22
+ .requestMIDIAccess()
23
+ .then((access) => {
24
+ setMidiAccess(access)
25
+ setError(null)
26
+ })
27
+ .catch(() => {
28
+ setError(PERMISSION_DENIED)
29
+ })
30
+ } else {
31
+ setError(NOT_SUPPORTED)
32
+ }
33
+ }, [])
34
+
35
+ useEffect(() => {
36
+ if (requestOnMount) request()
37
+ }, [requestOnMount, request])
38
+
39
+ return { request, midiAccess, error }
40
+ }
@@ -0,0 +1,50 @@
1
+ import { useCallback } from 'react'
2
+
3
+ import { useMIDIMessage } from './useMIDIMessage'
4
+
5
+ const MIDI_EVENT_TO_NUMBER = {
6
+ NOTE_ON: 0x90,
7
+ NOTE_OFF: 0x80,
8
+ PITCH_BEND: 0xe0,
9
+ }
10
+
11
+ // const NUMBER_TO_MIDI_EVENT: Record<number, string> = {
12
+ // 0x90: 'NOTE_ON',
13
+ // 0x80: 'NOTE_OFF',
14
+ // 0xe0: 'PITCH_BEND',
15
+ // }
16
+
17
+ /**
18
+ * Hooks for handling note on/off events. To be used with useMIDIAccess. Internally uses useMIDIMessage.
19
+ * @category hooks
20
+ */
21
+ export function useMIDIInput(
22
+ midiAccess: MIDIAccess | null,
23
+ onNoteOnEvent?: (note: number, velocity: number) => void,
24
+ onNoteOffEvent?: (note: number) => void,
25
+ onPitchBendEvent?: (msb: number, lsb: number) => void,
26
+ ) {
27
+ const handleMIDIMessage = useCallback(
28
+ (event: MIDIMessageEvent) => {
29
+ if (!event.data) return
30
+ // event.data[0] ... command
31
+ // event.data[1] ... note, MSB (Most Significant Byte)
32
+ // event.data[2] ... velocity, LSB (Least Significant Byte)
33
+ const kind = event.data[0] & 0xf0
34
+
35
+ if (
36
+ kind == MIDI_EVENT_TO_NUMBER.NOTE_OFF ||
37
+ (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON && event.data[2] == 0)
38
+ ) {
39
+ onNoteOffEvent?.(event.data[1])
40
+ } else if (kind == MIDI_EVENT_TO_NUMBER.NOTE_ON) {
41
+ onNoteOnEvent?.(event.data[1], event.data[2])
42
+ } else if (kind == MIDI_EVENT_TO_NUMBER.PITCH_BEND) {
43
+ onPitchBendEvent?.(event.data[1], event.data[2])
44
+ }
45
+ },
46
+ [onNoteOffEvent, onNoteOnEvent, onPitchBendEvent],
47
+ )
48
+
49
+ useMIDIMessage(midiAccess, handleMIDIMessage)
50
+ }
@@ -0,0 +1,24 @@
1
+ import { useEffect } from 'react'
2
+
3
+ /**
4
+ * Hooks for when you want to process MIDI events in more detail than useMIDIInput.
5
+ * @category hooks
6
+ */
7
+ export function useMIDIMessage(
8
+ midiAccess: MIDIAccess | null,
9
+ onMIDIMessage: (event: MIDIMessageEvent) => void,
10
+ ) {
11
+ useEffect(() => {
12
+ if (!midiAccess) return
13
+
14
+ for (const input of midiAccess.inputs.values()) {
15
+ input.addEventListener('midimessage', onMIDIMessage)
16
+ }
17
+
18
+ return () => {
19
+ for (const input of midiAccess.inputs.values()) {
20
+ input.removeEventListener('midimessage', onMIDIMessage)
21
+ }
22
+ }
23
+ }, [midiAccess, onMIDIMessage])
24
+ }
@@ -1,5 +1,9 @@
1
1
  import { useMemo } from 'react'
2
2
 
3
+ /**
4
+ * Internal
5
+ * @private
6
+ */
3
7
  export function useRefCallbackEvent<K extends keyof DocumentEventMap>(
4
8
  event: K,
5
9
  handler: (event: DocumentEventMap[K]) => void,
package/src/index.ts CHANGED
@@ -1,4 +1,5 @@
1
- import './styles/index.css'
1
+ // For bundled CSS (@tremolo-ui/react/styles/index.css)
2
+ import './styles/global.css'
2
3
  import './components/Knob/index.css'
3
4
  import './components/NumberInput/index.css'
4
5
  import './components/Piano/index.css'
@@ -33,17 +34,18 @@ export {
33
34
  type StepperProps,
34
35
  } from './components/NumberInput'
35
36
  export {
37
+ Piano,
38
+ type PianoProps,
39
+ type PianoMethods,
40
+ getNoteRangeArray,
41
+ WhiteKey,
36
42
  BlackKey,
43
+ type KeyProps,
44
+ type KeyMethods,
37
45
  KeyLabel,
38
46
  type KeyLabelProps,
39
- type KeyMethods,
40
- type KeyProps,
41
- type KeyboardShortcuts,
42
- Piano,
43
- type PianoProps,
44
47
  SHORTCUTS,
45
- WhiteKey,
46
- getNoteRangeArray,
48
+ type KeyboardShortcuts,
47
49
  } from './components/Piano'
48
50
  export {
49
51
  Scale,
@@ -76,12 +78,18 @@ export {
76
78
  type XYPadThumbMethods,
77
79
  } from './components/XYPad'
78
80
 
81
+ // hooks
79
82
  export { useAnimationFrame } from './hooks/useAnimationFrame'
80
- export { useDrag, type UseDragProps } from './hooks/useDrag'
81
- export {
82
- useDragWithElement,
83
- type UseDragWithElementProps,
84
- } from './hooks/useDragWithElement'
83
+ export { useDrag } from './hooks/useDrag'
84
+ export { useDragWithElement } from './hooks/useDragWithElement'
85
85
  export { useEventListener } from './hooks/useEventListener'
86
86
  export { useInterval } from './hooks/useInterval'
87
87
  export { useLongPress } from './hooks/useLongPress'
88
+ export {
89
+ useMIDIAccess,
90
+ NOT_SUPPORTED,
91
+ PERMISSION_DENIED,
92
+ type MIDIAccessError,
93
+ } from './hooks/useMIDIAccess'
94
+ export { useMIDIInput } from './hooks/useMIDIInput'
95
+ export { useMIDIMessage } from './hooks/useMIDIMessage'
@@ -1,5 +1,5 @@
1
1
  :root {
2
- --tremolo-theme-color: #4e76e6;
2
+ --tremolo-theme-color: oklch(59.5% 0.17548 266.373);
3
3
  }
4
4
 
5
5
  .tremolo-no-select {