@tremolo-ui/react 0.5.0 → 0.7.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 (86) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +1669 -541
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1571 -486
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1571 -486
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1640 -538
  16. package/dist/index.js.map +1 -1
  17. package/package.json +29 -27
  18. package/src/components/AnimationCanvas/index.tsx +115 -76
  19. package/src/components/DropZone/index.tsx +100 -0
  20. package/src/components/FileInput/Trigger.tsx +39 -0
  21. package/src/components/FileInput/context.tsx +24 -0
  22. package/src/components/FileInput/index.tsx +144 -0
  23. package/src/components/Knob/ActiveLine.tsx +7 -6
  24. package/src/components/Knob/InactiveLine.tsx +9 -10
  25. package/src/components/Knob/SVGRoot.tsx +5 -3
  26. package/src/components/Knob/Thumb.tsx +33 -11
  27. package/src/components/Knob/context.tsx +17 -1
  28. package/src/components/Knob/index.tsx +156 -60
  29. package/src/components/NumberInput/DecrementStepper.tsx +3 -10
  30. package/src/components/NumberInput/IncrementStepper.tsx +3 -10
  31. package/src/components/NumberInput/InputField.tsx +185 -108
  32. package/src/components/NumberInput/Stepper.tsx +120 -84
  33. package/src/components/NumberInput/StepperButton.tsx +100 -0
  34. package/src/components/NumberInput/context.tsx +34 -8
  35. package/src/components/NumberInput/index.tsx +270 -82
  36. package/src/components/Piano/index.tsx +363 -200
  37. package/src/components/PointsEditor/Background.tsx +13 -10
  38. package/src/components/PointsEditor/Container.tsx +83 -29
  39. package/src/components/PointsEditor/Point.tsx +241 -79
  40. package/src/components/PointsEditor/SelectionBox.tsx +48 -0
  41. package/src/components/PointsEditor/context.tsx +78 -10
  42. package/src/components/PointsEditor/index.tsx +440 -41
  43. package/src/components/Slider/Marks.tsx +48 -19
  44. package/src/components/Slider/MarksOption.tsx +64 -45
  45. package/src/components/Slider/Thumb.tsx +107 -55
  46. package/src/components/Slider/Track.tsx +66 -62
  47. package/src/components/Slider/context.tsx +4 -2
  48. package/src/components/Slider/index.tsx +136 -77
  49. package/src/components/Slider/type.ts +29 -9
  50. package/src/components/XYPad/Area.tsx +46 -41
  51. package/src/components/XYPad/Thumb.tsx +131 -64
  52. package/src/components/XYPad/context.tsx +4 -1
  53. package/src/components/XYPad/index.tsx +183 -53
  54. package/src/components/_util/Placement.tsx +68 -0
  55. package/src/components/_util/VisuallyHiddenRangeInput.tsx +12 -0
  56. package/src/components/_util/css-length.ts +13 -0
  57. package/src/components/_util/decimal-digits.ts +23 -0
  58. package/src/components/_util/visually-hidden.ts +25 -0
  59. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  60. package/src/css-variables.ts +23 -0
  61. package/src/hooks/_internal/useCallbackRef.ts +3 -1
  62. package/src/hooks/_internal/useCheckSteps.ts +169 -0
  63. package/src/hooks/useAnimationFrame.ts +17 -7
  64. package/src/hooks/useDrag.ts +51 -12
  65. package/src/hooks/useDragValue.ts +71 -11
  66. package/src/hooks/useDropZone.ts +78 -0
  67. package/src/hooks/useEventListener.ts +10 -6
  68. package/src/hooks/useLongPress.ts +42 -8
  69. package/src/hooks/useMIDIAccess.ts +82 -11
  70. package/src/hooks/useMIDIInput.ts +45 -13
  71. package/src/hooks/useMIDIMessage.ts +23 -5
  72. package/src/hooks/useWheel.ts +4 -1
  73. package/src/index.ts +45 -25
  74. package/src/input-event.ts +32 -0
  75. package/dist/index.css +0 -501
  76. package/src/Globals.d.ts +0 -3
  77. package/src/components/Knob/index.css +0 -81
  78. package/src/components/NumberInput/index.css +0 -121
  79. package/src/components/NumberInput/stepperButton.tsx +0 -90
  80. package/src/components/Piano/index.css +0 -90
  81. package/src/components/PointsEditor/index.css +0 -35
  82. package/src/components/Slider/index.css +0 -125
  83. package/src/components/XYPad/index.css +0 -60
  84. package/src/components/_util/index.ts +0 -28
  85. package/src/styles/global.css +0 -19
  86. /package/src/components/Piano/{keyboardShortcuts.ts → keyboard-shortcuts.ts} +0 -0
