@tremolo-ui/react 0.4.0 → 0.6.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 (90) 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 +2272 -1650
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1461 -475
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1461 -475
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2258 -1648
  16. package/dist/index.js.map +1 -1
  17. package/package.json +32 -31
  18. package/src/components/AnimationCanvas/index.tsx +162 -194
  19. package/src/components/Knob/ActiveLine.tsx +9 -8
  20. package/src/components/Knob/InactiveLine.tsx +11 -14
  21. package/src/components/Knob/SVGRoot.tsx +10 -17
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +73 -89
  24. package/src/components/Knob/index.tsx +227 -105
  25. package/src/components/NumberInput/DecrementStepper.tsx +21 -50
  26. package/src/components/NumberInput/IncrementStepper.tsx +22 -50
  27. package/src/components/NumberInput/InputField.tsx +206 -0
  28. package/src/components/NumberInput/Stepper.tsx +150 -21
  29. package/src/components/NumberInput/StepperButton.tsx +95 -0
  30. package/src/components/NumberInput/context.tsx +104 -149
  31. package/src/components/NumberInput/index.tsx +461 -110
  32. package/src/components/Piano/index.tsx +413 -263
  33. package/src/components/Piano/keyboard-shortcuts.ts +61 -0
  34. package/src/components/PointsEditor/Background.tsx +19 -4
  35. package/src/components/PointsEditor/Container.tsx +92 -22
  36. package/src/components/PointsEditor/Point.tsx +293 -53
  37. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  38. package/src/components/PointsEditor/context.tsx +97 -75
  39. package/src/components/PointsEditor/index.tsx +497 -62
  40. package/src/components/Slider/Marks.tsx +84 -0
  41. package/src/components/Slider/MarksOption.tsx +115 -0
  42. package/src/components/Slider/Thumb.tsx +99 -54
  43. package/src/components/Slider/Track.tsx +73 -62
  44. package/src/components/Slider/context.tsx +40 -53
  45. package/src/components/Slider/index.tsx +237 -190
  46. package/src/components/Slider/type.ts +27 -9
  47. package/src/components/XYPad/Area.tsx +52 -33
  48. package/src/components/XYPad/Thumb.tsx +121 -60
  49. package/src/components/XYPad/context.tsx +49 -0
  50. package/src/components/XYPad/index.tsx +338 -246
  51. package/src/components/_util/Placement.tsx +68 -0
  52. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  53. package/src/components/_util/css-length.ts +13 -0
  54. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  55. package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
  56. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  57. package/src/hooks/useAnimationFrame.ts +17 -7
  58. package/src/hooks/useDrag.ts +39 -9
  59. package/src/hooks/useDragValue.ts +194 -0
  60. package/src/hooks/useEventListener.ts +11 -7
  61. package/src/hooks/useLongPress.ts +42 -8
  62. package/src/hooks/useMIDIAccess.ts +82 -11
  63. package/src/hooks/useMIDIInput.ts +45 -13
  64. package/src/hooks/useMIDIMessage.ts +23 -5
  65. package/src/hooks/useWheel.ts +29 -9
  66. package/src/index.ts +50 -27
  67. package/src/input-event.ts +32 -0
  68. package/dist/index.css +0 -536
  69. package/src/Globals.d.ts +0 -3
  70. package/src/components/AnimationCanvas/canvas.ts +0 -46
  71. package/src/components/Knob/index.css +0 -77
  72. package/src/components/NumberInput/InternalInput.tsx +0 -216
  73. package/src/components/NumberInput/index.css +0 -185
  74. package/src/components/NumberInput/type.ts +0 -58
  75. package/src/components/Piano/KeyLabel.tsx +0 -51
  76. package/src/components/Piano/context.tsx +0 -73
  77. package/src/components/Piano/index.css +0 -90
  78. package/src/components/Piano/key.tsx +0 -205
  79. package/src/components/Piano/keyboardShortcuts.ts +0 -31
  80. package/src/components/PointsEditor/index.css +0 -30
  81. package/src/components/Slider/Scale.tsx +0 -52
  82. package/src/components/Slider/ScaleOption.tsx +0 -103
  83. package/src/components/Slider/index.css +0 -112
  84. package/src/components/XYPad/index.css +0 -48
  85. package/src/components/_util/index.ts +0 -28
  86. package/src/components/_util/type.ts +0 -1
  87. package/src/hooks/useDragWithElement.ts +0 -100
  88. package/src/hooks/usePianoDrag.ts +0 -75
  89. package/src/hooks/useRefCallbackEvent.ts +0 -34
  90. package/src/styles/global.css +0 -19
