@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
@@ -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
@@ -1,18 +1,35 @@
1
- import { useEffect, useState } from 'react'
2
-
3
- import { createWheel } from '@tremolo-ui/dom'
4
-
5
- import { useCallbackRef } from './useCallbackRef'
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
+ }
6
22
 
7
23
  /**
8
24
  * Listen to wheel events on an element.
9
25
  *
10
26
  * The listener is not passive, so the handler may call `preventDefault()`.
11
27
  *
12
- * @returns a ref callback to attach to the element
28
+ * @returns a ref callback to attach to the element, unused when `target` is given
13
29
  */
14
30
  export function useWheel<T extends Element>(
15
31
  onWheel: (event: WheelEvent) => void,
32
+ { requireFocus = false, target }: UseWheelOptions = {},
16
33
  ): (node: T | null) => void {
17
34
  const wheelHandler = useCallbackRef(onWheel)
18
35
 
@@ -20,12 +37,15 @@ export function useWheel<T extends Element>(
20
37
  const [node, setNode] = useState<T | null>(null)
21
38
 
22
39
  useEffect(() => {
23
- if (!node) return
40
+ const element = target ? target.current : node
41
+ if (!element) return
24
42
 
25
- const instance = createWheel(node, (event) => wheelHandler(event))
43
+ const instance = createWheel(element, (event) => wheelHandler(event), {
44
+ requireFocus,
45
+ })
26
46
 
27
47
  return () => instance.destroy()
28
- }, [node, wheelHandler])
48
+ }, [node, target, wheelHandler, requireFocus])
29
49
 
30
50
  return setNode
31
51
  }
