@tremolo-ui/react 0.0.10 → 0.0.12

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 (137) hide show
  1. package/dist/cjs/index.js +435 -203
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/AnimationCanvas/index.d.ts +2 -2
  4. package/dist/cjs/types/components/AnimationCanvas/index.d.ts.map +1 -1
  5. package/dist/cjs/types/components/AnimationKnob/index.d.ts +10 -3
  6. package/dist/cjs/types/components/AnimationKnob/index.d.ts.map +1 -1
  7. package/dist/cjs/types/components/Knob/index.d.ts +13 -3
  8. package/dist/cjs/types/components/Knob/index.d.ts.map +1 -1
  9. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts +2 -0
  10. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts.map +1 -1
  11. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts +2 -0
  12. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts.map +1 -1
  13. package/dist/cjs/types/components/NumberInput/InternalInput.d.ts +18 -0
  14. package/dist/cjs/types/components/NumberInput/InternalInput.d.ts.map +1 -0
  15. package/dist/cjs/types/components/NumberInput/Stepper.d.ts +2 -0
  16. package/dist/cjs/types/components/NumberInput/Stepper.d.ts.map +1 -1
  17. package/dist/cjs/types/components/NumberInput/context.d.ts +3 -2
  18. package/dist/cjs/types/components/NumberInput/context.d.ts.map +1 -1
  19. package/dist/cjs/types/components/NumberInput/index.d.ts +12 -3
  20. package/dist/cjs/types/components/NumberInput/index.d.ts.map +1 -1
  21. package/dist/cjs/types/components/Piano/BlackKey.d.ts.map +1 -1
  22. package/dist/cjs/types/components/Piano/WhiteKey.d.ts.map +1 -1
  23. package/dist/cjs/types/components/Piano/index.d.ts +7 -4
  24. package/dist/cjs/types/components/Piano/index.d.ts.map +1 -1
  25. package/dist/cjs/types/components/Piano/key.d.ts +4 -0
  26. package/dist/cjs/types/components/Piano/key.d.ts.map +1 -1
  27. package/dist/cjs/types/components/Piano/keyboardShortcuts.d.ts +2 -2
  28. package/dist/cjs/types/components/Slider/Scale.d.ts +1 -3
  29. package/dist/cjs/types/components/Slider/Scale.d.ts.map +1 -1
  30. package/dist/cjs/types/components/Slider/Thumb.d.ts +1 -0
  31. package/dist/cjs/types/components/Slider/Thumb.d.ts.map +1 -1
  32. package/dist/cjs/types/components/Slider/Track.d.ts +2 -4
  33. package/dist/cjs/types/components/Slider/Track.d.ts.map +1 -1
  34. package/dist/cjs/types/components/Slider/context.d.ts +2 -10
  35. package/dist/cjs/types/components/Slider/context.d.ts.map +1 -1
  36. package/dist/cjs/types/components/Slider/index.d.ts +5 -1
  37. package/dist/cjs/types/components/Slider/index.d.ts.map +1 -1
  38. package/dist/cjs/types/components/WheelObserver/index.d.ts +3 -3
  39. package/dist/cjs/types/components/WheelObserver/index.d.ts.map +1 -1
  40. package/dist/cjs/types/components/XYPad/Area.d.ts +3 -3
  41. package/dist/cjs/types/components/XYPad/Area.d.ts.map +1 -1
  42. package/dist/cjs/types/components/XYPad/Thumb.d.ts +1 -0
  43. package/dist/cjs/types/components/XYPad/Thumb.d.ts.map +1 -1
  44. package/dist/cjs/types/components/XYPad/index.d.ts +6 -1
  45. package/dist/cjs/types/components/XYPad/index.d.ts.map +1 -1
  46. package/dist/cjs/types/hooks/useAnimationFrame.d.ts.map +1 -1
  47. package/dist/cjs/types/hooks/useInterval.d.ts +2 -0
  48. package/dist/cjs/types/hooks/useInterval.d.ts.map +1 -0
  49. package/dist/cjs/types/hooks/useLongPress.d.ts +2 -0
  50. package/dist/cjs/types/hooks/useLongPress.d.ts.map +1 -0
  51. package/dist/cjs/types/index.d.ts +5 -0
  52. package/dist/cjs/types/index.d.ts.map +1 -1
  53. package/dist/esm/index.js +431 -204
  54. package/dist/esm/index.js.map +1 -1
  55. package/dist/esm/types/components/AnimationCanvas/index.d.ts +2 -2
  56. package/dist/esm/types/components/AnimationCanvas/index.d.ts.map +1 -1
  57. package/dist/esm/types/components/AnimationKnob/index.d.ts +10 -3
  58. package/dist/esm/types/components/AnimationKnob/index.d.ts.map +1 -1
  59. package/dist/esm/types/components/Knob/index.d.ts +13 -3
  60. package/dist/esm/types/components/Knob/index.d.ts.map +1 -1
  61. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts +2 -0
  62. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts.map +1 -1
  63. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts +2 -0
  64. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts.map +1 -1
  65. package/dist/esm/types/components/NumberInput/InternalInput.d.ts +18 -0
  66. package/dist/esm/types/components/NumberInput/InternalInput.d.ts.map +1 -0
  67. package/dist/esm/types/components/NumberInput/Stepper.d.ts +2 -0
  68. package/dist/esm/types/components/NumberInput/Stepper.d.ts.map +1 -1
  69. package/dist/esm/types/components/NumberInput/context.d.ts +3 -2
  70. package/dist/esm/types/components/NumberInput/context.d.ts.map +1 -1
  71. package/dist/esm/types/components/NumberInput/index.d.ts +12 -3
  72. package/dist/esm/types/components/NumberInput/index.d.ts.map +1 -1
  73. package/dist/esm/types/components/Piano/BlackKey.d.ts.map +1 -1
  74. package/dist/esm/types/components/Piano/WhiteKey.d.ts.map +1 -1
  75. package/dist/esm/types/components/Piano/index.d.ts +7 -4
  76. package/dist/esm/types/components/Piano/index.d.ts.map +1 -1
  77. package/dist/esm/types/components/Piano/key.d.ts +4 -0
  78. package/dist/esm/types/components/Piano/key.d.ts.map +1 -1
  79. package/dist/esm/types/components/Piano/keyboardShortcuts.d.ts +2 -2
  80. package/dist/esm/types/components/Slider/Scale.d.ts +1 -3
  81. package/dist/esm/types/components/Slider/Scale.d.ts.map +1 -1
  82. package/dist/esm/types/components/Slider/Thumb.d.ts +1 -0
  83. package/dist/esm/types/components/Slider/Thumb.d.ts.map +1 -1
  84. package/dist/esm/types/components/Slider/Track.d.ts +2 -4
  85. package/dist/esm/types/components/Slider/Track.d.ts.map +1 -1
  86. package/dist/esm/types/components/Slider/context.d.ts +2 -10
  87. package/dist/esm/types/components/Slider/context.d.ts.map +1 -1
  88. package/dist/esm/types/components/Slider/index.d.ts +5 -1
  89. package/dist/esm/types/components/Slider/index.d.ts.map +1 -1
  90. package/dist/esm/types/components/WheelObserver/index.d.ts +3 -3
  91. package/dist/esm/types/components/WheelObserver/index.d.ts.map +1 -1
  92. package/dist/esm/types/components/XYPad/Area.d.ts +3 -3
  93. package/dist/esm/types/components/XYPad/Area.d.ts.map +1 -1
  94. package/dist/esm/types/components/XYPad/Thumb.d.ts +1 -0
  95. package/dist/esm/types/components/XYPad/Thumb.d.ts.map +1 -1
  96. package/dist/esm/types/components/XYPad/index.d.ts +6 -1
  97. package/dist/esm/types/components/XYPad/index.d.ts.map +1 -1
  98. package/dist/esm/types/hooks/useAnimationFrame.d.ts.map +1 -1
  99. package/dist/esm/types/hooks/useInterval.d.ts +2 -0
  100. package/dist/esm/types/hooks/useInterval.d.ts.map +1 -0
  101. package/dist/esm/types/hooks/useLongPress.d.ts +2 -0
  102. package/dist/esm/types/hooks/useLongPress.d.ts.map +1 -0
  103. package/dist/esm/types/index.d.ts +5 -0
  104. package/dist/esm/types/index.d.ts.map +1 -1
  105. package/dist/styles/index.css +115 -33
  106. package/package.json +41 -40
  107. package/src/components/AnimationCanvas/index.tsx +15 -7
  108. package/src/components/AnimationKnob/index.tsx +216 -187
  109. package/src/components/Knob/index.tsx +230 -171
  110. package/src/components/NumberInput/DecrementStepper.tsx +9 -2
  111. package/src/components/NumberInput/IncrementStepper.tsx +9 -2
  112. package/src/components/NumberInput/InternalInput.tsx +219 -0
  113. package/src/components/NumberInput/Stepper.tsx +2 -0
  114. package/src/components/NumberInput/context.tsx +21 -9
  115. package/src/components/NumberInput/index.css +76 -22
  116. package/src/components/NumberInput/index.tsx +94 -162
  117. package/src/components/Piano/BlackKey.tsx +14 -0
  118. package/src/components/Piano/WhiteKey.tsx +14 -0
  119. package/src/components/Piano/index.css +22 -6
  120. package/src/components/Piano/index.tsx +20 -22
  121. package/src/components/Piano/key.ts +5 -0
  122. package/src/components/Piano/keyboardShortcuts.ts +2 -2
  123. package/src/components/Slider/Scale.tsx +1 -4
  124. package/src/components/Slider/Thumb.tsx +6 -0
  125. package/src/components/Slider/Track.tsx +5 -4
  126. package/src/components/Slider/context.tsx +5 -20
  127. package/src/components/Slider/index.css +5 -2
  128. package/src/components/Slider/index.tsx +52 -26
  129. package/src/components/WheelObserver/index.tsx +4 -2
  130. package/src/components/XYPad/Area.tsx +7 -6
  131. package/src/components/XYPad/Thumb.tsx +6 -0
  132. package/src/components/XYPad/index.css +12 -2
  133. package/src/components/XYPad/index.tsx +57 -41
  134. package/src/hooks/useInterval.ts +27 -0
  135. package/src/hooks/useLongPress.ts +31 -0
  136. package/src/index.ts +6 -0
  137. package/src/styles/index.css +0 -1