package/package.json CHANGED
@@ -1,55 +1,54 @@
1
1
  {
2
2
  "name": "@tremolo-ui/react",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "React component library for audio app",
5
5
  "type": "module",
6
- "sideEffects": [
7
- "*.css"
8
- ],
6
+ "sideEffects": false,
9
7
  "main": "dist/index.cjs",
10
8
  "module": "dist/index.js",
11
9
  "types": "dist/index.d.cts",
12
10
  "scripts": {
13
- "sb": "storybook dev -p 6006",
11
+ "sb": "storybook dev",
14
12
  "typecheck": "tsc",
15
- "build:sb": "storybook build",
13
+ "build:sb": "storybook build -o storybook-static/i/storybook-react && cp .storybook/_headers storybook-static/_headers",
16
14
  "serve": "http-server ./storybook-static",
17
15
  "lint": "eslint --fix --ext .ts,.tsx .",
18
16
  "format": "prettier --write .",
19
- "test": "jest",
17
+ "test": "vitest run",
18
+ "test:watch": "vitest",
20
19
  "build:package": "tsc && tsdown",
21
20
  "clean": "npm run clean:build && npm run clean:sb",
22
21
  "clean:build": "rimraf ./dist",
23
22
  "clean:sb": "rimraf ./storybook-static"
24
23
  },
25
24
  "devDependencies": {
26
- "@storybook/addon-a11y": "10.5.10",
27
- "@storybook/addon-links": "10.5.10",
28
- "@storybook/react-vite": "10.5.10",
25
+ "@storybook/addon-a11y": "10.6.0",
26
+ "@storybook/addon-docs": "10.6.0",
27
+ "@storybook/addon-links": "10.6.0",
28
+ "@storybook/react-vite": "10.6.0",
29
+ "@testing-library/jest-dom": "7.0.1",
29
30
  "@testing-library/react": "16.3.3",
30
- "@types/react": "19.2.18",
31
- "@types/react-dom": "19.2.5",
32
- "jest-environment-jsdom": "29.7.0",
31
+ "@types/react": "19.3.0",
32
+ "@types/react-dom": "19.3.0",
33
33
  "jotai": "2.20.3",
34
- "react": "19.2.8",
35
- "react-dom": "19.2.8",
36
- "storybook": "10.5.10",
37
- "tone": "15.1.22",
38
- "@storybook/addon-docs": "10.5.10"
34
+ "react": "19.3.0",
35
+ "react-dom": "19.3.0",
36
+ "storybook": "10.6.0",
37
+ "tone": "15.1.22"
39
38
  },
40
39
  "peerDependencies": {
41
40
  "react": "^18 ||^19",
42
41
  "react-dom": "^18 || ^19"
43
42
  },
44
43
  "dependencies": {
45
- "@tremolo-ui/dom": "^0.4.0",
46
- "@tremolo-ui/functions": "^0.4.0",
47
- "clsx": "^2.1.1",
48
- "zustand": "^5.0.3"
44
+ "@tremolo-ui/dom": "^0.6.0",
45
+ "@tremolo-ui/functions": "^0.6.0"
49
46
  },
50
47
  "files": [
51
48
  "dist",
52
- "src"
49
+ "src",
50
+ "!src/**/*.test.{ts,tsx}",
51
+ "!src/**/*.stories.{ts,tsx}"
53
52
  ],
54
53
  "publishConfig": {
55
54
  "access": "public"
@@ -83,13 +82,15 @@
83
82
  "default": "./dist/index.js"
84
83
  }
85
84
  },