@@ -1,7 +1,8 @@
1
- import clsx from 'clsx'
2
1
  import { SVGProps } from 'react'
3
2
 
4
- import { arcRadius, pointOnArc, useKnobContext } from './context'
3
+ import { useCheckPlacement } from '../_util/Placement'
4
+
5
+ import { arcPath, arcRadius, useKnobContext } from './context'
5
6
 
6
7
  export function ActiveLine({
7
8
  stroke = 'currentColor',
@@ -9,17 +10,17 @@ export function ActiveLine({
9
10
  className,
10
11
  ...props
11
12
  }: Omit<SVGProps<SVGPathElement>, 'd'>) {
13
+ useCheckPlacement('Knob.ActiveLine', 'Knob.SVGRoot')
14
+
12
15
  const r2 = useKnobContext((s) => s.r2)
13
16
  const r3 = useKnobContext((s) => s.r3)
14
17
 
15
18
  const radius = arcRadius(strokeWidth)
16
- const start = pointOnArc(r2, radius)
17
- const end = pointOnArc(r3, radius)
18
19
 
19
20
  return (
20
21
  <path
21
- className={clsx('tremolo-knob-active-line', className)}
22
- d={`M ${start.x} ${start.y} A ${radius} ${radius} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${end.x} ${end.y}`}
22
+ className={className}
23
+ d={arcPath(r2, r3, radius)}
23
24
  fill="none"
24
25
  stroke={stroke}
25
26
  strokeWidth={strokeWidth}
@@ -1,7 +1,8 @@
1
- import clsx from 'clsx'
2
1
  import { SVGProps } from 'react'
3
2
 
4
- import { arcRadius, pointOnArc, useKnobContext } from './context'
3
+ import { useCheckPlacement } from '../_util/Placement'
4
+
5
+ import { arcPath, arcRadius, useKnobContext } from './context'
5
6
 
6
7
  export function InactiveLine({
7
8
  stroke = 'currentColor',
@@ -9,6 +10,8 @@ export function InactiveLine({
9
10
  className,
10
11
  ...props
11
12
  }: Omit<SVGProps<SVGPathElement>, 'd'>) {
13
+ useCheckPlacement('Knob.InactiveLine', 'Knob.SVGRoot')
14
+
12
15
  const min = useKnobContext((s) => s.min)
13
16
  const max = useKnobContext((s) => s.max)
14
17
  const startValue = useKnobContext((s) => s.startValue)
@@ -19,17 +22,13 @@ export function InactiveLine({
19
22
  const r4 = useKnobContext((s) => s.r4)
20
23
 
21
24
  const radius = arcRadius(strokeWidth)
22
- const p1 = pointOnArc(r1, radius)
23
- const p2 = pointOnArc(r2, radius)
24
- const p3 = pointOnArc(r3, radius)
25
- const p4 = pointOnArc(r4, radius)
26
25
 
27
26
  return (
28
27
  <>
29
28
  {startValue > min && (
30
29
  <path
31
- className={clsx('tremolo-knob-inactive-line', className)}
32
- d={`M ${p1.x} ${p1.y} A ${radius} ${radius} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${p2.x} ${p2.y}`}
30
+ className={className}
31
+ d={arcPath(r1, r2, radius)}
33
32
  fill="none"
34
33
  stroke={stroke}
35
34
  strokeWidth={strokeWidth}
@@ -38,8 +37,8 @@ export function InactiveLine({
38
37
  )}
39
38
  {startValue < max && (
40
39
  <path
41
- className={clsx('tremolo-knob-inactive-line', className)}
42
- d={`M ${p3.x} ${p3.y} A ${radius} ${radius} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${p4.x} ${p4.y}`}
40
+ className={className}
41
+ d={arcPath(r3, r4, radius)}
43
42
  fill="none"
44
43
  stroke={stroke}
45
44
  strokeWidth={strokeWidth}
@@ -1,8 +1,10 @@
1
1
  import { ReactNode, SVGProps } from 'react'
2
2
 
3
+ import { Placement } from '../_util/Placement'
4
+
3
5
  import { viewBoxSize } from './context'
4
6
 
5
- export interface SVGRootProps {
7
+ export interface KnobSVGRootProps {
6
8
  /**
7
9
  * `<Knob.InactiveLine />`, `<Knob.ActiveLine />` and `<Knob.Thumb />` go
8
10
  * here, in the order you want them painted.
@@ -14,7 +16,7 @@ export function SVGRoot({
14
16
  children,
15
17
  style,
16
18
  ...props
17
- }: SVGRootProps & Omit<SVGProps<SVGSVGElement>, keyof SVGRootProps>) {
19
+ }: KnobSVGRootProps & Omit<SVGProps<SVGSVGElement>, keyof KnobSVGRootProps>) {
18
20
  return (
19
21
  <svg
20
22
  viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
@@ -24,7 +26,7 @@ export function SVGRoot({
24
26
  }}
25
27
  {...props}
26
28
  >
27
- {children}
29
+ <Placement name="Knob.SVGRoot">{children}</Placement>
28
30
  </svg>
29
31
  )
30
32
  }
@@ -1,24 +1,44 @@
1
- import clsx from 'clsx'
2
1
  import { SVGProps } from 'react'
3
2
 
4
3
  import { clamp } from '@tremolo-ui/functions'
5
4
 
5
+ import { useCheckPlacement } from '../_util/Placement'
6
+
6
7
  import { useKnobContext, viewBoxSize } from './context'
7
8
 
8
- interface Props {
9
- /** color */
9
+ export interface KnobThumbProps {
10
+ /**
11
+ * Fill colour of the circle.
12
+ * @default 'currentColor'
13
+ */
10
14
  thumb?: string
11
- /** color */
15
+ /**
16
+ * Colour of the line that points at the value.
17
+ * @default 'currentColor'
18
+ */
12
19
  thumbLine?: string
13
- /** percent (0-100) */
20
+ /**
21
+ * Diameter of the circle, as a percentage of the knob.
22
+ * @default 84
23
+ */
14
24
  thumbSize?: number
15
- /** percent (0-100) */
25
+ /**
26
+ * Thickness of the line, as a percentage of the knob.
27
+ * @default 6
28
+ */
16
29
  thumbLineWeight?: number
17
- /** percent (0-100) */
30
+ /**
31
+ * How far down the line reaches, as a percentage of the knob from its top.
32
+ * The line starts at the edge of the circle.
33
+ * @default 35
34
+ */
18
35
  thumbLineLength?: number
19
36
 
37
+ /**
38
+ * Classes for what the thumb draws inside itself: `thumbLine` is the line
39
+ * that points at the value.
40
+ */
20
41
  classes?: {
21
- thumb?: string
22
42
  thumbLine?: string
23
43
  }
24
44
  }
@@ -34,17 +54,19 @@ export function Thumb({
34
54
  classes,
35
55
 
36
56
  ...props
37
- }: Props & Omit<SVGProps<SVGSVGElement>, 'd' | keyof Props>) {
57
+ }: KnobThumbProps & Omit<SVGProps<SVGSVGElement>, 'd' | keyof KnobThumbProps>) {
58
+ useCheckPlacement('Knob.Thumb', 'Knob.SVGRoot')
59
+
38
60
  const angleRange = useKnobContext((s) => s.angleRange)
39
61
 
40
62
  const p = useKnobContext((s) => s.p)
41
63
  const r1 = useKnobContext((s) => s.r1)
42
64
 
43
65
  return (
44
- <svg className={clsx('tremolo-knob-thumb', classes?.thumb)} {...props}>
66
+ <svg className={className} {...props}>
45
67
  <circle cx="50%" cy="50%" r={`${thumbSize / 2}%`} fill={thumb} />
46
68
  <line
47
- className={clsx('tremolo-knob-thumb-line', classes?.thumbLine)}
69
+ className={classes?.thumbLine}
48
70
  x1="50%"
49
71
  y1={`${(viewBoxSize - clamp(thumbSize, 0, 100)) / 2}%`}
50
72
  x2="50%"
@@ -51,6 +51,22 @@ export function arcRadius(strokeWidth: number | string | undefined) {
51
51
  return Number.isFinite(width) ? center - width / 2 : center
52
52
  }
53
53
 
54
+ /** Build an SVG path for an arc, splitting full turns into drawable segments. */
55
+ export function arcPath(startAngle: number, endAngle: number, radius: number) {
56
+ const start = pointOnArc(startAngle, radius)
57
+ const sweep = endAngle - startAngle
58
+ const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180))
59
+ const segmentSweep = sweep / segmentCount
60
+ let path = `M ${start.x} ${start.y}`
61
+
62
+ for (let i = 1; i <= segmentCount; i += 1) {
63
+ const end = pointOnArc(startAngle + segmentSweep * i, radius)
64
+ path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`
65
+ }
66
+
67
+ return path
68
+ }
69
+
54
70
  /** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
55
71
  export function calcAngles({
56
72
  value,
@@ -71,7 +87,7 @@ export function calcAngles({
71
87
  return { p, r1, r2, r3, r4 }
72
88
  }
73
89
 
74
- const KnobContext = createContext<KnobContextValue | null>(null)
90
+ const KnobContext = /* @__PURE__ */ createContext<KnobContextValue | null>(null)
75
91
 
76
92
  export const KnobProvider = KnobContext.Provider
77
93
 
@@ -1,6 +1,6 @@
1
- import clsx from 'clsx'
2
1
  import {
3
2
  ComponentPropsWithoutRef,
3
+ CSSProperties,
4
4
  forwardRef,
5
5
  ReactNode,
6
6
  useCallback,
@@ -9,19 +9,26 @@ import {
9
9
  useRef,
10
10
  } from 'react'
11
11
 
12
- import type { AxisOptions, XY } from '@tremolo-ui/dom'
13
12
  import {
14
13
  applyDelta,
15
- InputEventOption,
16
- linearScale,
17
- type Scale,
18
- type ValueRange,
19
- } from '@tremolo-ui/functions'
14
+ type AxisOptions,
15
+ type InputEventOption,
16
+ type ModifierValue,
17
+ selectModifier,
18
+ type XY,
19
+ } from '@tremolo-ui/dom'
20
+ import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
20
21
 
22
+ import { useComposedRefs } from '../../compose-refs'
23
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
21
24
  import { useDragValue } from '../../hooks/useDragValue'
22
25
  import { useWheel } from '../../hooks/useWheel'
23
- import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
24
- import { useComposedRefs } from '../_util/composeRefs'
26
+ import {
27
+ DEFAULT_DRAG_SENSITIVITY,
28
+ DEFAULT_KEYBOARD_OPTIONS,
29
+ DEFAULT_WHEEL_OPTIONS,
30
+ } from '../../input-event'
31
+ import { cssLength } from '../_util/css-length'
25
32
 
26
33
  import { ActiveLine } from './ActiveLine'
27
34
  import { calcAngles, KnobProvider } from './context'
@@ -29,18 +36,26 @@ import { InactiveLine } from './InactiveLine'
29
36
  import { SVGRoot } from './SVGRoot'
30
37
  import { Thumb } from './Thumb'
31
38
 
39
+ import type { CSSVariables } from '../../css-variables'
40
+
32
41
  const defaultExternalStyles: KnobProps['externalStyles'] = {
33
- userSelectNone: true,
34
42
  cursor: 'grabbing',
35
43
  }
36
44
 
37
45
  export interface KnobProps {
38
- // required
46
+ /** The current value. The knob shows only this, so update it from `onChange`. */
39
47
  value: number
48
+ /** The value with the knob turned all the way down. */
40
49
  min: number
50
+ /** The value with the knob turned all the way up. */
41
51
  max: number
42
52
 
43
- // optional
53
+ /**
54
+ * Granularity of the value. A drag, the wheel and the arrow keys snap it to
55
+ * multiples of `step`.
56
+ *
57
+ * @default 1
58
+ */
44
59
  step?: number
45
60
  /**
46
61
  * How the value is distributed across the travel.
@@ -53,52 +68,123 @@ export interface KnobProps {
53
68
  */
54
69
  scale?: Scale
55
70
  /**
56
- * value set when double-clicking
57
- * restriction: enableDoubleClickDefault = true
71
+ * The value a double click restores, while `enableDoubleClickDefault` is on.
58
72
  * @default min
59
- * @see enableDoubleClickDefault
60
73
  */
61
74
  defaultValue?: number
62
75
 
63
76
  /**
64
- * Value to be used as the starting point of the line when drawing.
77
+ * Where the active arc starts. Put it at the centre of a bipolar control,
78
+ * such as a pan knob, so that the arc grows from there either way.
65
79
  * @default min
66
80
  */
67
81
  startValue?: number
68
82
 
69
- /** width and height */
70
83
  /**
71
- * Width and height of the knob.
72
- * Defaults to the `--knob-size` CSS variable (50px).
84
+ * Width and height of the knob. Sets `--knob-size`; the size the theme
85
+ * gives it stands when this is omitted.
73
86
  */
74
87
  size?: number | string
75
88
 
76
89
  /**
77
- * Global style to apply when dragged
78
- * @default defaultExternalStyles
90
+ * The cursor to show while dragging. It is set on the dragged element, so it
91
+ * stays while the pointer is outside the knob.
92
+ *
93
+ * @default { cursor: 'grabbing' }
79
94
  */
80
95
  externalStyles?: {
81
- userSelectNone?: boolean
82
- cursor?: Cursor
96
+ cursor?: CSSProperties['cursor']
83
97
  }
84
98
  /**
85
- * wheel control option
86
- * If null, no event will be triggered
99
+ * How much one notch of the wheel moves the value. It only acts while the
100
+ * focus is inside, so that scrolling the page past the knob leaves it alone.
101
+ *
102
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
103
+ * range between `min` and `max`. The result is snapped to `step`, except for
104
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
105
+ * the wheel off.
106
+ *
107
+ * @default ['raw', 1]
108
+ */
109
+ wheel?: ModifierValue<InputEventOption> | null
110
+ /**
111
+ * How much a drag moves the value, per modifier key.
112
+ *
113
+ * `1` is the normal travel of 100px for the whole range; `0.1` makes the
114
+ * same movement cover a tenth of it. Shift is bound to `0.1` by default, to
115
+ * match what it does on the arrow keys.
116
+ *
117
+ * Pressing or releasing the key mid-drag does not disturb the value: the
118
+ * travel so far is kept and the new sensitivity applies from there. It takes
119
+ * effect on the next movement, since a key on its own produces no pointer
120
+ * event.
121
+ *
122
+ * @default { default: 1, shift: 0.1 }
123
+ */
124
+ dragSensitivity?: ModifierValue<number>
125
+
126
+ /**
127
+ * Hide the cursor while dragging and read the pointer movement directly,
128
+ * rather than letting it wander off across the screen.
129
+ *
130
+ * A knob does not care where the pointer is, only how far it moved, and
131
+ * letting it wander costs twice: the cursor ends up far from the knob it is
132
+ * holding, and **the drag stops at the edge of the screen**, where the
133
+ * operating system pins the pointer and the coordinates stop changing. A
134
+ * `dragSensitivity` below 1 reaches that edge quickly.
135
+ *
136
+ * Off by default because it is not free: the browser shows its own notice,
137
+ * Esc takes the lock back, and the request needs a user gesture and can be
138
+ * refused. A refused request is not an error — the drag simply carries on as
139
+ * an ordinary one.
140
+ *
141
+ * @default false
142
+ */
143
+ pointerLock?: boolean
144
+
145
+ /**
146
+ * How much one arrow key press moves the value.
147
+ *
148
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
149
+ * range between `min` and `max`. The result is snapped to `step`, except for
150
+ * an amount set on a modifier key, which is what lets shift move off the
151
+ * grid. `null` turns the arrow keys off.
152
+ *
153
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
154
+ * the amount to match: 1 would round straight back to where it started, and
155
+ * a development build warns about it.
156
+ *
157
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
87
158
  */
88
- wheel?: InputEventOption | null
159
+ keyboard?: ModifierValue<InputEventOption> | null
89
160
  /**
90
- * keyboard control option
91
- * If null, no event will be triggered
161
+ * Restore `defaultValue` on a double click.
162
+ * @default true
92
163
  */
93
- keyboard?: InputEventOption | null
94
164
  enableDoubleClickDefault?: boolean
95
165
 
166
+ /**
167
+ * Make the knob unchangeable and remove it from the tab order.
168
+ * The parts carry `data-disabled` while it is set.
169
+ */
96
170
  disabled?: boolean
171
+ /**
172
+ * Make the knob unchangeable while leaving it focusable.
173
+ * The parts carry `data-readonly` while it is set.
174
+ */
97
175
  readonly?: boolean
98
176
 
99
- /** angle range [degree] */
177
+ /**
178
+ * How far the knob turns from `min` to `max`, in degrees, centred on the
179
+ * top.
180
+ * @default 270
181
+ */
100
182
  angleRange?: number
101
183
 
184
+ /**
185
+ * Called with the new value when a drag, the wheel, an arrow key or a double
186
+ * click moves it.
187
+ */
102
188
  onChange?: (value: number) => void
103
189
 
104
190
  /**
@@ -115,6 +201,8 @@ export interface KnobProps {
115
201
  * </Knob.Root>
116
202
  */
117
203
  children: ReactNode
204
+
205
+ style?: CSSProperties & CSSVariables<'knob-size'>
118
206
  }
119
207
 
120
208
  export interface KnobMethods {
@@ -130,7 +218,7 @@ type Props = KnobProps & Omit<ComponentPropsWithoutRef<'div'>, keyof KnobProps>
130
218
  */
131
219
  const WHEEL_OPTIONS = { requireFocus: true }
132
220
 
133
- export const Root = forwardRef<KnobMethods, Props>(
221
+ export const Root = /* @__PURE__ */ forwardRef<KnobMethods, Props>(
134
222
  (
135
223
  {
136
224
  value,
@@ -142,8 +230,10 @@ export const Root = forwardRef<KnobMethods, Props>(
142
230
  startValue = min,
143
231
  size,
144
232
  externalStyles: _externalStyles,
145
- wheel = ['raw', 1],
146
- keyboard = ['raw', 1],
233
+ wheel = DEFAULT_WHEEL_OPTIONS,
234
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
235
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
236
+ pointerLock = false,
147
237
  enableDoubleClickDefault = true,
148
238
  disabled = false,
149
239
  readonly = false,
@@ -162,6 +252,7 @@ export const Root = forwardRef<KnobMethods, Props>(
162
252
  const elmRef = useRef<HTMLElement | SVGElement>(null)
163
253
 
164
254
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
255
+ const inactive = disabled || readonly
165
256
 
166
257
  // --- internal functions ---
167
258
  const range: ValueRange = useMemo(
@@ -169,17 +260,19 @@ export const Root = forwardRef<KnobMethods, Props>(
169
260
  [min, max, step, scale],
170
261
  )
171
262
 
263
+ useCheckSteps({ component: 'Knob', range, keyboard, wheel })
264
+
172
265
  const handleKeyDown = useCallback(
173
266
  (event: React.KeyboardEvent<HTMLOrSVGElement>) => {
174
- if (!keyboard || !onChange || readonly) return
267
+ if (!keyboard || !onChange || inactive) return
175
268
  const key = event.key
176
269
  if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
177
270
  event.preventDefault()
178
- const direction = key == 'ArrowRight' || key == 'ArrowUp' ? 1 : -1
179
- onChange(applyDelta(value, direction, keyboard, range))
271
+ const direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
272
+ onChange(applyDelta(value, direction, keyboard, range, event))
180
273
  }
181
274
  },
182
- [keyboard, onChange, readonly, value, range],
275
+ [keyboard, onChange, inactive, value, range],
183
276
  )
184
277
 
185
278
  // --- hooks ---
@@ -197,25 +290,23 @@ export const Root = forwardRef<KnobMethods, Props>(
197
290
  >({
198
291
  axis,
199
292
  getValue: () => [value, value],
293
+ sensitivity: (state) =>
294
+ selectModifier(dragSensitivity, state.event).value,
200
295
  threshold: 1,
201
- cursor: readonly ? undefined : externalStyles.cursor,
296
+ cursor: inactive ? undefined : externalStyles.cursor,
297
+ pointerLock: inactive ? false : pointerLock,
298
+ shouldStart: () => !inactive,
202
299
  onChange: (v) => {
203
- if (readonly) return
300
+ if (inactive) return
204
301
  onChange?.(v[1])
205
302
  },
206
- onDragStart: () => {
207
- if (externalStyles.userSelectNone) addUserSelectNone()
208
- },
209
- onDragEnd: () => {
210
- if (externalStyles.userSelectNone) removeUserSelectNone()
211
- },
212
303
  })
213
304
 
214
305
  const wheelRefCallback = useWheel<HTMLElement>((event) => {
215
- if (!wheel || readonly) return
306
+ if (!wheel || inactive) return
216
307
  event.preventDefault()
217
- if (!onChange || event.deltaY == 0) return
218
- onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range))
308
+ if (!onChange || event.deltaY === 0) return
309
+ onChange(applyDelta(value, -Math.sign(event.deltaY), wheel, range, event))
219
310
  }, WHEEL_OPTIONS)
220
311
 
221
312
  // Composed once, so React attaches the refs a single time instead of
@@ -234,35 +325,38 @@ export const Root = forwardRef<KnobMethods, Props>(
234
325
  useImperativeHandle(forwardedRef, () => {
235
326
  return {
236
327
  focus() {
237
- elmRef.current?.focus()
328
+ if (!disabled) elmRef.current?.focus()
238
329
  },
239
330
  blur() {
240
331
  elmRef.current?.blur()
241
332
  },
242
333
  }
243
- }, [])
334
+ }, [disabled])
244
335
 
245
336
  return (
246
337
  <KnobProvider value={context}>
247
338
  <div
248
339
  ref={rootRefCallback}
249
- className={clsx('tremolo-knob', className)}
250
- tabIndex={0}
340
+ className={className}
341
+ tabIndex={disabled ? -1 : 0}
251
342
  role="slider"
252
343
  aria-valuenow={value}
253
344
  aria-valuemin={min}
254
345
  aria-valuemax={max}
255
346
  aria-disabled={disabled}
256
347
  aria-readonly={readonly}
257
- data-dragging={dragging}
258
- style={{
259
- width: size,
260
- height: size,
261
- ...style,
262
- }}
348
+ data-disabled={disabled ? '' : undefined}
349
+ data-readonly={readonly ? '' : undefined}
350
+ data-dragging={dragging ? '' : undefined}
351
+ style={
352
+ {
353
+ '--knob-size': cssLength(size),
354
+ ...style,
355
+ } as CSSProperties
356
+ }
263
357
  onPointerDown={onPointerDown}
264
358
  onDoubleClick={(event) => {
265
- if (enableDoubleClickDefault && onChange) {
359
+ if (!inactive && enableDoubleClickDefault && onChange) {
266
360
  onChange(defaultValue)
267
361
  }
268
362
  onDoubleClick?.(event)
@@ -291,4 +385,6 @@ export const Knob = {
291
385
  Thumb,
292
386
  }
293
387
 
294
- export { useKnobContext } from './context'
388
+ export { useKnobContext, type KnobContextValue } from './context'
389
+ export { type KnobSVGRootProps } from './SVGRoot'
390
+ export { type KnobThumbProps } from './Thumb'
@@ -1,18 +1,11 @@
1
- import { ComponentPropsWithoutRef } from 'react'
2
-
3
1
  import {
4
2
  StepperArrow,
5
3
  StepperButton,
6
- StepperButtonProps,
7
- } from './stepperButton'
8
-
9
- export type DecrementStepperProps = StepperButtonProps
4
+ type StepperButtonProps,
5
+ } from './StepperButton'
10
6
 
11
7
  /** Lowers the value by one `step`, repeating while held. */
12
- export function DecrementStepper(
13
- props: DecrementStepperProps &
14
- Omit<ComponentPropsWithoutRef<'div'>, keyof DecrementStepperProps>,
15
- ) {
8
+ export function DecrementStepper(props: StepperButtonProps) {
16
9
  return (
17
10
  <StepperButton
18
11
  direction={-1}
@@ -1,18 +1,11 @@
1
- import { ComponentPropsWithoutRef } from 'react'
2
-
3
1
  import {
4
2
  StepperArrow,
5
3
  StepperButton,
6
- StepperButtonProps,
7
- } from './stepperButton'
8
-
9
- export type IncrementStepperProps = StepperButtonProps
4
+ type StepperButtonProps,
5
+ } from './StepperButton'
10
6
 
11
7
  /** Raises the value by one `step`, repeating while held. */
12
- export function IncrementStepper(
13
- props: IncrementStepperProps &
14
- Omit<ComponentPropsWithoutRef<'div'>, keyof IncrementStepperProps>,
15
- ) {
8
+ export function IncrementStepper(props: StepperButtonProps) {
16
9
  return (
17
10
  <StepperButton
18
11
  direction={1}