@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
@@ -12,7 +12,7 @@ type State = {
12
12
  glissando: boolean
13
13
  midiMax: number
14
14
  fill: boolean
15
- onPlayNote: (noteNumber: number) => void
15
+ onPlayNote: (noteNumber: number, velocity?: number) => void
16
16
  onStopNote: (noteNumber: number) => void
17
17
  label: (note: number, index: number) => ReactNode
18
18
  }
@@ -7,10 +7,10 @@
7
7
  }
8
8
 
9
9
  .tremolo-piano-white-key {
10
- --bg: white;
11
- --color: black;
12
- --active-bg: #ccc;
13
- --active-color: black;
10
+ --bg: oklch(100% 0 0);
11
+ --color: oklch(0% 0 0);
12
+ --active-bg: oklch(90% 0 0);
13
+ --active-color: oklch(0% 0 0);
14
14
 
15
15
  box-sizing: border-box;
16
16
  position: absolute;
@@ -33,10 +33,10 @@
33
33
  }
34
34
 
35
35
  .tremolo-piano-black-key {
36
- --color: white;
37
- --bg: #333;
38
- --active-color: white;
39
- --active-bg: #666;
36
+ --color: oklch(100% 0 0);
37
+ --bg: oklch(32.109% 0.00004 271.152);
38
+ --active-color: oklch(100% 0 0);
39
+ --active-bg: oklch(47.836% 0.00005 271.152);
40
40
 
41
41
  box-sizing: border-box;
42
42
  position: absolute;
@@ -79,3 +79,19 @@
79
79
  aspect-ratio: 1;
80
80
  max-width: calc(100% - 16px);
81
81
  }