86
- "./styles/index.css": "./dist/index.css",
87
- "./styles/global.css": "./src/styles/global.css",
88
- "./styles/Knob.css": "./src/components/Knob/index.css",
89
- "./styles/NumberInput.css": "./src/components/NumberInput/index.css",
90
- "./styles/Piano.css": "./src/components/Piano/index.css",
91
- "./styles/PointsEditor.css": "./src/components/PointsEditor/index.css",
92
- "./styles/Slider.css": "./src/components/Slider/index.css",
93
- "./styles/XYPad.css": "./src/components/XYPad/index.css"
85
+ "./compose-refs": {
86
+ "require": {
87
+ "types": "./dist/compose-refs.d.cts",
88
+ "default": "./dist/compose-refs.cjs"
89
+ },
90
+ "import": {
91
+ "types": "./dist/compose-refs.d.ts",
92
+ "default": "./dist/compose-refs.js"
93
+ }
94
+ }
94
95
  }
95
96
  }
@@ -1,19 +1,18 @@
1
- import clsx from 'clsx'
2
1
  import {
3
2
  ComponentPropsWithoutRef,
4
- ReactElement,
5
- RefObject,
6
- useCallback,
3
+ forwardRef,
7
4
  useEffect,
8
5
  useRef,
6
+ useState,
9
7
  } from 'react'
10
8
 
11
9
  import {
12
- DrawingContext,
13
- drawingState,
14
- DrawingStateValue,
15
- setDprConfig,
16
- } from './canvas'
10
+ createAnimationCanvas,
11
+ type AnimationCanvasInstance,
12
+ type AnimationFrame,
13
+ } from '@tremolo-ui/dom'
14
+
15
+ import { useComposedRefs } from '../../compose-refs'
17
16
 
