@tremolo-ui/react 0.1.5 → 0.2.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 (40) hide show
  1. package/dist/index.cjs +325 -239
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +149 -64
  4. package/dist/index.css.map +1 -1
  5. package/dist/index.d.cts +229 -199
  6. package/dist/index.d.cts.map +1 -1
  7. package/dist/index.d.ts +229 -199
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +314 -223
  10. package/dist/index.js.map +1 -1
  11. package/package.json +2 -2
  12. package/src/components/Knob/index.css +50 -25
  13. package/src/components/Knob/index.tsx +37 -32
  14. package/src/components/NumberInput/index.css +31 -15
  15. package/src/components/NumberInput/index.tsx +22 -14
  16. package/src/components/Piano/context.tsx +1 -1
  17. package/src/components/Piano/index.css +17 -8
  18. package/src/components/Piano/index.tsx +259 -220
  19. package/src/components/Piano/key.tsx +3 -3
  20. package/src/components/Slider/Thumb.tsx +1 -1
  21. package/src/components/Slider/Track.tsx +3 -3
  22. package/src/components/Slider/index.css +33 -8
  23. package/src/components/Slider/index.tsx +25 -25
  24. package/src/components/XYPad/Area.tsx +4 -3
  25. package/src/components/XYPad/Thumb.tsx +8 -4
  26. package/src/components/XYPad/index.css +18 -4
  27. package/src/components/XYPad/index.tsx +25 -19
  28. package/src/hooks/useAnimationFrame.ts +3 -0
  29. package/src/hooks/useCallbackRef.ts +2 -2
  30. package/src/hooks/useDrag.ts +2 -1
  31. package/src/hooks/useDragWithElement.ts +2 -1
  32. package/src/hooks/useEventListener.ts +3 -0
  33. package/src/hooks/useInterval.ts +3 -0
  34. package/src/hooks/useLongPress.ts +3 -0
  35. package/src/hooks/useMIDIAccess.ts +40 -0
  36. package/src/hooks/useMIDIInput.ts +50 -0
  37. package/src/hooks/useMIDIMessage.ts +24 -0
  38. package/src/hooks/useRefCallbackEvent.ts +4 -0
  39. package/src/index.ts +30 -35
  40. package/src/styles/global.css +0 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tremolo-ui/react",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "React component library for audio app",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -46,7 +46,7 @@
46
46
  "react-dom": "^18 || ^19"
47
47
  },
48
48
  "dependencies": {
49
- "@tremolo-ui/functions": "^0.1.5",
49
+ "@tremolo-ui/functions": "^0.1.6",
50
50
  "clsx": "^2.1.1",
51
51
  "zustand": "^5.0.3"
52
52
  },
@@ -4,45 +4,70 @@
4
4
  overflow: visible;
5
5
  outline: 0;
6
6
 
7
- &:focus,
8
- &:hover {
9
- > .tremolo-knob-active-line {
10
- color: #4e76e6;
11
- }
12
-
13
- > .tremolo-knob-inactive-line {
14
- color: #ddd;
15
- }
16
-
17
- > .tremolo-knob-thumb {
18
- color: #ccc;
19
- }
20
- }
21
-
22
7
  &[aria-readonly='true'] {
23
8
  cursor: not-allowed;
24
9
  }
25
-
26
- &[aria-disabled='true'] {
27
- > .tremolo-knob-active-line {
28
- color: #5d6478;
29
- }
30
- }
31
10
  }
32
11
 
33
12
  .tremolo-knob-active-line {
34
13
  overflow: visible;
35
- color: #7998ec;
14
+ color: oklch(69.2% 0.17548 266.373);
15
+
16
+ :where(.tremolo-knob:focus, .tremolo-knob:hover) & {
17
+ color: oklch(59.5% 0.17548 266.373);
18
+ }
19
+
20
+ :where(.tremolo-knob[aria-disabled='true']) & {
21
+ color: oklch(50.5% 0.03307 270.021);
22
+ }
36
23
  }
37
24
 
38
25
  .tremolo-knob-inactive-line {
39
- color: #eee;
26
+ color: oklch(95% 0 0);
27
+
28
+ :where(.tremolo-knob:focus, .tremolo-knob:hover) & {
29
+ color: oklch(90% 0 0);
30
+ }
40
31
  }
41
32
 