82
+
83
+ :where(.dark, [data-theme='dark']) {
84
+ .tremolo-piano-white-key {
85
+ --bg: oklch(48.193% 0.00005 271.152);
86
+ --color: oklch(100% 0 0);
87
+ --active-bg: oklch(52.78% 0.00006 271.152);
88
+ --active-color: oklch(100% 0 0);
89
+ }
90
+
91
+ .tremolo-piano-black-key {
92
+ --color: oklch(100% 0 0);
93
+ --bg: oklch(32.109% 0 0);
94
+ --active-color: oklch(100% 0 0);
95
+ --active-bg: oklch(38.666% 0.00004 271.152);
96
+ }
97
+ }
@@ -3,11 +3,13 @@ import React, {
3
3
  ComponentPropsWithoutRef,
4
4
  createRef,
5
5
  CSSProperties,
6
+ forwardRef,
6
7
  ReactElement,
7
8
  ReactNode,
8
9
  RefObject,
9
10
  useCallback,
10
11
  useEffect,
12
+ useImperativeHandle,
11
13
  useMemo,
12
14
  useRef,
13
15
  useState,
@@ -35,13 +37,9 @@ import {
35
37
  import { KeyboardShortcuts } from './keyboardShortcuts'
36
38
 
37
39
  /**
38
- * Piano component
39
- *
40
- * TODO:
41
- * - add scale highlight
40
+ * [noteRange.first, noteRange.first + 1 ..., noteRange.last]
41
+ * @category Piano
42
42
  */
43
-
44
- /** @category Piano */
45
43
  export function getNoteRangeArray(noteRange: NoteRange) {
46
44
  return Array.from(
47
45
  { length: noteRange.last - noteRange.first + 1 },
@@ -51,10 +49,8 @@ export function getNoteRangeArray(noteRange: NoteRange) {
51
49
 
52
50
  /** @category Piano */
53
51
  export interface PianoProps {
54
- // required
55
52
  noteRange: NoteRange
56
53
 
57
- // optional
58
54
  glissando?: boolean
59
55
  midiMax?: number
60
56
  keyboardShortcuts?: KeyboardShortcuts
@@ -62,11 +58,10 @@ export interface PianoProps {
62
58
  whiteNoteWidth?: number
63
59
  blackNoteWidth?: number
64
60
  height?: number | string
65
-
66
61
  style?: CSSProperties
67
62
 
68
- onPlayNote?: (noteNumber: number) => void
69
- onStopNote?: (noteNumber: number) => void
63
+ onPlayNote?: (note: number, velocity?: number) => void
64
+ onStopNote?: (note: number) => void
70
65
 
71
66
  label?: (note: number, index: number) => ReactNode
72
67
 
@@ -78,238 +73,278 @@ export interface PianoProps {
78
73
 
79
74
  const blackPerWhiteWidth = defaultBlackKeyWidth / defaultWhiteKeyWidth // 0.65
80
75
 
76
+ /**
77
+ * @category Piano
78
+ */
79
+ export interface PianoMethods {
80
+ playNote: (note: number, velocity?: number) => void
81
+ stopNote: (note: number) => void
82
+ }
83
+
84
+ type Props = PianoProps &
85
+ Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>
86
+
81
87
  /**
82
88
  * Customizable piano component.
83
89
  * @category Piano
84
90
  */
85
- export function Piano({
86
- noteRange,
87
- glissando = true,
88
- midiMax = 127,
89
- keyboardShortcuts,
90
- fill = false,
91
- height = fill ? '100%' : 160,
92
- whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
93
- style,
94
- className,
95
- onPlayNote,
96
- onStopNote,
97
- label,
98
- children,
99
- onPointerDown,
100
- ...props
101
- }: PianoProps & Omit<ComponentPropsWithoutRef<'div'>, keyof PianoProps>) {
102
- // -- state and ref ---
103
- const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
104
- const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
105
- for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
106
- keyRefs.current[i] = createRef<KeyMethods>()
107
- }
108
- const pianoRef = useRef<HTMLDivElement>(null)
109
- const hitKeyIndex = useRef(-1)
91
+ export const Piano = forwardRef<PianoMethods, Props>(
92
+ (
93
+ {
94
+ noteRange,
95
+ glissando = true,
96
+ midiMax = 127,
97
+ keyboardShortcuts,
98
+ fill = false,
99
+ height = fill ? '100%' : 160,
100
+ whiteNoteWidth: _whiteNoteWidth = defaultWhiteKeyWidth,
101
+ style,
102
+ className,
103
+ onPlayNote,
104
+ onStopNote,
105
+ label,
106
+ children,
107
+ onPointerDown,
108
+ ...props
109
+ },
110
+ forwardedRef,
111
+ ) => {
112
+ // -- state and ref ---
113
+ const [whiteNoteWidth, setWhiteNoteWidth] = useState(_whiteNoteWidth)
114
+ const keyRefs = useRef<RefObject<KeyMethods | null>[]>([])
115
+ for (let i = 0; i < noteRange.last - noteRange.first + 1; i++) {
116
+ keyRefs.current[i] = createRef<KeyMethods>()
117
+ }
118
+ const pianoRef = useRef<HTMLDivElement>(null)
119
+ const hitKeyIndex = useRef(-1)
110
120
 
111
- // --- interpret props ---
112
- const noteRangeArray = getNoteRangeArray(noteRange)
113
- const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v))
114
- const blackNotes = noteRangeArray.filter((v) => isBlackKey(v))
115
- const whiteNoteCount = whiteNotes.length
116
- const padding = 1
117
- const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount
118
- // const blackNoteShiftPercent = 0.3
121
+ // --- interpret props ---
122
+ const noteRangeArray = getNoteRangeArray(noteRange)
123
+ const whiteNotes = noteRangeArray.filter((v) => isWhiteKey(v))
124
+ const blackNotes = noteRangeArray.filter((v) => isBlackKey(v))
125
+ const whiteNoteCount = whiteNotes.length
126
+ const padding = 1
127
+ const staticWidth = (whiteNoteWidth + padding) * whiteNoteCount
128
+ // const blackNoteShiftPercent = 0.3
119
129
 
120
- const childrenWithProps = useMemo(() => {
121
- return (
122
- children &&
123
- React.Children.map(children, (child, index) => {
124
- if (React.isValidElement(child)) {
125
- const __width =
126
- child.type == WhiteKey ? whiteNoteWidth : whiteNoteWidth * 0.65
127
- const props = { ref: keyRefs.current[index], __width }
128
- return React.cloneElement(child, props)
130
+ const childrenWithProps = useMemo(() => {
131
+ return (
132
+ children &&
133
+ React.Children.map(children, (child, index) => {
134
+ if (React.isValidElement(child)) {
135
+ const __width =
136
+ child.type == WhiteKey ? whiteNoteWidth : whiteNoteWidth * 0.65
137
+ const props = { ref: keyRefs.current[index], __width }
138
+ return React.cloneElement(child, props)
139
+ }
140
+ return child
141
+ })
142
+ )
143
+ }, [children, whiteNoteWidth])
144
+
145
+ // --- internal functions ---
146
+ const notePosition = useCallback(
147
+ (note: number) => {
148
+ const pitchPositions: Record<NoteKey, number> = {
149
+ C: 0,
150
+ 'C#': 1,
151
+ D: 1,
152
+ 'D#': 2,
153
+ E: 2,
154
+ F: 3,
155
+ 'F#': 4,
156
+ G: 4,
157
+ 'G#': 5,
158
+ A: 5,
159
+ 'A#': 6,
160
+ B: 6,
129
161
  }
130
- return child
131
- })
132
- )
133
- }, [children, whiteNoteWidth])
134
162
 
135
- // --- internal functions ---
136
- const notePosition = useCallback(
137
- (note: number) => {
138
- const pitchPositions: Record<NoteKey, number> = {
139
- C: 0,
140
- 'C#': 1,
141
- D: 1,
142
- 'D#': 2,
143
- E: 2,
144
- F: 3,
145
- 'F#': 4,
146
- G: 4,
147
- 'G#': 5,
148
- A: 5,
149
- 'A#': 6,
150
- B: 6,
151
- }
163
+ const blackNoteWidth = whiteNoteWidth * blackPerWhiteWidth
164
+ const padding = 1
165
+ const targetNoteKey = noteKey(note)
166
+ const firstNoteKey = noteKey(noteRange.first)
167
+ const octave = Math.floor((note - noteRange.first) / 12)
168
+ const octaveOffset =
169
+ noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey)
170
+ ? 1
171
+ : 0
172
+ const w = whiteNoteWidth + padding
173
+ const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey]
174
+ const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0
175
+ return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset
176
+ },
177
+ [noteRange.first, whiteNoteWidth],
178
+ )
152
179
 
153
- const blackNoteWidth = whiteNoteWidth * blackPerWhiteWidth
154
- const padding = 1
155
- const targetNoteKey = noteKey(note)
156
- const firstNoteKey = noteKey(noteRange.first)
157
- const octave = Math.floor((note - noteRange.first) / 12)
158
- const octaveOffset =
159
- noteKeys.indexOf(firstNoteKey) > noteKeys.indexOf(targetNoteKey) ? 1 : 0
160
- const w = whiteNoteWidth + padding
161
- const pos = pitchPositions[targetNoteKey] - pitchPositions[firstNoteKey]
162
- const blackKeyOffset = isBlackKey(note) ? blackNoteWidth / 2 : 0
163
- return pos * w + (octave + octaveOffset) * 7 * w - blackKeyOffset
164
- },
165
- [noteRange.first, whiteNoteWidth],
166
- )
180
+ const getHitKeyIndex = useCallback(
181
+ (x: number, y: number) => {
182
+ if (!pianoRef.current) return -1
183
+ const containerHeight = pianoRef.current.clientHeight
184
+ const notes = [...blackNotes, ...whiteNotes]
185
+ for (let i = 0; i < notes.length; i++) {
186
+ const note = notes[i]
187
+ const pos = notePosition(note)
188
+ const w = isWhiteKey(note)
189
+ ? whiteNoteWidth
190
+ : whiteNoteWidth * blackPerWhiteWidth
191
+ const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6
192
+ if (pos <= x && x < pos + w && 0 <= y && y < h) {
193
+ return note
194
+ }
195
+ }
196
+ return -1
197
+ },
198
+ [blackNotes, notePosition, whiteNoteWidth, whiteNotes],
199
+ )
167
200
 
168
- const getHitKeyIndex = useCallback(
169
- (x: number, y: number) => {
170
- if (!pianoRef.current) return -1
171
- const containerHeight = pianoRef.current.clientHeight
172
- const notes = [...blackNotes, ...whiteNotes]
173
- for (let i = 0; i < notes.length; i++) {
174
- const note = notes[i]
175
- const pos = notePosition(note)
176
- const w = isWhiteKey(note)
177
- ? whiteNoteWidth
178
- : whiteNoteWidth * blackPerWhiteWidth
179
- const h = isWhiteKey(note) ? containerHeight : containerHeight * 0.6
180
- if (pos <= x && x < pos + w && 0 <= y && y < h) {
181
- return note
201
+ // TODO: 単一のポインターに対しては、useDragで対応可能だが、
202
+ // マルチタッチに対しては、TouchEventを使う必要がありそう
203
+ // 取り敢えず、シングルタッチだけ対応
204
+ const onDrag = useCallback(
205
+ (perX: number, perY: number) => {
206
+ if (!pianoRef.current) return
207
+ const x = perX * staticWidth
208
+ const y = perY * pianoRef.current.clientHeight
209
+ const note = getHitKeyIndex(x, y)
210
+ const index = noteRangeArray.indexOf(note)
211
+ if (index == -1) return
212
+ if (hitKeyIndex.current != index) {
213
+ keyRefs.current[hitKeyIndex.current]?.current?.stop()
214
+ keyRefs.current[index]?.current?.play()
215
+ hitKeyIndex.current = index
182
216
  }
183
- }
184
- return -1
185
- },
186
- [blackNotes, notePosition, whiteNoteWidth, whiteNotes],
187
- )
217
+ },
218
+ [getHitKeyIndex, noteRangeArray, staticWidth],
219
+ )
188
220
 
189
- // TODO: 単一のポインターに対しては、useDragで対応可能だが、
190
- // マルチタッチに対しては、TouchEventを使う必要がありそう
191
- // 取り敢えず、シングルタッチだけ対応
192
- const onDrag = useCallback(
193
- (perX: number, perY: number) => {
194
- if (!pianoRef.current) return
195
- const x = perX * staticWidth
196
- const y = perY * pianoRef.current.clientHeight
197
- const note = getHitKeyIndex(x, y)
198
- const index = noteRangeArray.indexOf(note)
199
- if (index == -1) return
200
- if (hitKeyIndex.current != index) {
221
+ const onDragEnd = useCallback(() => {
222
+ if (keyRefs.current[hitKeyIndex.current]?.current?.played()) {
201
223
  keyRefs.current[hitKeyIndex.current]?.current?.stop()
202
- keyRefs.current[index]?.current?.play()
203
- hitKeyIndex.current = index
204
224
  }
205
- },
206
- [getHitKeyIndex, noteRangeArray, staticWidth],
207
- )
225
+ hitKeyIndex.current = -1
226
+ }, [keyRefs])
208
227
 
209
- const onDragEnd = useCallback(() => {
210
- if (keyRefs.current[hitKeyIndex.current]?.current?.played()) {
211
- keyRefs.current[hitKeyIndex.current]?.current?.stop()
212
- }
213
- hitKeyIndex.current = -1
214
- }, [keyRefs])
228
+ // --- hooks ---
229
+ useEffect(() => {
230
+ if (fill && pianoRef.current) {
231
+ const parent = pianoRef.current.parentElement
232
+ if (!parent) throw new Error("doesn't have a parent element.")
233
+ const resizeObserver = new ResizeObserver(() => {
234
+ const w = pianoRef.current!.clientWidth
235
+ setWhiteNoteWidth(w / whiteNoteCount - padding)
236
+ })
237
+ resizeObserver.observe(parent)
238
+ return () => {
239
+ resizeObserver.unobserve(parent)
240
+ }
241
+ } else {
242
+ setWhiteNoteWidth(_whiteNoteWidth)
243
+ }
244
+ }, [fill, _whiteNoteWidth, whiteNoteCount])
215
245
 
216
- // --- hooks ---
217
- useEffect(() => {
218
- if (fill && pianoRef.current) {
219
- const parent = pianoRef.current.parentElement
220
- if (!parent) throw new Error("doesn't have a parent element.")
221
- const resizeObserver = new ResizeObserver(() => {
222
- const w = pianoRef.current!.clientWidth
223
- setWhiteNoteWidth(w / whiteNoteCount - padding)
246
+ const [touchMoveRefCallback, pointerDownHandler] =
247
+ useDragWithElement<HTMLDivElement>({
248
+ baseElementRef: pianoRef,
249
+ onDrag: onDrag,
250
+ onDragEnd: onDragEnd,
224
251
  })
225
- resizeObserver.observe(parent)
226
- return () => {
227
- resizeObserver.unobserve(parent)
228
- }
229
- } else {
230
- setWhiteNoteWidth(_whiteNoteWidth)
231
- }
232
- }, [fill, _whiteNoteWidth, whiteNoteCount])
233
252
 
234
- const [touchMoveRefCallback, pointerDownHandler] =
235
- useDragWithElement<HTMLDivElement>({
236
- baseElementRef: pianoRef,
237
- onDrag: onDrag,
238
- onDragEnd: onDragEnd,
253
+ useEventListener(globalThis.window, 'keydown', (e) => {
254
+ if (e.repeat) return
255
+ if (!keyboardShortcuts) return
256
+ const index = keyboardShortcuts.keys.indexOf(e.key)
257
+ if (index != -1) {
258
+ if (!keyRefs.current[index]?.current?.played()) {
259
+ keyRefs.current[index]?.current?.play()
260
+ }
261
+ }
239
262
  })
240
263
 
241
- useEventListener(globalThis.window, 'keydown', (e) => {
242
- if (e.repeat) return
243
- if (!keyboardShortcuts) return
244
- const index = keyboardShortcuts.keys.indexOf(e.key)
245
- if (index != -1) {
246
- if (!keyRefs.current[index]?.current?.played()) {
247
- keyRefs.current[index]?.current?.play()
264
+ useEventListener(globalThis.window, 'keyup', (e) => {
265
+ if (e.repeat) return
266
+ if (!keyboardShortcuts) return
267
+ const index = keyboardShortcuts.keys.indexOf(e.key)
268
+ if (index != -1) {
269
+ keyRefs.current[index]?.current?.stop()
248
270
  }
249
- }
250
- })
271
+ })
251
272
 
252
- useEventListener(globalThis.window, 'keyup', (e) => {
253
- if (e.repeat) return
254
- if (!keyboardShortcuts) return
255
- const index = keyboardShortcuts.keys.indexOf(e.key)
256
- if (index != -1) {
257
- keyRefs.current[index]?.current?.stop()
258
- }
259
- })
273
+ useImperativeHandle(forwardedRef, () => {
274
+ return {
275
+ playNote(note, velocity) {
276
+ const index = noteRangeArray.indexOf(note)
277
+ if (index != -1) {
278
+ if (!keyRefs.current[index]?.current?.played()) {
279
+ keyRefs.current[index]?.current?.play(velocity)
280
+ }
281
+ } else {
282
+ onPlayNote?.(note, velocity)
283
+ }
284
+ },
285
+ stopNote(note: number) {
286
+ const index = noteRangeArray.indexOf(note)
287
+ if (index != -1) {
288
+ keyRefs.current[index]?.current?.stop()
289
+ } else {
290
+ onStopNote?.(note)
291
+ }
292
+ },
293
+ }
294
+ }, [noteRangeArray, onPlayNote, onStopNote])
260
295
 
261
- return (
262
- <PianoProvider
263
- notePosition={notePosition}
264
- noteRange={noteRange}
265
- glissando={glissando}
266
- midiMax={midiMax}
267
- fill={fill}
268
- onPlayNote={onPlayNote}
269
- onStopNote={onStopNote}
270
- label={label}
271
- >
272
- <div
273
- ref={(div) => {
274
- pianoRef.current = div
275
- touchMoveRefCallback(div)
276
- }}
277
- className={clsx('tremolo-piano', className)}
278
- style={{
279
- width: fill ? '100%' : staticWidth,
280
- height: height,
281
- ...style,
282
- }}
283
- onPointerDown={(event) => {
284
- pointerDownHandler(event)
285
- onPointerDown?.(event)
286
- }}
287
- {...props}
296
+ return (
297
+ <PianoProvider
298
+ notePosition={notePosition}
299
+ noteRange={noteRange}
300
+ glissando={glissando}
301
+ midiMax={midiMax}
302
+ fill={fill}
303
+ onPlayNote={onPlayNote}
304
+ onStopNote={onStopNote}
305
+ label={label}
288
306
  >
289
- {childrenWithProps ||
290
- noteRangeArray.map((note, index) =>
291
- isWhiteKey(note) ? (
292
- <WhiteKey
293
- ref={keyRefs.current[index]}
294
- key={note}
295
- noteNumber={note}
296
- __width={whiteNoteWidth}
297
- />
298
- ) : (
299
- <BlackKey
300
- ref={keyRefs.current[index]}
301
- key={note}
302
- noteNumber={note}
303
- __width={whiteNoteWidth * blackPerWhiteWidth}
304
- />
305
- ),
306
- )}
307
- </div>
308
- </PianoProvider>
309
- )
310
- }
311
-
312
- export * from './keyboardShortcuts'
307
+ <div
308
+ ref={(div) => {
309
+ pianoRef.current = div
310
+ touchMoveRefCallback(div)
311
+ }}
312
+ className={clsx('tremolo-piano', className)}
313
+ style={{
314
+ width: fill ? '100%' : staticWidth,
315
+ height: height,
316
+ ...style,
317
+ }}
318
+ onPointerDown={(event) => {
319
+ pointerDownHandler(event)
320
+ onPointerDown?.(event)
321
+ }}
322
+ {...props}
323
+ >
324
+ {childrenWithProps ||
325
+ noteRangeArray.map((note, index) =>
326
+ isWhiteKey(note) ? (
327
+ <WhiteKey
328
+ ref={keyRefs.current[index]}
329
+ key={note}
330
+ noteNumber={note}
331
+ __width={whiteNoteWidth}
332
+ />
333
+ ) : (
334
+ <BlackKey
335
+ ref={keyRefs.current[index]}
336
+ key={note}
337
+ noteNumber={note}
338
+ __width={whiteNoteWidth * blackPerWhiteWidth}
339
+ />
340
+ ),
341
+ )}
342
+ </div>
343
+ </PianoProvider>
344
+ )
345
+ },
346
+ )
313
347
 
348
+ export { type KeyboardShortcuts, SHORTCUTS } from './keyboardShortcuts'
314
349
  export { WhiteKey, BlackKey, type KeyProps, type KeyMethods } from './key'
315
350
  export { KeyLabel, type KeyLabelProps } from './KeyLabel'
@@ -40,7 +40,7 @@ export interface KeyProps {
40
40
  * @category Piano
41
41
  */
42
42
  export interface KeyMethods {
43
- play: () => void
43
+ play: (velocity?: number) => void
44
44
  stop: () => void
45
45
  played: () => boolean
46
46
  }
@@ -92,10 +92,10 @@ const KeyImpl = forwardRef<KeyMethods, ImplProps>(
92
92
 
93
93
  useImperativeHandle(ref, () => {
94
94
  return {
95
- play() {
95
+ play(velocity) {
96
96
  if (disabled) return
97
97
  setPlayed(true)
98
- if (onPlayNote) onPlayNote(noteNumber)
98
+ if (onPlayNote) onPlayNote(noteNumber, velocity)
99
99
  },
100
100
  stop() {
101
101
  setPlayed(false)
@@ -2,10 +2,10 @@ import clsx from 'clsx'
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
3
 
4
4
  import { xor } from '@tremolo-ui/functions'
5
- import { generateOptionsList, ScaleOptions } from '@tremolo-ui/functions/Slider'
6
5
 
7
6
  import { useSliderContext } from './context'
8
7
  import { ScaleOption } from './ScaleOption'
8
+ import { generateOptionsList, ScaleOptions } from './type'
9
9
 
10
10
  /** @category Slider */
11
11
  export interface ScaleProps {
@@ -2,9 +2,9 @@ import clsx from 'clsx'
2
2
  import { ComponentPropsWithoutRef, CSSProperties, useCallback } from 'react'
3
3
 
4
4
  import { toFixed, xor, normalizeValue } from '@tremolo-ui/functions'
5
- import { ScaleType } from '@tremolo-ui/functions/Slider'
6
5
 
7
6
  import { useSliderContext } from './context'
7
+ import { ScaleType } from './type'
8
8
 
9
9
  /** @category Slider */
10
10
  export interface ScaleOptionProps {
@@ -64,8 +64,8 @@ export function SliderTrack({
64
64
  : {
65
65
  ...colors,
66
66
  background: xor(vertical, reverse)
67
- ? `linear-gradient(to ${direction}, var(--inactive, #eee) ${__percent}%, var(--active, #7998ec) ${__percent}%)`
68
- : `linear-gradient(to ${direction}, var(--active, #7998ec) ${__percent}%, var(--inactive, #eee) ${__percent}%)`,
67
+ ? `linear-gradient(to ${direction}, var(--inactive) ${__percent}%, var(--active) ${__percent}%)`
68
+ : `linear-gradient(to ${direction}, var(--active) ${__percent}%, var(--inactive) ${__percent}%)`,
69
69
  borderRadius: styleHelper(thickness!, '/', 2),
70
70
  width: !vertical ? length : thickness,
71
71
  height: vertical ? length : thickness,