@@ -1,5 +1,11 @@
1
1
  import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, useCallback, useRef } from 'react'
2
+ import {
3
+ ComponentPropsWithoutRef,
4
+ forwardRef,
5
+ useCallback,
6
+ useImperativeHandle,
7
+ useRef,
8
+ } from 'react'
3
9
 
4
10
  import {
5
11
  clamp,
@@ -70,189 +76,242 @@ export interface KnobProps {
70
76
  onChange?: (value: number) => void
71
77
  }
72
78
 
73
- /** @category Knob */
74
- export function Knob({
75
- value,
76
- min,
77
- max,
78
- step = 1,
79
- skew = 1,
80
- defaultValue = min,
81
- startValue = min,
82
- size = 50,
83
- bodyNoSelect = true,
84
- wheel = ['raw', 1],
85
- keyboard = ['raw', 1],
86
- enableDoubleClickDefault = true,
87
- disabled = false,
88
- readonly = false,
89
- activeLine,
90
- inactiveLine,
91
- thumb,
92
- thumbLine,
93
- onChange,
94
- className,
95
- classes,
96
- ...props
97
- }: KnobProps & Omit<ComponentPropsWithoutRef<'svg'>, keyof KnobProps>) {
98
- const valueRef = useRef(0)
79
+ /**
80
+ * @category Knob
81
+ */
82
+ export interface KnobMethods {
83
+ focus: () => void
84
+ blur: () => void
85
+ }
99
86
 
100
- // --- interpret props ---
101
- const padding = 8 // %
102
- const thumbLineWeight = 6 // %
103
- const thumbLength = 35 // %
104
- const lineWeight = 3 // px
105
- const p = normalizeValue(value, min, max, skew)
106
- const s = normalizeValue(startValue, min, max, skew)
87
+ type Props = KnobProps & Omit<ComponentPropsWithoutRef<'svg'>, keyof KnobProps>
107
88
 
108
- // --- internal functions ---
109
- const onDrag = useCallback(
110
- (_x: number, y: number) => {
111
- if (!onChange || readonly) return
112
- const v = rawValue(valueRef.current - y / 100, min, max, skew)
113
- const v2 = clamp(stepValue(v, step), min, max)
114
- onChange(v2)
115
- },
116
- [max, min, onChange, readonly, skew, step],
117
- )
89
+ /**
90
+ * Interactive rotary knob component implemented in SVG.
91
+ *
92
+ * @category Knob
93
+ */
94
+ export const Knob = forwardRef<KnobMethods, Props>(
95
+ (
96
+ {
97
+ value,
98
+ min,
99
+ max,
100
+ step = 1,
101
+ skew = 1,
102
+ defaultValue = min,
103
+ startValue = min,
104
+ size = 50,
105
+ bodyNoSelect = true,
106
+ wheel = ['raw', 1],
107
+ keyboard = ['raw', 1],
108
+ enableDoubleClickDefault = true,
109
+ disabled = false,
110
+ readonly = false,
111
+ activeLine,
112
+ inactiveLine,
113
+ thumb,
114
+ thumbLine,
115
+ onChange,
116
+ onKeyDown,
117
+ onPointerDown,
118
+ onDoubleClick,
119
+ className,
120
+ classes,
121
+ ...props
122
+ }: Props,
123
+ ref,
124
+ ) => {
125
+ const valueRef = useRef(0)
126
+ const elmRef = useRef<HTMLOrSVGElement>(null)
118
127
 
119
- const handleKeyDown = useCallback(
120
- (event: React.KeyboardEvent<HTMLOrSVGElement>) => {
121
- if (!keyboard || !onChange || readonly) return
122
- const key = event.key
123
- if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
124
- event.preventDefault()
125
- const x =
126
- key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
127
- let v
128
- if (keyboard[0] == 'normalized') {
128
+ // --- interpret props ---
129
+ const padding = 8 // %
130
+ const thumbLineWeight = 6 // %
131
+ const thumbLineLength = 35 // %
132
+ const lineWeight = size * 0.06 // px
133
+ const p = normalizeValue(value, min, max, skew)
134
+ const s = normalizeValue(startValue, min, max, skew)
135
+
136
+ // --- internal functions ---
137
+ const onDrag = useCallback(
138
+ (_x: number, y: number) => {
139
+ if (!onChange || readonly) return
140
+ const v = rawValue(valueRef.current - y / 100, min, max, skew)
141
+ const v2 = clamp(stepValue(v, step), min, max)
142
+ onChange(v2)
143
+ },
144
+ [max, min, onChange, readonly, skew, step],
145
+ )
146
+
147
+ const updateValueByEvent = useCallback(
148
+ (eventType: InputEventOption[0], x: number) => {
149
+ let newValue
150
+ if (eventType == 'normalized') {
129
151
  const n = normalizeValue(value, min, max, skew)
130
- v = rawValue(n + x, min, max, skew)
152
+ newValue = rawValue(n + x, min, max, skew)
131
153
  } else {
132
- v = value + x
154
+ newValue = value + x
133
155
  }
134
- onChange(clamp(stepValue(v, step), min, max))
135
- }
136
- },
137
- [value, min, max, step, skew, keyboard, readonly, onChange],
138
- )
156
+ return clamp(stepValue(newValue, step), min, max)
157
+ },
158
+ [max, min, skew, step, value],
159
+ )
139
160
 
140
- // --- hooks ---
141
- const [touchMoveRefCallback, pointerDownHandler] = useDrag<SVGElement>({
142
- onDrag: onDrag,
143
- onDragStart: () => {
144
- valueRef.current = normalizeValue(value, min, max, skew)
145
- if (bodyNoSelect) addNoSelect()
146
- },
147
- onDragEnd: () => {
148
- if (bodyNoSelect) removeNoSelect()
149
- },
150
- })
161
+ const handleKeyDown = useCallback(
162
+ (event: React.KeyboardEvent<HTMLOrSVGElement>) => {
163
+ if (!keyboard || !onChange || readonly) return
164
+ const key = event.key
165
+ if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
166
+ event.preventDefault()
167
+ const x =
168
+ key == 'ArrowRight' || key == 'ArrowUp' ? keyboard[1] : -keyboard[1]
169
+ onChange(updateValueByEvent(keyboard[0], x))
170
+ }
171
+ },
172
+ [keyboard, onChange, readonly, updateValueByEvent],
173
+ )
151
174
 
152
- const wheelRefCallback = useRefCallbackEvent(
153
- 'wheel',
154
- (event) => {
155
- if (!wheel) return
156
- event.preventDefault()
157
- const x = event.deltaY > 0 ? -wheel[1] : wheel[1]
158
- let v
159
- if (wheel[0] == 'normalized') {
160
- const n = normalizeValue(value, min, max, skew)
161
- v = rawValue(n + x, min, max, skew)
162
- } else {
163
- v = value + x
175
+ // --- hooks ---
176
+ const [touchMoveRefCallback, pointerDownHandler] = useDrag<SVGElement>({
177
+ onDrag: onDrag,
178
+ onDragStart: () => {
179
+ valueRef.current = normalizeValue(value, min, max, skew)
180
+ if (bodyNoSelect) addNoSelect()
181
+ },
182
+ onDragEnd: () => {
183
+ if (bodyNoSelect) removeNoSelect()
184
+ },
185
+ })
186
+
187
+ const wheelRefCallback = useRefCallbackEvent(
188
+ 'wheel',
189
+ (event) => {
190
+ if (!wheel || readonly) return
191
+ event.preventDefault()
192
+ if (!onChange || event.deltaY == 0) return
193
+ const x = Math.sign(event.deltaY) * -wheel[1]
194
+ onChange(updateValueByEvent(wheel[0], x))
195
+ },
196
+ { passive: false },
197
+ [wheel, onChange, readonly, updateValueByEvent],
198
+ )
199
+
200
+ useImperativeHandle(ref, () => {
201
+ return {
202
+ focus() {
203
+ elmRef.current?.focus()
204
+ },
205
+ blur() {
206
+ elmRef.current?.blur()
207
+ },
164
208
  }
165
- if (onChange) onChange(clamp(stepValue(v, step), min, max))
166
- },
167
- { passive: false },
168
- [wheel, value, min, max, skew, step, onChange],
169
- )
209
+ }, [])
170
210
 
171
- const center = size / 2
172
- const r1 = -135
173
- const r2 = -135 + Math.min(p, s) * 270
174
- const r3 = -135 + Math.max(p, s) * 270
175
- const r4 = 135
176
- const x1 = center + center * Math.cos(radian(r1 - 90))
177
- const y1 = center + center * Math.sin(radian(r1 - 90))
178
- const x2 = center + center * Math.cos(radian(r2 - 90))
179
- const y2 = center + center * Math.sin(radian(r2 - 90))
180
- const x3 = center + center * Math.cos(radian(r3 - 90))
181
- const y3 = center + center * Math.sin(radian(r3 - 90))
182
- const x4 = center + center * Math.cos(radian(r4 - 90))
183
- const y4 = center + center * Math.sin(radian(r4 - 90))
211
+ const center = size / 2
212
+ const r1 = -135
213
+ const r2 = -135 + Math.min(p, s) * 270
214
+ const r3 = -135 + Math.max(p, s) * 270
215
+ const r4 = 135
216
+ const x1 = center + center * Math.cos(radian(r1 - 90))
217
+ const y1 = center + center * Math.sin(radian(r1 - 90))
218
+ const x2 = center + center * Math.cos(radian(r2 - 90))
219
+ const y2 = center + center * Math.sin(radian(r2 - 90))
220
+ const x3 = center + center * Math.cos(radian(r3 - 90))
221
+ const y3 = center + center * Math.sin(radian(r3 - 90))
222
+ const x4 = center + center * Math.cos(radian(r4 - 90))
223
+ const y4 = center + center * Math.sin(radian(r4 - 90))
184
224
 
185
- return (
186
- <svg
187
- className={clsx('tremolo-knob', className)}
188
- ref={(div) => {
189
- wheelRefCallback(div)
190
- touchMoveRefCallback(div)
191
- }}
192
- width={size}
193
- height={size}
194
- tabIndex={0}
195
- aria-valuenow={value}
196
- aria-valuemin={min}
197
- aria-valuemax={max}
198
- aria-disabled={disabled}
199
- aria-readonly={readonly}
200
- onPointerDown={pointerDownHandler}
201
- onDoubleClick={() => {
202
- if (enableDoubleClickDefault && onChange) {
203
- onChange(defaultValue)
204
- }
205
- }}
206
- onKeyDown={handleKeyDown}
207
- onContextMenu={(e) => e.preventDefault()}
208
- {...props}
209
- >
210
- {/* rotary meter */}
211
- {startValue > min && (
225
+ return (
226
+ <svg
227
+ className={clsx('tremolo-knob', className)}
228
+ ref={(div) => {
229
+ elmRef.current = div
230
+ wheelRefCallback(div)
231
+ touchMoveRefCallback(div)
232
+ }}
233
+ width={size}
234
+ height={size}
235
+ tabIndex={0}
236
+ aria-valuenow={value}
237
+ aria-valuemin={min}
238
+ aria-valuemax={max}
239
+ aria-disabled={disabled}
240
+ aria-readonly={readonly}
241
+ onPointerDown={(event) => {
242
+ pointerDownHandler(event)
243
+ onPointerDown?.(event)
244
+ }}
245
+ onDoubleClick={(event) => {
246
+ if (enableDoubleClickDefault && onChange) {
247
+ onChange(defaultValue)
248
+ }
249
+ onDoubleClick?.(event)
250
+ }}
251
+ onKeyDown={(event) => {
252
+ handleKeyDown(event)
253
+ onKeyDown?.(event)
254
+ }}
255
+ {...props}
256
+ >
257
+ {/* rotary meter */}
258
+ {startValue > min && (
259
+ <path
260
+ className={clsx(
261
+ 'tremolo-knob-inactive-line',
262
+ classes?.inactiveLine,
263
+ )}
264
+ d={`M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2}, ${y2}`}
265
+ fill="none"
266
+ stroke={inactiveLine || 'currentColor'}
267
+ strokeWidth={lineWeight}
268
+ />
269
+ )}
212
270
  <path
213
- className={clsx('tremolo-knob-inactive-line', classes?.inactiveLine)}
214
- d={`M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2}, ${y2}`}
271
+ className={clsx('tremolo-knob-active-line', classes?.activeLine)}
272
+ d={`M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`}
215
273
  fill="none"
216
- stroke={inactiveLine || 'currentColor'}
274
+ stroke={activeLine || 'currentColor'}
217
275
  strokeWidth={lineWeight}
218
276
  />
219
- )}
220
- <path
221
- className={clsx('tremolo-knob-active-line', classes?.activeLine)}
222
- d={`M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3}, ${y3}`}
223
- fill="none"
224
- stroke={activeLine || 'currentColor'}
225
- strokeWidth={lineWeight}
226
- />
227
- {startValue < max && (
228
- <path
229
- className={clsx('tremolo-knob-inactive-line', classes?.inactiveLine)}
230
- d={`M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`}
231
- fill="none"
232
- stroke={inactiveLine || 'currentColor'}
233
- strokeWidth={lineWeight}
234
- />
235
- )}
236
- {/* thumb */}
237
- <svg className={clsx('tremolo-knob-thumb', classes?.thumb)}>
238
- <circle
239
- cx="50%"
240
- cy="50%"
241
- r={`${50 - padding}%`}
242
- fill={thumb || 'currentColor'}
243
- />
244
- <line
245
- className={clsx('tremolo-knob-thumb-line', classes?.thumbLine)}
246
- x1="50%"
247
- y1={`${padding}%`}
248
- x2="50%"
249
- y2={`${thumbLength}%`}
250
- stroke={thumbLine || 'currentColor'}
251
- strokeWidth={`${thumbLineWeight}%`}
252
- transform={`rotate(${-135 + p * 270})`}
253
- transform-origin="50% 50%"
254
- />
277
+ {startValue < max && (
278
+ <path
279
+ className={clsx(
280
+ 'tremolo-knob-inactive-line',
281
+ classes?.inactiveLine,
282
+ )}
283
+ d={`M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4}, ${y4}`}
284
+ fill="none"
285
+ stroke={inactiveLine || 'currentColor'}
286
+ strokeWidth={lineWeight}
287
+ />
288
+ )}
289
+ {/* thumb */}
290
+ <svg className={clsx('tremolo-knob-thumb', classes?.thumb)}>
291
+ <circle
292
+ cx="50%"
293
+ cy="50%"
294
+ r={`${50 - padding}%`}
295
+ fill={thumb || 'currentColor'}
296
+ />
297
+ <line
298
+ className={clsx('tremolo-knob-thumb-line', classes?.thumbLine)}
299
+ x1="50%"
300
+ y1={`${padding}%`}
301
+ x2="50%"
302
+ y2={`${thumbLineLength}%`}
303
+ stroke={thumbLine || 'currentColor'}
304
+ strokeWidth={`${thumbLineWeight}%`}
305
+ // NOTE
306
+ // https://bugs.webkit.org/show_bug.cgi?id=201854
307
+ // https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
308
+ style={{
309
+ transform: `rotate(${-135 + p * 270}deg)`,
310
+ transformOrigin: '50% 50%',
311
+ }}
312
+ />
313
+ </svg>
255
314
  </svg>
256
- </svg>
257
- )
258
- }
315
+ )
316
+ },
317
+ )
@@ -1,13 +1,17 @@
1
1
  import clsx from 'clsx'
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
3
 
4
+ import { useLongPress } from '../../hooks/useLongPress'
5
+
4
6
  import { useNumberInputContext } from './context'
5
7
 
8
+ /** @category NumberInput */
6
9
  export interface DecrementStepperProps {
7
10
  size?: number
8
11
  children?: ReactNode
9
12
  }
10
13
 
14
+ /** @category NumberInput */
11
15
  export function DecrementStepper({
12
16
  size = 12,
13
17
  children,
@@ -17,12 +21,14 @@ export function DecrementStepper({
17
21
  Omit<ComponentPropsWithoutRef<'div'>, keyof DecrementStepperProps>) {
18
22
  const min = useNumberInputContext((s) => s.min)
19
23
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
24
+ const readonly = useNumberInputContext((s) => s.readonly)
20
25
  const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange)
21
26
  const decrement = useNumberInputContext((s) => s.decrement)
22
27
 
23
- // TODO: add long press event
28
+ const press = useLongPress(decrement)
24
29
 
25
30
  return (
31
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
26
32
  <div
27
33
  className={clsx('tremolo-number-input-decrement-stepper', className)}
28
34
  role="button"
@@ -30,7 +36,8 @@ export function DecrementStepper({
30
36
  aria-disabled={
31
37
  keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER)
32
38
  }
33
- onPointerDown={decrement}
39
+ aria-readonly={readonly}
40
+ onPointerDown={press}
34
41
  {...props}
35
42
  >
36
43
  {children || (
@@ -1,13 +1,17 @@
1
1
  import clsx from 'clsx'
2
2
  import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
3
 
4
+ import { useLongPress } from '../../hooks/useLongPress'
5
+
4
6
  import { useNumberInputContext } from './context'
5
7
 
8
+ /** @category NumberInput */
6
9
  export interface IncrementStepperProps {
7
10
  size?: number
8
11
  children?: ReactNode
9
12
  }
10
13
 
14
+ /** @category NumberInput */
11
15
  export function IncrementStepper({
12
16
  size = 12,
13
17
  children,
@@ -17,12 +21,14 @@ export function IncrementStepper({
17
21
  Omit<ComponentPropsWithoutRef<'div'>, keyof IncrementStepperProps>) {
18
22
  const max = useNumberInputContext((s) => s.max)
19
23
  const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
24
+ const readonly = useNumberInputContext((s) => s.readonly)
20
25
  const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange)
21
26
  const increment = useNumberInputContext((s) => s.increment)
22
27
 
23
- // TODO: add long press event
28
+ const press = useLongPress(increment)
24
29
 
25
30
  return (
31
+ // eslint-disable-next-line jsx-a11y/role-supports-aria-props
26
32
  <div
27
33
  className={clsx('tremolo-number-input-increment-stepper', className)}
28
34
  role="button"
@@ -30,7 +36,8 @@ export function IncrementStepper({
30
36
  aria-disabled={
31
37
  keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER)
32
38
  }
33
- onPointerDown={increment}
39
+ aria-readonly={readonly}
40
+ onPointerDown={press}
34
41
  {...props}
35
42
  >
36
43
  {children || (