package/src/index.ts CHANGED
@@ -19,35 +19,40 @@ export {
19
19
  export { Knob, type KnobProps, type KnobMethods } from './components/Knob'
20
20
  export {
21
21
  NumberInput,
22
+ useNumberInputContext,
22
23
  type NumberInputProps,
23
24
  type NumberInputMethods,
25
+ type NumberInputFieldProps,
24
26
  type StepperProps,
25
27
  type IncrementStepperProps,
26
28
  type DecrementStepperProps,
27
29
  } from './components/NumberInput'
28
30
  export {
29
31
  Piano,
30
- getNoteRangeArray,
31
32
  SHORTCUTS,
32
33
  type PianoProps,
33
34
  type PianoMethods,
34
- type KeyProps,
35
- type KeyMethods,
36
- type KeyLabelProps,
35
+ type KeyState,
36
+ type KeyAttributes,
37
+ type CSSVariables,
37
38
  type KeyboardShortcuts,
38
39
  } from './components/Piano'
39
40
  export {
40
41
  PointsEditor,
42
+ usePointsEditorContext,
41
43
  clampPoint,
42
44
  type PointsEditorProps,
45
+ type PointsEditorContextValue,
46
+ type PointsEditorBackgroundProps,
47
+ type PointsEditorContainerProps,
43
48
  type PointProps,
44
49
  type PointBaseType,
45
50
  } from './components/PointsEditor'
46
51
  export {
47
52
  Slider,
48
53
  useSliderContext,
49
- type ScaleOptionProps,
50
- type ScaleProps,
54
+ type MarksOptionProps,
55
+ type MarksProps,
51
56
  type SliderMethods,
52
57
  type SliderProps,
53
58
  type SliderThumbMethods,
@@ -61,13 +66,14 @@ export {
61
66
  type XYPadAreaProps,
62
67
  type XYPadThumbProps,
63
68
  type XYPadThumbMethods,
64
- type ValueOptions,
69
+ type XY,
70
+ type XYInput,
65
71
  } from './components/XYPad'
66
72
 
67
73
  // hooks
68
74
  export { useAnimationFrame } from './hooks/useAnimationFrame'
69
75
  export { useDrag } from './hooks/useDrag'
70
- export { useDragWithElement } from './hooks/useDragWithElement'
76
+ export { useDragValue, type UseDragValueOptions } from './hooks/useDragValue'
71
77
  export { useEventListener } from './hooks/useEventListener'
72
78
  export { useInterval } from './hooks/useInterval'
73
79
  export { useLongPress } from './hooks/useLongPress'
@@ -79,4 +85,4 @@ export {
79
85
  } from './hooks/useMIDIAccess'
80
86
  export { useMIDIInput } from './hooks/useMIDIInput'
81
87
  export { useMIDIMessage } from './hooks/useMIDIMessage'
82
- export { useWheel } from './hooks/useWheel'
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,216 +0,0 @@
1
- import clsx from 'clsx'
2
- import {
3
- ComponentPropsWithoutRef,
4
- forwardRef,
5
- useCallback,
6
- useImperativeHandle,
7
- useMemo,
8
- useRef,
9
- useState,
10
- } from 'react'
11
-
12
- import {
13
- clamp,
14
- InputEventOption,
15
- normalizeValue,
16
- rawValue,
17
- stepValue,
18
- } from '@tremolo-ui/functions'
19
-
20
- import { useWheel } from '../../hooks/useWheel'
21
-
22
- import { safeClamp, useNumberInputContext } from './context'
23
- import { parseValue } from './type'
24
-
25
- import { NumberInputMethods } from '.'
26
-
27
- interface InternalProps {
28
- disabled: boolean
29
- readonly: boolean
30
- wheel: InputEventOption | null
31
- keyboard: InputEventOption | null
32
-
33
- selectWithFocus?: 'all' | 'number' | 'none'
34
- blurOnEnter?: boolean
35
- keepWithinRange?: boolean
36
- clampValueOnBlur?: boolean
37
-
38
- onChange?: (value: number, text: string) => void
39
- onFocus?: (
40
- value: number,
41
- text: string,
42
- event: React.FocusEvent<HTMLInputElement, Element>,
43
- ) => void
44
- onBlur?: (
45
- value: number,
46
- text: string,
47
- event: React.FocusEvent<HTMLInputElement, Element>,
48
- ) => void
49
- }
50
-
51
- type InternalPropsWithAttributes = InternalProps &
52
- Omit<ComponentPropsWithoutRef<'input'>, keyof InternalProps>
53
-
54
- export const InternalInput = forwardRef<
55
- NumberInputMethods,
56
- InternalPropsWithAttributes
57
- >(
58
- (
59
- {
60
- disabled,
61
- readonly,
62
- selectWithFocus,
63
- blurOnEnter,
64
- clampValueOnBlur,
65
- wheel,
66
- keyboard,
67
- className,
68
- onChange,
69
- onFocus,
70
- onBlur,
71
- onKeyDown,
72
- children: _children,
73
- ...props
74
- }: InternalPropsWithAttributes,
75
- ref,
76
- ) => {
77
- const elmRef = useRef<HTMLInputElement>(null)
78
- const [editing, setEditing] = useState(false)
79
- const value = useNumberInputContext((s) => s.value)
80
- const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
81
- const min = useNumberInputContext((s) => s.min)
82
- const max = useNumberInputContext((s) => s.max)
83
- const step = useNumberInputContext((s) => s.step)
84
- const units = useNumberInputContext((s) => s.units)
85
- const digit = useNumberInputContext((s) => s.digit)
86
- const change = useNumberInputContext((s) => s.change)
87
-
88
- const error = useMemo(() => {
89
- if (editing) return false
90
- const v = valueAsNumber
91
- return (min ?? -Infinity) > v || v > (max ?? Infinity)
92
- }, [editing, max, min, valueAsNumber])
93
-
94
- const updateValueByEvent = useCallback(
95
- (eventType: InputEventOption[0], x: number) => {
96
- let newValue
97
- if (eventType == 'normalized') {
98
- if (!min || !max) {
99
- throw new Error(
100
- '[NumberInput] "min" and "max" are required when InputEventOption[0] is set to "normalized".',
101
- )
102
- }
103
- const n = normalizeValue(valueAsNumber, min, max)
104
- newValue = rawValue(n + x, min, max)
105
- return clamp(stepValue(newValue, step), min, max)
106
- } else {
107
- newValue = valueAsNumber + x
108
- return stepValue(newValue, step)
109
- }
110
- },
111
- [max, min, step, valueAsNumber],
112
- )
113
-
114
- const handleKeyDown = useCallback(
115
- (event: React.KeyboardEvent<HTMLDivElement>) => {
116
- if (!keyboard || !onChange || readonly) return
117
- const key = event.key
118
- if (!['ArrowUp', 'ArrowDown'].includes(key)) return
119
- event.preventDefault()
120
- const x = key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
121
- const parsed = parseValue(String(updateValueByEvent(keyboard[0], x)))
122
- // console.log('key control')
123
- onChange?.(parsed.rawValue, parsed.formatValue)
124
- },
125
- [keyboard, readonly, onChange, updateValueByEvent],
126
- )
127
-
128
- const wheelRefCallback = useWheel<HTMLInputElement>((event) => {
129
- if (!wheel || readonly) return
130
- event.preventDefault()
131
- if (!onChange || event.deltaY == 0) return
132
- const x = Math.sign(event.deltaY) * -wheel[1]
133
- const parsed = parseValue(String(updateValueByEvent(wheel[0], x)))
134
- // console.log('wheel control')
135
- onChange?.(parsed.rawValue, parsed.formatValue)
136
- })
137
-
138
- useImperativeHandle(ref, () => {
139
- return {
140
- focus() {
141
- elmRef.current?.focus()
142
- },
143
- blur() {
144
- elmRef.current?.blur()
145
- },
146
- }
147
- }, [])
148
-
149
- return (
150
- <input
151
- ref={(input) => {
152
- elmRef.current = input
153
- wheelRefCallback(input)
154
- }}
155
- className={clsx('tremolo-number-input', className)}
156
- value={value}
157
- min={min}
158
- max={max}
159
- step={step}
160
- type="text"
161
- aria-disabled={disabled}
162
- aria-readonly={readonly}
163
- tabIndex={-1}
164
- data-error={error}
165
- onChange={(event) => {
166
- if (readonly) return
167
- const v = event.currentTarget.value
168
- const cursorPos = event.target.selectionStart
169
- change(v)
170
- const valueAsNumber = parseValue(v, units, digit).rawValue
171
- onChange?.(valueAsNumber, v)
172
- if (cursorPos) {
173
- event.target.selectionStart = cursorPos
174
- event.target.selectionEnd = cursorPos
175
- }
176
- }}
177
- onFocus={(event) => {
178
- setEditing(true)
179
- const v = event.currentTarget.value
180
- const parsed = parseValue(v, units, digit)
181
- if (selectWithFocus == 'all') {
182
- event.currentTarget.setSelectionRange(0, v.length)
183
- } else if (selectWithFocus == 'number') {
184
- event.currentTarget.setSelectionRange(
185
- 0,
186
- parsed.formatValue.length - parsed.unit.length,
187
- )
188
- }
189
- onFocus?.(parsed.rawValue, parsed.formatValue, event)
190
- }}
191
- onBlur={(event) => {
192
- setEditing(false)
193
- // update value
194
- let v = parseValue(event.currentTarget.value, units, digit).rawValue
195
- if (clampValueOnBlur) {
196
- v = safeClamp(v, min, max)
197
- }
198
- const parsed = parseValue(String(v), units, digit)
199
- change(parsed.formatValue)
200
- if (clampValueOnBlur) {
201
- onChange?.(parsed.rawValue, parsed.formatValue)
202
- }
203
- onBlur?.(parsed.rawValue, parsed.formatValue, event)
204
- }}
205
- onKeyDown={(event) => {
206
- if (blurOnEnter && event.key == 'Enter') {
207
- event.currentTarget.blur()
208
- }
209
- handleKeyDown(event)
210
- onKeyDown?.(event)
211
- }}
212
- {...props}
213
- />
214
- )
215
- },
216
- )
@@ -1,58 +0,0 @@
1
- /**
2
- * [unit, scale][]
3
- * @example
4
- * [['Hz', 1], ['kHz', 1000]]
5
- */
6
- export type Units = [string, number][]
7
-
8
- export function selectUnit(units: Units, value: number): [string, number] {
9
- let i = 0
10
- for (; i < units.length; i++) {
11
- if (Math.abs(units[i][1]) > Math.abs(value)) break
12
- }
13
- return units[Math.max(0, i - 1)]
14
- }
15
-
16
- export function parseValue(
17
- inputString: string,
18
- units?: string | Units,
19
- digit?: number,
20
- ): {
21
- rawValue: number
22
- formatValue: string
23
- unit: string
24
- } {
25
- const str = inputString.trim()
26
- if (!units || typeof units == 'string') {
27
- const m = str.match(/-?\d+(\.\d+)?/)
28
- let v = Number(m?.[0] ?? '0')
29
- v = isNaN(v) ? 0 : v
30
- return {
31
- rawValue: v,
32
- formatValue: (digit != undefined ? v.toFixed(digit) : v) + (units ?? ''),
33
- unit: units ?? '',
34
- }
35
- }
36
-
37
- const unitList = units.map(([u, _s]) => u)
38
- const scaleList = units.map(([_u, s]) => s)
39
- let rawValue = 0
40
- let unit = unitList[0]
41
- let formatValue =
42
- (digit != undefined ? rawValue.toFixed(digit) : rawValue) + unit
43
- const m = str.match(/^(-?\d+(\.\d+)?)\s*(\w*)$/)
44
- if (m) {
45
- rawValue = Number(m[1]) || 0
46
- const uIndex = unitList.indexOf(m[3])
47
- rawValue *= uIndex != -1 ? scaleList[uIndex] : 1
48
- const [u, scale] = selectUnit(units, rawValue)
49
- const v = rawValue / scale
50
- formatValue = (digit != undefined ? v.toFixed(digit) : v) + u
51
- unit = u
52
- }
53
- return {
54
- rawValue: rawValue,
55
- formatValue: formatValue,
56
- unit: unit,
57
- }
58
- }
@@ -1,51 +0,0 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'
3
-
4
- import { usePianoContext } from './context'
5
-
6
- import { getNoteRangeArray } from '.'
7
-
8
- export interface KeyLabelProps {
9
- /**
10
- * override Piano.label
11
- */
12
- label?: (note: number, index: number) => ReactNode
13
-
14
- wrapperClassName?: string
15
- wrapperStyle?: CSSProperties
16
-
17
- /** @internal */
18
- __note?: number
19
- /** @internal */
20
- __label?: (note: number, index: number) => ReactNode
21
- }
22
-
23
- export function KeyLabel({
24
- label,
25
- className,
26
- wrapperClassName,
27
- wrapperStyle,
28
- __note,
29
- __label,
30
- ...props
31
- }: KeyLabelProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KeyLabelProps>) {
32
- const noteRange = usePianoContext((s) => s.noteRange)
33
- const noteRangeArray = getNoteRangeArray(noteRange)
34
- const index = noteRangeArray.indexOf(__note!)
35
- const content = label
36
- ? label(__note!, index)
37
- : __label && __label(__note!, index)
38
-
39
- return (
40
- (content != undefined || content != null) && (
41
- <div
42
- className={clsx('tremolo-piano-key-label-wrapper', wrapperClassName)}
43
- style={wrapperStyle}
44
- >
45
- <div className={clsx('tremolo-piano-key-label', className)} {...props}>
46
- {content}
47
- </div>
48
- </div>
49
- )
50
- )
51
- }