42
33
  .tremolo-knob-thumb {
43
- color: #d0d0d0;
34
+ color: oklch(85.7% 0 0);
35
+
36
+ :where(.tremolo-knob:focus, .tremolo-knob:hover) & {
37
+ color: oklch(84.5% 0 0);
38
+ }
44
39
  }
45
40
 
46
41
  .tremolo-knob-thumb-line {
47
- color: #eee;
42
+ color: oklch(94.2% 0 0);
43
+ }
44
+
45
+ :where(.dark, [data-theme='dark']) {
46
+ .tremolo-knob-active-line {
47
+ color: oklch(59.5% 0.17548 266.373);
48
+
49
+ :where(.tremolo-knob:focus, .tremolo-knob:hover) & {
50
+ color: oklch(65.2% 0.17548 266.373);
51
+ }
52
+ }
53
+
54
+ .tremolo-knob-inactive-line {
55
+ color: oklch(41.7% 0 0);
56
+
57
+ :where(.tremolo-knob:focus, .tremolo-knob:hover) & {
58
+ color: oklch(44.59% 0 0);
59
+ }
60
+ }
61
+
62
+ .tremolo-knob-thumb {
63
+ color: oklch(48.6% 0 0);
64
+
65
+ :where(.tremolo-knob:focus, .tremolo-knob:hover) & {
66
+ color: oklch(51.7% 0 0);
67
+ }
68
+ }
69
+
70
+ .tremolo-knob-thumb-line {
71
+ color: oklch(84.2% 0 0);
72
+ }
48
73
  }