18
17
  export type InitFunction = (
19
18
  context: CanvasRenderingContext2D,
@@ -27,216 +26,185 @@ export type InitFunction = (
27
26
 
28
27
  export type DrawFunction = (
29
28
  context: CanvasRenderingContext2D,
30
- option: {
31
- /** current canvas width */
32
- width: number
33
- /** current canvas height */
34
- height: number
35
- /** frame count */
36
- count: number
37
- /** delta time (ms) */
38
- deltaTime: number
39
- /** elapsed time (ms) */
40
- elapsedTime: number
41
- /** frame per second */
42
- fps: number
43
- },
29
+ option: AnimationFrame,
44
30
  ) => void
45
31
 
46
- export interface CommonProps {
32
+ export interface AnimationCanvasCommonProps {
33
+ /**
34
+ * Draw one frame. It is given the 2D context and the frame: the size in CSS
35
+ * pixels, `count`, `deltaTime`, `elapsedTime` and `fps`.
36
+ *
37
+ * A new function replaces the old one in place, so writing it inline does
38
+ * not restart anything.
39
+ */
47
40
  draw: DrawFunction
41
+ /**
42
+ * Called once before the first frame, with the context and the size in CSS
43
+ * pixels. Set up what every frame shares here.
44
+ */
48
45
  init?: InitFunction
46
+ /**
47
+ * Redraw on every animation frame. Turn it off to draw only when there is a
48
+ * reason to: when the canvas mounts, when it is resized, and when the
49
+ * component re-renders.
50
+ *
51
+ * @default true
52
+ */
49
53
  animate?: boolean
50
54
  /**
55
+ * Read once, when the 2D context is created, so changing it later has no
56
+ * effect. Passing a fresh object on every render is therefore harmless.
57
+ *
51
58
  * @see https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement/getContext#contextattributes
52
59
  */
53
60
  options?: CanvasRenderingContext2DSettings
61
+ /**
62
+ * Carry the drawing across a resize, so that the canvas does not blank for a
63
+ * frame while the new size is drawn. A fixed canvas is resized too, when its
64
+ * `width` or `height` changes.
65
+ *
66
+ * @default true
67
+ */
68
+ reduceFlickering?: boolean
54
69
  }
55
70
 
56
- export interface AbsoluteSizingProps {
71
+ /** A canvas of the size given in CSS pixels. This is the default. */
72
+ export interface AnimationCanvasFixedProps {
73
+ /** Leave it out, or pass `false`, for a canvas of the size below. */
74
+ resizable?: false
75
+ /**
76
+ * Width of the canvas in CSS pixels.
77
+ * @default 100
78
+ */
57
79
  width?: number
80
+ /**
81
+ * Height of the canvas in CSS pixels.
82
+ * @default 100
83
+ */
58
84
  height?: number
59
85
  }
60
86
 
61
- export interface RelativeSizingProps {
62
- relativeSize?: boolean
63
- reduceFlickering?: boolean
87
+ /**
88
+ * A canvas that follows the size of its parent element, so the parent needs a
89
+ * size of its own.
90
+ */
91
+ export interface AnimationCanvasResizableProps {
92
+ /**
93
+ * Follow the size of the parent element instead of `width` and `height`.
94
+ * Switching it rebuilds the canvas.
95
+ */
96
+ resizable: true
97
+ /** Not accepted: the size comes from the parent element. */
98
+ width?: never
99
+ /** Not accepted: the size comes from the parent element. */
100
+ height?: never
64
101
  }
65
102
 
66
- export type AnimationCanvasProps = CommonProps &
67
- AbsoluteSizingProps &
68
- RelativeSizingProps
103
+ export type AnimationCanvasProps = AnimationCanvasCommonProps &
104
+ (AnimationCanvasFixedProps | AnimationCanvasResizableProps)
69
105
 
70
- /**
71
- * A simple animatable canvas with requestAnimationFrame()
72
- */
73
- export function AnimationCanvas(
74
- props: CommonProps &
75
- AbsoluteSizingProps &
76
- Omit<ComponentPropsWithoutRef<'canvas'>, keyof AnimationCanvasProps>,
77
- ): ReactElement
78
- export function AnimationCanvas(
79
- pros: CommonProps &
80
- RelativeSizingProps &
81
- Omit<ComponentPropsWithoutRef<'canvas'>, keyof AnimationCanvasProps>,
82
- ): ReactElement
83
- export function AnimationCanvas({
84
- // common
85
- draw,
86
- init,
87
- animate = true,
88
- options,
89
- // absolute
90
- width: _width = 100,
91
- height: _height = 100,
92
- // relative
93
- relativeSize,
94
- reduceFlickering = true,
95
- // canvas props
96
- className,
97
- onContextMenu = (event) => event.preventDefault(),
98
- ...props
99
- }: AnimationCanvasProps &
106
+ type Props = AnimationCanvasProps &
100
107
  Omit<
101
108
  ComponentPropsWithoutRef<'canvas'>,
102
- keyof AnimationCanvasProps
103
- >): ReactElement {
104
- const canvasRef = useRef<HTMLCanvasElement>(null)
105
- const memoCanvasRef = useRef<HTMLCanvasElement>(null)
106
- const reqIdRef = useRef(-1)
107
- const widthRef = useRef(0)
108
- const heightRef = useRef(0)
109
- const deltaMemoRef = useRef(-1)
110
- const startTimeRef = useRef(-1)
111
-
112
- const loop = useCallback(
113
- (
114
- context: CanvasRenderingContext2D,
115
- width: RefObject<number>,
116
- height: RefObject<number>,
117
- count: number,
118
- ) => {
119
- const now = performance.now()
120
- const deltaTime = now - deltaMemoRef.current
121
- const elapsedTime = now - startTimeRef.current
122
- deltaMemoRef.current = now
123
- if (animate) {
124
- reqIdRef.current = requestAnimationFrame(() =>
125
- loop(context, width, height, count + 1),
126
- )
127
- }
128
- draw(context, {
129
- width: width.current,
130
- height: height.current,
131
- count: count + 1,
132
- deltaTime,
133
- elapsedTime,
134
- fps: 1000 / deltaTime,
135
- })
136
- },
137
- [draw, animate],
138
- )
109
+ keyof AnimationCanvasCommonProps | 'resizable' | 'width' | 'height'
110
+ >
139
111
 
112
+ /**
113
+ * A simple animatable canvas with requestAnimationFrame()
114
+ */
115
+ export const AnimationCanvas = /* @__PURE__ */ forwardRef<
116
+ HTMLCanvasElement,
117
+ Props
118
+ >(function AnimationCanvas(
119
+ {
120
+ // common
121
+ draw,
122
+ init,
123
+ animate = true,
124
+ options,
125
+ reduceFlickering = true,
126
+ // fixed
127
+ width = 100,
128
+ height = 100,
129
+ // resizable
130
+ resizable = false,
131
+ // canvas props
132
+ className,
133
+ onContextMenu = (event) => event.preventDefault(),
134
+ ...props
135
+ },
136
+ forwardedRef,
137
+ ) {
138
+ // See useDrag for why the node is held in state rather than a ref: an inline
139
+ // ref would be re-attached on every render and tear the instance down.
140
+ const [node, setNode] = useState<HTMLCanvasElement | null>(null)
141
+ // The caller's ref gets the same element.
142
+ const composedRef = useComposedRefs<HTMLCanvasElement>(forwardedRef, setNode)
143
+
144
+ // Read when the instance is created. The effect below keeps it current, and
145
+ // runs right after, so a stale handler is replaced within the same commit.
146
+ const latest = useRef({
147
+ draw,
148
+ init,
149
+ animate,
150
+ width,
151
+ height,
152
+ reduceFlickering,
153
+ options,
154
+ })
155
+ const instanceRef = useRef<AnimationCanvasInstance | null>(null)
156
+
157
+ // Runs before the creation effect below. This keeps a rebuilt instance on
158
+ // the current render's size and handlers while updating an existing one in
159
+ // place on ordinary renders.
140
160
  useEffect(() => {
141
- if (!canvasRef.current) return
142
- const canvas = canvasRef.current
143
- const context = canvas.getContext('2d', options)
144
- if (!context) throw new Error('Cannot get canvas context.')
145
- const dpr = globalThis.devicePixelRatio
146
-
147
- const firstRendering = (width: number, height: number) => {
148
- setDprConfig(canvas, context, width, height, dpr)
149
- widthRef.current = width
150
- heightRef.current = height
151
-
152
- if (init) init(context, { width, height })
153
- const now = performance.now()
154
- deltaMemoRef.current = now
155
- startTimeRef.current = now
156
- loop(context, widthRef, heightRef, -1)
161
+ latest.current = {
162
+ draw,
163
+ init,
164
+ animate,
165
+ width,
166
+ height,
167
+ reduceFlickering,
168
+ options,
157
169
  }
170
+ instanceRef.current?.update({
171
+ animate,
172
+ size: { width, height },
173
+ reduceFlickering,
174
+ })
175
+ })
158
176
 
159
- if (relativeSize) {
160
- const parent = canvas.parentElement
161
- if (!parent) throw new Error("Canvas doesn't have a parent element.")
162
- const memoCanvas = memoCanvasRef.current
163
- const memoContext = memoCanvas?.getContext('2d', options)
164
-
165
- const ro = new ResizeObserver((entries) => {
166
- for (const entry of entries) {
167
- const w = entry.contentRect.width
168
- const h = entry.contentRect.height
169
- const contextMemo = {} as DrawingContext
170
- if (reduceFlickering && memoCanvas && memoContext) {
171
- // Prevents loss of some context when the canvas is resized
172
- for (const prop of drawingState) {
173
- ;(contextMemo[prop] as DrawingStateValue) = context[prop]
174
- }
175
- memoCanvas.width = w * dpr
176
- memoCanvas.height = h * dpr
177
- memoContext.scale(1 / dpr, 1 / dpr)
178
- if (canvas.width > 0 && canvas.height > 0) {
179
- memoContext.drawImage(canvas, 0, 0)
180
- }
181
- }
182
-
183
- setDprConfig(canvas, context, w, h, dpr)
184
- widthRef.current = w
185
- heightRef.current = h
186
-
187
- if (
188
- reduceFlickering &&
189
- memoContext &&
190
- memoCanvas &&
191
- memoCanvas.width > 0 &&
192
- memoCanvas.height > 0
193
- ) {
194
- for (const prop of drawingState) {
195
- ;(context[prop] as DrawingStateValue) = contextMemo[prop]
196
- }
197
-
198
- context.drawImage(memoContext.canvas, 0, 0)
199
- }
200
-
201
- if (!animate) {
202
- // re rendering
203
- loop(context, widthRef, heightRef, -1)
204
- }
205
- }
206
- })
207
- ro.observe(parent)
208
-
209
- const width = parent.clientWidth
210
- const height = parent.clientHeight
211
- firstRendering(width, height)
212
-
213
- return () => {
214
- if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current)
215
- ro.disconnect()
216
- }
217
- } else {
218
- const { width, height } = canvas.getBoundingClientRect()
219
- firstRendering(width, height)
220
-
221
- return () => {
222
- if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current)
223
- }
177
+ useEffect(() => {
178
+ if (!node) return
179
+
180
+ const current = latest.current
181
+ const instance = createAnimationCanvas(node, {
182
+ draw: (context, frame) => latest.current.draw(context, frame),
183
+ init: (context, size) => latest.current.init?.(context, size),
184
+ animate: current.animate,
185
+ size: { width: current.width, height: current.height },
186
+ reduceFlickering: current.reduceFlickering,
187
+ resizable,
188
+ contextAttributes: current.options,
189
+ })
190
+ instanceRef.current = instance
191
+
192
+ return () => {
193
+ instanceRef.current = null
194
+ instance.destroy()
224
195
  }
225
- }, [loop, init, options, reduceFlickering, relativeSize, animate])
196
+ // Only `resizable` decides how the instance is wired, so it is the one
197
+ // setting that rebuilds it. `options` is read from the ref above rather
198
+ // than depended on: it is almost always written inline, and depending on
199
+ // it would tear the canvas down on every render.
200
+ }, [node, resizable])
226
201
 
227
202
  return (
228
- <>
229
- <canvas
230
- className={clsx('tremolo-animation-canvas', className)}
231
- width={relativeSize ? 0 : _width}
232
- height={relativeSize ? 0 : _height}
233
- ref={canvasRef}
234
- onContextMenu={onContextMenu}
235
- {...props}
236
- ></canvas>
237
- {relativeSize && reduceFlickering && (
238
- <canvas style={{ display: 'none' }} ref={memoCanvasRef}></canvas>
239
- )}
240
- </>
203
+ <canvas
204
+ className={className}
205
+ ref={composedRef}
206
+ onContextMenu={onContextMenu}
207
+ {...props}
208
+ />
241
209
  )
242
- }
210
+ })
@@ -1,7 +1,8 @@
1
- import clsx from 'clsx'
2
1
  import { SVGProps } from 'react'
3
2
 
4
- import { center, 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
- const x2 = useKnobContext((s) => s.x2)
15
- const y2 = useKnobContext((s) => s.y2)
16
- const x3 = useKnobContext((s) => s.x3)
17
- const y3 = useKnobContext((s) => s.y3)
17
+
18
+ const radius = arcRadius(strokeWidth)
18
19
 
19
20
  return (
20
21
  <path
21
- className={clsx('tremolo-knob-active-line', className)}
22
- d={`M ${x2} ${y2} A ${center} ${center} -135 ${r3 - r2 > 180 ? 1 : 0} 1 ${x3} ${y3}`}
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 { center, 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)
@@ -17,21 +20,15 @@ export function InactiveLine({
17
20
  const r2 = useKnobContext((s) => s.r2)
18
21
  const r3 = useKnobContext((s) => s.r3)
19
22
  const r4 = useKnobContext((s) => s.r4)
20
- const x1 = useKnobContext((s) => s.x1)
21
- const y1 = useKnobContext((s) => s.y1)
22
- const x2 = useKnobContext((s) => s.x2)
23
- const y2 = useKnobContext((s) => s.y2)
24
- const x3 = useKnobContext((s) => s.x3)
25
- const y3 = useKnobContext((s) => s.y3)
26
- const x4 = useKnobContext((s) => s.x4)
27
- const y4 = useKnobContext((s) => s.y4)
23
+
24
+ const radius = arcRadius(strokeWidth)
28
25
 
29
26
  return (
30
27
  <>
31
28
  {startValue > min && (
32
29
  <path
33
- className={clsx('tremolo-knob-inactive-line', className)}
34
- d={`M ${x1} ${y1} A ${center} ${center} -135 ${r2 - r1 > 180 ? 1 : 0} 1 ${x2} ${y2}`}
30
+ className={className}
31
+ d={arcPath(r1, r2, radius)}
35
32
  fill="none"
36
33
  stroke={stroke}
37
34
  strokeWidth={strokeWidth}
@@ -40,8 +37,8 @@ export function InactiveLine({
40
37
  )}
41
38
  {startValue < max && (
42
39
  <path
43
- className={clsx('tremolo-knob-inactive-line', className)}
44
- d={`M ${x3} ${y3} A ${center} ${center} -135 ${r4 - r3 > 180 ? 1 : 0} 1 ${x4} ${y4}`}
40
+ className={className}
41
+ d={arcPath(r3, r4, radius)}
45
42
  fill="none"
46
43
  stroke={stroke}
47
44
  strokeWidth={strokeWidth}
@@ -1,39 +1,32 @@
1
- import { SVGProps } from 'react'
1
+ import { ReactNode, SVGProps } from 'react'
2
2
 
3
- import { Override } from '../_util/type'
3
+ import { Placement } from '../_util/Placement'
4
4
 
5
- import { ActiveLine } from './ActiveLine'
6
5
  import { viewBoxSize } from './context'
7
- import { InactiveLine } from './InactiveLine'
8
- import { Thumb } from './Thumb'
9
6
 
10
- interface SVGRoot {
11
- block?: boolean
12
- overflowVisible?: boolean
7
+ export interface KnobSVGRootProps {
8
+ /**
9
+ * `<Knob.InactiveLine />`, `<Knob.ActiveLine />` and `<Knob.Thumb />` go
10
+ * here, in the order you want them painted.
11
+ */
12
+ children: ReactNode
13
13
  }
14
14
 
15
15
  export function SVGRoot({
16
16
  children,
17
17
  style,
18
18
  ...props
19
- }: Override<SVGRoot, SVGProps<SVGSVGElement>>) {
19
+ }: KnobSVGRootProps & Omit<SVGProps<SVGSVGElement>, keyof KnobSVGRootProps>) {
20
20
  return (
21
21
  <svg
22
22
  viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
23
23
  style={{
24
24
  display: 'block',
25
- overflow: 'visible',
26
25
  ...style,
27
26
  }}
28
27
  {...props}
29
28
  >
30
- {children || (
31
- <>
32
- <InactiveLine />
33
- <ActiveLine />
34
- <Thumb />
35
- </>
36
- )}
29
+ <Placement name="Knob.SVGRoot">{children}</Placement>
37
30
  </svg>
38
31
  )
39
32
  }