@@ -43,8 +43,8 @@ export interface KnobProps {
43
43
  */
44
44
  startValue?: number
45
45
 
46
- // TODO: relative size (Lower priority)
47
- size?: number
46
+ /** width and height */
47
+ size?: number | string
48
48
 
49
49
  /** Whether to apply `{use-select: none}` when dragging */
50
50
  bodyNoSelect?: boolean
@@ -69,15 +69,16 @@ export interface KnobProps {
69
69
  thumb?: string
70
70
  /** color */
71
71
  thumbLine?: string
72
-
73
72
  /** percent (0-100) */
74
73
  thumbSize?: number
75
74
  /** percent (0-100) */
76
75
  thumbLineWeight?: number
77
76
  /** percent (0-100) */
78
77
  thumbLineLength?: number
79
- /** percent (0-100) */
78
+ /** line weight [px] */
80
79
  lineWeight?: number
80
+ /** angle range [degree] */
81
+ angleRange?: number
81
82
 
82
83
  classes?: {
83
84
  activeLine?: string
@@ -125,6 +126,11 @@ export const Knob = forwardRef<KnobMethods, Props>(
125
126
  inactiveLine,
126
127
  thumb,
127
128
  thumbLine,
129
+ thumbSize = 84,
130
+ thumbLineWeight = 6,
131
+ thumbLineLength = 35,
132
+ lineWeight = 6,
133
+ angleRange = 270,
128
134
  onChange,
129
135
  onKeyDown,
130
136
  onPointerDown,
@@ -133,26 +139,18 @@ export const Knob = forwardRef<KnobMethods, Props>(
133
139
  classes,
134
140
  ...props
135
141
  }: Props,
136
- ref,
142
+ forwardedRef,
137
143
  ) => {
138
144
  const valueRef = useRef(0)
139
145
  const elmRef = useRef<HTMLOrSVGElement>(null)
140
146
 
141
- // --- interpret props ---
142
- const padding = 8 // %
143
- const thumbLineWeight = 6 // %
144
- const thumbLineLength = 35 // %
145
- const lineWeight = size * 0.06 // px
146
- const p = normalizeValue(value, min, max, skew)
147
- const s = normalizeValue(startValue, min, max, skew)
148
-
149
147
  // --- internal functions ---
150
148
  const onDrag = useCallback(
151
149
  (_x: number, y: number) => {
152
150
  if (!onChange || readonly) return
153
151
  const v = rawValue(valueRef.current - y / 100, min, max, skew)
154
- const v2 = clamp(stepValue(v, step), min, max)
155
- onChange(v2)
152
+ const clamped = clamp(stepValue(v, step), min, max)
153
+ onChange(clamped)
156
154
  },
157
155
  [max, min, onChange, readonly, skew, step],
158
156
  )
@@ -210,7 +208,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
210
208
  [wheel, onChange, readonly, updateValueByEvent],
211
209
  )
212
210
 
213
- useImperativeHandle(ref, () => {
211
+ useImperativeHandle(forwardedRef, () => {
214
212
  return {
215
213
  focus() {
216
214
  elmRef.current?.focus()
@@ -221,11 +219,17 @@ export const Knob = forwardRef<KnobMethods, Props>(
221
219
  }
222
220
  }, [])
223
221
 
224
- const center = size / 2
225
- const r1 = -135
226
- const r2 = -135 + Math.min(p, s) * 270
227
- const r3 = -135 + Math.max(p, s) * 270
228
- const r4 = 135
222
+ const viewBoxSize = 100
223
+ const center = viewBoxSize / 2
224
+ const padding = (viewBoxSize - clamp(thumbSize, 0, 100)) / 2 // %
225
+ const p = normalizeValue(value, min, max, skew)
226
+ const s = normalizeValue(startValue, min, max, skew)
227
+
228
+ // 時計回りで描画していく
229
+ const r1 = -angleRange / 2 // ロータリー開始位置
230
+ const r2 = r1 + Math.min(p, s) * angleRange // activeLineの開始位置
231
+ const r3 = r1 + Math.max(p, s) * angleRange // activeLineの終了位置
232
+ const r4 = angleRange / 2 // ロータリー終了位置
229
233
  const x1 = center + center * Math.cos(radian(r1 - 90))
230
234
  const y1 = center + center * Math.sin(radian(r1 - 90))
231
235
  const x2 = center + center * Math.cos(radian(r2 - 90))
@@ -237,12 +241,13 @@ export const Knob = forwardRef<KnobMethods, Props>(
237
241
 
238
242
  return (
239
243
  <svg
240
- className={clsx('tremolo-knob', className)}
241
244
  ref={(div) => {
242
245
  elmRef.current = div
243
246
  wheelRefCallback(div)
244
247
  touchMoveRefCallback(div)
245
248
  }}
249
+ className={clsx('tremolo-knob', className)}
250
+ viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
246
251
  width={size}
247
252
  height={size}
248
253
  tabIndex={0}
@@ -268,7 +273,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
268
273
  }}
269
274
  {...props}
270
275
  >
271
- {/* rotary meter */}
276
+ {/* lines */}
272
277
  {startValue > min && (
273
278
  <path
274
279
  className={clsx(
@@ -281,13 +286,6 @@ export const Knob = forwardRef<KnobMethods, Props>(
281
286
  strokeWidth={lineWeight}
282
287
  />
283
288
  )}
284
- <path
285
- className={clsx('tremolo-knob-active-line', classes?.activeLine)}
286
- d={`M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`}
287
- fill="none"
288
- stroke={activeLine || 'currentColor'}
289
- strokeWidth={lineWeight}
290
- />
291
289
  {startValue < max && (
292
290
  <path
293
291
  className={clsx(
@@ -300,12 +298,19 @@ export const Knob = forwardRef<KnobMethods, Props>(
300
298
  strokeWidth={lineWeight}
301
299
  />
302
300
  )}
301
+ <path
302
+ className={clsx('tremolo-knob-active-line', classes?.activeLine)}
303
+ d={`M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`}
304
+ fill="none"
305
+ stroke={activeLine || 'currentColor'}
306
+ strokeWidth={lineWeight}
307
+ />
303
308
  {/* thumb */}
304
309
  <svg className={clsx('tremolo-knob-thumb', classes?.thumb)}>
305
310
  <circle
306
311
  cx="50%"
307
312
  cy="50%"
308
- r={`${50 - padding}%`}
313
+ r={`${thumbSize / 2}%`}
309
314
  fill={thumb || 'currentColor'}
310
315
  />
311
316
  <line
@@ -320,7 +325,7 @@ export const Knob = forwardRef<KnobMethods, Props>(
320
325
  // https://bugs.webkit.org/show_bug.cgi?id=201854
321
326
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
322
327
  style={{
323
- transform: `rotate(${-135 + p * 270}deg)`,
328
+ transform: `rotate(${r1 + p * angleRange}deg)`,
324
329
  transformOrigin: '50% 50%',
325
330
  }}
326
331
  />
@@ -1,7 +1,8 @@
1
1
  .tremolo-number-input-wrapper {
2
2
  --border-radius: 4px;
3
3
  --stepper-width: 20px;
4
- --active-color: var(--tremolo-theme-color);
4
+ --active-color: oklch(59.5% 0.17548 266.373);
5
+ --border-color: oklch(81% 0 0);
5
6
 
6
7
  display: inline-flex;
7
8
  flex-direction: row;
@@ -9,13 +10,13 @@
9
10
  width: 140px;
10
11
  height: min-content;
11
12
  border-style: solid;
12
- border-color: #c1c1c1;
13
+ border-color: var(--border-color);
13
14
  border-width: 0px;
14
15
  outline: none;
15
16
  transition: all 0.1s;
16
17
 
17
18
  &:not(:focus-within):has(> .tremolo-number-input[data-error='true']) {
18
- border-color: #f34343;
19
+ border-color: oklch(64.245% 0.21266 25.499);
19
20
  }
20
21
 
21
22
  &[data-variant='outline'] {
@@ -23,29 +24,30 @@
23
24
  border-radius: var(--border-radius);
24
25
 
25
26
  &:hover {
26
- border-color: #909090;
27
+ --border-color: oklch(65.335% 0 0);
27
28
  }
28
29
 
29
30
  &:focus-within {
30
- border-color: var(--active-color);
31
+ --border-color: var(--active-color);
31
32
  box-shadow: 0px 0px 0px 2px
32
33
  color-mix(in srgb, var(--active-color) 10%, transparent);
33
34
  }
34
35
  }
35
36
 
36
37
  &[data-variant='filled'] {
38
+ --bg: oklch(95.88% 0 0);
37
39
  border-width: 1px;
38
40
  border-radius: var(--border-radius);
39
41
  border-color: transparent;
40
- background-color: #edf2f7;
42
+ background-color: var(--bg);
41
43
 
42
44
  &:hover {
43
- background-color: #e4e9ee;
45
+ --bg: oklch(93.166% 0 0);
44
46
  }
45
47
 
46
48
  &:focus-within {
47
49
  background-color: inherit;
48
- border-color: var(--active-color);
50
+ --border-color: var(--active-color);
49
51
  box-shadow: 0px 0px 0px 2px
50
52
  color-mix(in srgb, var(--active-color) 10%, transparent);
51
53
  }
@@ -55,11 +57,11 @@
55
57
  border-bottom-width: 1px;
56
58
 
57
59
  &:hover {
58
- border-color: #909090;
60
+ --border-color: oklch(65.335% 0 0);
59
61
  }
60
62
 
61
63
  &:focus-within {
62
- border-color: var(--active-color);
64
+ --border-color: var(--active-color);
63
65
  box-shadow: 0px 2px 0px 0px
64
66
  color-mix(in srgb, var(--active-color) 10%, transparent);
65
67
  }
@@ -116,7 +118,7 @@
116
118
  .tremolo-number-input-increment-stepper {
117
119
  border-inline-start-width: 1px;
118
120
  border-inline-start-style: solid;
119
- border-inline-start-color: #ccc;
121
+ border-inline-start-color: oklch(84.522% 0 0);
120
122
  font-size: calc(0.75rem);
121
123
  display: flex;
122
124
  justify-content: center;
@@ -128,7 +130,7 @@
128
130
  border-top-right-radius: var(--border-radius);
129
131
 
130
132
  &[aria-disabled='false']:active {
131
- background-color: #3443560e;
133
+ background-color: oklch(37.798% 0.03812 254.612 / 0.055);
132
134
  }
133
135
 
134
136
  &[aria-disabled='true'] {
@@ -143,8 +145,8 @@
143
145
  .tremolo-number-input-decrement-stepper {
144
146
  border-inline-start-width: 1px;
145
147
  border-inline-start-style: solid;
146
- border-inline-start-color: #ccc;
147
- border-top: 1px solid #ccc;
148
+ border-inline-start-color: oklch(84.522% 0 0);
149
+ border-top: 1px solid oklch(84.522% 0 0);
148
150
  font-size: calc(0.75rem);
149
151
  display: flex;
150
152
  justify-content: center;
@@ -156,7 +158,7 @@
156
158
  border-bottom-right-radius: var(--border-radius);
157
159
 
158
160
  &[aria-disabled='false']:active {
159
- background-color: #3443560e;
161
+ background-color: oklch(37.798% 0.03812 254.612 / 0.055);
160
162
  }
161
163
 
162
164
  &[aria-disabled='true'] {
@@ -167,3 +169,17 @@
167
169
  cursor: not-allowed;
168
170
  }
169
171
  }
172
+
173
+ :where(.dark, [data-theme='dark']) {
174
+ .tremolo-number-input-wrapper {
175
+ --border-color: oklch(52% 0 0);
176
+
177
+ &[data-variant='filled'] {
178
+ --bg: oklch(43.5% 0 0);
179
+
180
+ &:hover {
181
+ --bg: oklch(46.6% 0 0);
182
+ }
183
+ }
184
+ }
185
+ }
@@ -4,7 +4,10 @@ import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'
4
4
  import { InputEventOption } from '@tremolo-ui/functions'
5
5
 
6
6
  import { NumberInputProvider } from './context'
7
+ import { DecrementStepper } from './DecrementStepper'
8
+ import { IncrementStepper } from './IncrementStepper'
7
9
  import { InternalInput } from './InternalInput'
10
+ import { Stepper } from './Stepper'
8
11
  import { Units } from './type'
9
12
 
10
13
  /*
@@ -18,11 +21,11 @@ export interface NumberInputProps {
18
21
  value: number | string
19
22
 
20
23
  /**
21
- * @default Number.MIN_SAFE_INTEGER
24
+ * Number.MIN_SAFE_INTEGER
22
25
  */
23
26
  min?: number
24
27
  /**
25
- * @default Number.MAX_SAFE_INTEGER
28
+ * Number.MAX_SAFE_INTEGER
26
29
  */
27
30
  max?: number
28
31
  step?: number
@@ -55,7 +58,6 @@ export interface NumberInputProps {
55
58
  keepWithinRange?: boolean
56
59
  clampValueOnBlur?: boolean
57
60
 
58
- // TODO
59
61
  /**
60
62
  * wheel control option
61
63
  */
@@ -95,11 +97,7 @@ export interface NumberInputMethods {
95
97
  type Props = NumberInputProps &
96
98
  Omit<ComponentPropsWithoutRef<'input'>, keyof NumberInputProps | 'type'>
97
99
 
98
- /**
99
- * Input with some useful functions for entering numerical values.
100
- * @category NumberInput
101
- */
102
- export const NumberInput = forwardRef<NumberInputMethods, Props>(
100
+ const NumberInputImpl = forwardRef<NumberInputMethods, Props>(
103
101
  (
104
102
  {
105
103
  value,
@@ -126,9 +124,9 @@ export const NumberInput = forwardRef<NumberInputMethods, Props>(
126
124
  children,
127
125
  ...props
128
126
  }: Props,
129
- ref,
127
+ forwardedRef,
130
128
  ) => {
131
- const colors: { [key: string]: string | undefined } = {
129
+ const colors: Record<string, string | undefined> = {
132
130
  '--active-color': activeColor,
133
131
  }
134
132
  return (
@@ -154,7 +152,7 @@ export const NumberInput = forwardRef<NumberInputMethods, Props>(
154
152
  data-variant={variant}
155
153
  >
156
154
  <InternalInput
157
- ref={ref}
155
+ ref={forwardedRef}
158
156
  readonly={readonly}
159
157
  disabled={disabled}
160
158
  selectWithFocus={selectWithFocus}
@@ -175,7 +173,17 @@ export const NumberInput = forwardRef<NumberInputMethods, Props>(
175
173
  },
176
174
  )
177
175
 
178
- export * from './Stepper'
179
- export * from './IncrementStepper'
180
- export * from './DecrementStepper'
176
+ /**
177
+ * Input with some useful functions for entering numerical values.
178
+ * @category NumberInput
179
+ */
180
+ export const NumberInput = Object.assign(NumberInputImpl, {
181
+ Stepper,
182
+ IncrementStepper,
183
+ DecrementStepper,
184
+ })
185
+
186
+ export { type StepperProps } from './Stepper'
187
+ export { type IncrementStepperProps } from './IncrementStepper'
188
+ export { type DecrementStepperProps } from './DecrementStepper'
181
189
  export { type Units } from './type'
@@ -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(92% 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% 0 0);
38
+ --active-color: oklch(100% 0 0);
39
+ --active-bg: oklch(44% 0 0);
40
40
 
41
41
  box-sizing: border-box;
42
42
  position: absolute;
@@ -79,3 +79,12 @@
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(80% 0 0);
86
+ --color: oklch(0% 0 0);
87
+ --active-bg: oklch(72% 0 0);
88
+ --active-color: oklch(0% 0 0);
89
+ }
90
+ }