@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,4 +1,3 @@
1
- import clsx from 'clsx'
2
1
  import React, {
3
2
  ComponentPropsWithoutRef,
4
3
  CSSProperties,
@@ -10,20 +9,25 @@ import React, {
10
9
  useRef,
11
10
  } from 'react'
12
11
 
13
- import type { AxisOptions, XY } from '@tremolo-ui/dom'
14
12
  import {
15
13
  applyDelta,
16
- linearScale,
17
- toFixed,
18
- InputEventOption,
19
- xor,
20
- type Scale,
21
- } 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, toFixed } from '@tremolo-ui/functions'
22
21
 
22
+ import { useComposedRefs } from '../../compose-refs'
23
+ import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
23
24
  import { useDragValue } from '../../hooks/useDragValue'
24
25
  import { useWheel } from '../../hooks/useWheel'
25
- import { addUserSelectNone, Cursor, removeUserSelectNone } from '../_util'
26
- import { useComposedRefs } from '../_util/composeRefs'
26
+ import {
27
+ DEFAULT_DRAG_SENSITIVITY,
28
+ DEFAULT_KEYBOARD_OPTIONS,
29
+ DEFAULT_WHEEL_OPTIONS,
30
+ } from '../../input-event'
27
31
 
28
32
  import { SliderProvider } from './context'
29
33
  import { Marks } from './Marks'
@@ -32,17 +36,23 @@ import { Thumb, SliderThumbMethods } from './Thumb'
32
36
  import { Track } from './Track'
33
37
 
34
38
  const defaultExternalStyles: SliderProps['externalStyles'] = {
35
- userSelectNone: true,
36
39
  cursor: 'pointer',
37
40
  }
38
41
 
39
42
  export interface SliderProps {
40
- // required
43
+ /** The current value. The slider shows only this, so update it from `onChange`. */
41
44
  value: number
45
+ /** The value at the start of the travel. */
42
46
  min: number
47
+ /** The value at the end of the travel. */
43
48
  max: number
44
49
 
45
- // optional
50
+ /**
51
+ * Granularity of the value. A drag, the wheel and the arrow keys snap it to
52
+ * multiples of `step`.
53
+ *
54
+ * @default 1
55
+ */
46
56
  step?: number
47
57
  /**
48
58
  * How the value is distributed across the travel.
@@ -55,43 +65,91 @@ export interface SliderProps {
55
65
  */
56
66
  scale?: Scale
57
67
  /**
58
- * slider orientation
59
- * aria-orientation property is also applied.
68
+ * Run the slider vertically, with the value growing upwards. The range input
69
+ * inside the thumb takes its `aria-orientation` from this.
70
+ *
71
+ * @default false
60
72
  */
61
73
  vertical?: boolean
74
+ /**
75
+ * Grow the value the other way: leftwards, or downwards when `vertical`.
76
+ * The arrow keys follow the direction on screen.
77
+ *
78
+ * @default false
79
+ */
62
80
  reverse?: boolean
63
81
 
64
- /** Global style to apply when dragged */
82
+ /**
83
+ * The cursor to show while dragging. It is set on the dragged element, so it
84
+ * stays while the pointer is outside the slider.
85
+ *
86
+ * @default { cursor: 'pointer' }
87
+ */
65
88
  externalStyles?: {
66
- userSelectNone?: boolean
67
- cursor?: Cursor
89
+ cursor?: CSSProperties['cursor']
68
90
  }
69
91
  /**
70
- * wheel control option
71
- * If null, no event will be triggered
92
+ * How much one notch of the wheel moves the value. It only acts while the
93
+ * focus is inside, so that scrolling the page past the slider leaves it
94
+ * alone.
95
+ *
96
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
97
+ * range between `min` and `max`. The result is snapped to `step`, except for
98
+ * an amount set on a modifier key (`{ default: …, shift: … }`). `null` turns
99
+ * the wheel off.
100
+ *
101
+ * @default ['raw', 1]
102
+ */
103
+ wheel?: ModifierValue<InputEventOption> | null
104
+ /**
105
+ * How much a drag moves the value, per modifier key.
106
+ *
107
+ * `1` is the pointer position itself, which is what a drag normally is here.
108
+ * **Anything else turns the drag relative**: `0.1` makes the same movement
109
+ * cover a tenth of the travel, so the value stops following the pointer and
110
+ * starts moving a tenth as fast. Shift is bound to `0.1` by default, to
111
+ * match what it does on the arrow keys.
112
+ *
113
+ * Pressing or releasing the key mid-drag does not disturb the value: the
114
+ * travel so far is kept and the new sensitivity applies from there. **The
115
+ * pointer and the value stay apart for the rest of the drag** — snapping
116
+ * them back together on release would move the value nobody asked to move.
117
+ *
118
+ * @default { default: 1, shift: 0.1 }
72
119
  */
73
- wheel?: InputEventOption | null
120
+ dragSensitivity?: ModifierValue<number>
121
+
74
122
  /**
75
- * keyboard control option
76
- * If null, no event will be triggered
123
+ * How much one arrow key press moves the value.
124
+ *
125
+ * `['raw', n]` moves the value by `n`, and `['normalized', n]` by `n` of the
126
+ * range between `min` and `max`. The result is snapped to `step`, except for
127
+ * an amount set on a modifier key, which is what lets shift move off the
128
+ * grid. `null` turns the arrow keys off.
129
+ *
130
+ * The default moves by 1, and by 0.1 with shift. With a `step` above 1, raise
131
+ * the amount to match: 1 would round straight back to where it started, and
132
+ * a development build warns about it.
133
+ *
134
+ * @default { default: ['raw', 1], shift: ['raw', 0.1] }
77
135
  */
78
- keyboard?: InputEventOption | null
136
+ keyboard?: ModifierValue<InputEventOption> | null
79
137
 
80
138
  /**
81
- * Only the appearance will change.
82
- * Please consider using with readonly.
83
- * aria-disabled property is also applied.
139
+ * Make the slider unchangeable and remove it from the tab order.
140
+ * The parts carry `data-disabled` while it is set.
84
141
  */
85
142
  disabled?: boolean
86
143
  /**
87
144
  * Make the value unchangeable.
88
- * aria-readonly property is also applied.
145
+ * The parts carry `data-readonly` while it is set.
89
146
  */
90
147
  readonly?: boolean
91
- className?: string
92
- style?: CSSProperties
148
+ /** Called with the new value when a drag, the wheel or an arrow key moves it. */
93
149
  onChange?: (value: number) => void
150
+ /** Called when a drag starts, with the value where the track was pressed. */
94
151
  onDragStart?: (value: number) => void
152
+ /** Called when the drag ends, with the value it ended on. */
95
153
  onDragEnd?: (value: number) => void
96
154
  /**
97
155
  * The slider renders exactly what you compose here; there is no default
@@ -100,7 +158,7 @@ export interface SliderProps {
100
158
  * @example
101
159
  * <Slider.Root value={value} min={0} max={100} onChange={setValue}>
102
160
  * <Slider.Track>
103
- * <Slider.Thumb />
161
+ * <Slider.Thumb aria-label="Level" />
104
162
  * </Slider.Track>
105
163
  * </Slider.Root>
106
164
  */
@@ -121,7 +179,7 @@ type Props = SliderProps &
121
179
  */
122
180
  const WHEEL_OPTIONS = { requireFocus: true }
123
181
 
124
- export const Root = forwardRef<SliderMethods, Props>(
182
+ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
125
183
  (
126
184
  {
127
185
  value,
@@ -132,8 +190,9 @@ export const Root = forwardRef<SliderMethods, Props>(
132
190
  vertical = false,
133
191
  reverse = false,
134
192
  externalStyles: _externalStyles,
135
- wheel = ['raw', 1],
136
- keyboard = ['raw', 1],
193
+ wheel = DEFAULT_WHEEL_OPTIONS,
194
+ keyboard = DEFAULT_KEYBOARD_OPTIONS,
195
+ dragSensitivity = DEFAULT_DRAG_SENSITIVITY,
137
196
  disabled = false,
138
197
  readonly = false,
139
198
  onChange,
@@ -153,9 +212,9 @@ export const Root = forwardRef<SliderMethods, Props>(
153
212
  // -- state and ref ---
154
213
  const trackRef = useRef<HTMLDivElement>(null)
155
214
  const thumbRef = useRef<SliderThumbMethods>(null)
156
-
157
215
  // --- interpret props ---
158
216
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
217
+ const inactive = disabled || readonly
159
218
 
160
219
  const p = toFixed(scale.normalize(value, min, max) * 100)
161
220
  const rev = toFixed(100 - p)
@@ -164,10 +223,7 @@ export const Root = forwardRef<SliderMethods, Props>(
164
223
  // vertical -> rev (up)
165
224
  // reverse -> rev (left)
166
225
  // vertical & reverse -> normal (down)
167
- const displayReversed = useMemo(
168
- () => xor(vertical, reverse),
169
- [vertical, reverse],
170
- )
226
+ const displayReversed = vertical !== reverse
171
227
  const percent = displayReversed ? rev : p
172
228
 
173
229
  // --- internal functions ---
@@ -179,18 +235,20 @@ export const Root = forwardRef<SliderMethods, Props>(
179
235
  [min, max, step, scale, displayReversed],
180
236
  )
181
237
 
238
+ useCheckSteps({ component: 'Slider', range: axis, keyboard, wheel })
239
+
182
240
  const handleKeyDown = useCallback(
183
241
  (event: React.KeyboardEvent<HTMLDivElement>) => {
184
- if (!keyboard || !onChange || readonly) return
185
242
  const key = event.key
186
- if (['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key)) {
187
- event.preventDefault()
188
- let direction = key == 'ArrowRight' || key == 'ArrowUp' ? 1 : -1
189
- if (reverse) direction *= -1
190
- onChange(applyDelta(value, direction, keyboard, axis))
191
- }
243
+ if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
244
+ return
245
+ event.preventDefault()
246
+ if (!keyboard || !onChange || inactive) return
247
+ let direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
248
+ if (reverse) direction *= -1
249
+ onChange(applyDelta(value, direction, keyboard, axis, event))
192
250
  },
193
- [keyboard, onChange, readonly, reverse, value, axis],
251
+ [keyboard, onChange, inactive, reverse, value, axis],
194
252
  )
195
253
 
196
254
  // --- hooks ---
@@ -199,40 +257,38 @@ export const Root = forwardRef<SliderMethods, Props>(
199
257
  const { refCallback: dragRefCallback } = useDragValue<HTMLDivElement>({
200
258
  axis,
201
259
  baseElementRef: trackRef,
202
- cursor: readonly ? undefined : externalStyles.cursor,
260
+ cursor: inactive ? undefined : externalStyles.cursor,
261
+ shouldStart: () => !inactive,
262
+ sensitivity: (state) =>
263
+ selectModifier(dragSensitivity, state.event).value,
203
264
  updateOnPointerDown: true,
204
265
  onChange: (v) => {
205
- if (readonly) return
266
+ if (inactive) return
206
267
  onChange?.(valueOf(v))
207
268
  },
208
269
  onDragStart: (v) => {
209
- if (readonly) return
210
- if (externalStyles.userSelectNone) addUserSelectNone()
211
-
270
+ if (inactive) return
212
271
  thumbRef.current?.focus()
213
272
  onDragStart?.(valueOf(v))
214
273
  },
215
274
  onDragEnd: (v) => {
216
- if (readonly) return
217
-
218
- if (externalStyles.userSelectNone) removeUserSelectNone()
219
-
275
+ if (inactive) return
220
276
  onDragEnd?.(valueOf(v))
221
277
  },
222
278
  })
223
279
 
224
280
  const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
225
- if (!wheel || !onChange || readonly) return
281
+ if (!wheel || !onChange || inactive) return
226
282
  event.preventDefault()
227
283
  let direction
228
- if (!vertical && event.deltaX != 0) {
284
+ if (!vertical && event.deltaX !== 0) {
229
285
  direction = event.deltaX > 0 ? 1 : -1
230
286
  } else {
231
- if (event.deltaY == 0) return
287
+ if (event.deltaY === 0) return
232
288
  direction = event.deltaY > 0 ? -1 : 1
233
289
  }
234
- if (vertical && reverse) direction *= -1
235
- onChange(applyDelta(value, direction, wheel, axis))
290
+ if (reverse) direction *= -1
291
+ onChange(applyDelta(value, direction, wheel, axis, event))
236
292
  }, WHEEL_OPTIONS)
237
293
 
238
294
  // Composed once, so React attaches the refs a single time instead of
@@ -253,6 +309,7 @@ export const Root = forwardRef<SliderMethods, Props>(
253
309
  reverse,
254
310
  disabled,
255
311
  readonly,
312
+ onChange,
256
313
  percent,
257
314
  trackRef,
258
315
  thumbRef,
@@ -267,6 +324,7 @@ export const Root = forwardRef<SliderMethods, Props>(
267
324
  reverse,
268
325
  disabled,
269
326
  readonly,
327
+ onChange,
270
328
  percent,
271
329
  ],
272
330
  )
@@ -274,28 +332,29 @@ export const Root = forwardRef<SliderMethods, Props>(
274
332
  useImperativeHandle(forwardedRef, () => {
275
333
  return {
276
334
  focus() {
277
- thumbRef.current?.focus()
335
+ if (!disabled) thumbRef.current?.focus()
278
336
  },
279
337
  blur() {
280
338
  thumbRef.current?.blur()
281
339
  },
282
340
  }
283
- }, [])
341
+ }, [disabled])
284
342
 
285
343
  return (
286
344
  <SliderProvider value={context}>
345
+ {/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- the group is the pointer and keyboard event area, while the nested range input carries its control semantics */}
287
346
  <div
288
- className={clsx('tremolo-slider', className)}
347
+ className={className}
289
348
  ref={rootRefCallback}
349
+ role="group"
350
+ // A press lands on the track or the thumb, neither of which can hold
351
+ // focus, and the browser answers that by clearing the focus to the
352
+ // body — undoing the focus the drag just gave the input. Taking the
353
+ // focus here keeps it inside, and `onFocus` passes it to the input.
290
354
  tabIndex={-1}
291
- role="slider"
292
- aria-valuenow={value}
293
- aria-valuemin={min}
294
- aria-valuemax={max}
295
- aria-orientation={vertical ? 'vertical' : 'horizontal'}
296
- aria-disabled={disabled}
297
- aria-readonly={readonly}
298
- data-vertical={vertical}
355
+ data-orientation={vertical ? 'vertical' : 'horizontal'}
356
+ data-disabled={disabled ? '' : undefined}
357
+ data-readonly={readonly ? '' : undefined}
299
358
  style={style}
300
359
  onPointerDown={onPointerDown}
301
360
  onKeyDown={(event) => {
@@ -303,7 +362,7 @@ export const Root = forwardRef<SliderMethods, Props>(
303
362
  onKeyDown?.(event)
304
363
  }}
305
364
  onFocus={(event) => {
306
- thumbRef.current?.focus()
365
+ if (!disabled) thumbRef.current?.focus()
307
366
  onFocus?.(event)
308
367
  }}
309
368
  onBlur={(event) => {
@@ -330,9 +389,9 @@ export const Slider = {
330
389
  MarksOption,
331
390
  }
332
391
 
333
- export { useSliderContext } from './context'
392
+ export { useSliderContext, type SliderContextValue } from './context'
334
393
  export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
335
394
  export { type SliderTrackProps } from './Track'
336
- export { type MarksProps } from './Marks'
337
- export { type MarksOptionProps } from './MarksOption'
338
- export { type MarksOptions, type MarksType } from './type'
395
+ export { type SliderMarksProps } from './Marks'
396
+ export { type SliderMarksOptionProps } from './MarksOption'
397
+ export { type MarksOptions } from './type'
@@ -1,7 +1,21 @@
1
- import { decimalPart, toFixed } from '@tremolo-ui/functions'
1
+ import { toFixed } from '@tremolo-ui/functions'
2
2
 
3
- export type MarksType = 'mark' | 'mark-number' | 'number'
4
- export type MarksOptions = ['step', MarksType] | [number, MarksType]
3
+ import { decimalDigits } from '../_util/decimal-digits'
4
+
5
+ /**
6
+ * How `Slider.Marks` fills itself in when it is given no children: one option
7
+ * every `per`, or every `step` of the slider. The object form turns off the
8
+ * mark or the label for the whole set; a single option is customized by
9
+ * writing `Slider.MarksOption` out instead.
10
+ */
11
+ export type MarksOptions =
12
+ | 'step'
13
+ | number
14
+ | {
15
+ per: 'step' | number
16
+ mark?: boolean
17
+ label?: boolean
18
+ }
5
19
 
6
20
  export function generateOptionsList(
7
21
  options: MarksOptions,
@@ -9,18 +23,24 @@ export function generateOptionsList(
9
23
  max: number,
10
24
  step: number,
11
25
  ) {
26
+ const {
27
+ per,
28
+ mark = true,
29
+ label = true,
30
+ } = typeof options === 'object' ? options : { per: options }
12
31
  const optionsList: {
13
32
  value: number
14
- type: MarksType
33
+ mark: boolean
34
+ label: boolean
15
35
  }[] = []
16
- const per = options[0] == 'step' ? step : options[0]
17
- const count = Math.floor(max / per) - Math.ceil(min / per) + 1
36
+ const interval = per === 'step' ? step : per
37
+ const count = Math.floor(max / interval) - Math.ceil(min / interval) + 1
18
38
  for (let i = 0; i < count; i++) {
19
39
  const value = toFixed(
20
- per * (Math.ceil(min / per) + i),
21
- decimalPart(per)?.length,
40
+ interval * (Math.ceil(min / interval) + i),
41
+ decimalDigits(interval),
22
42
  )
23
- optionsList.push({ value: value, type: options[1] })
43
+ optionsList.push({ value: value, mark: mark, label: label })
24
44
  }
25
45
  return optionsList
26
46
  }
@@ -1,51 +1,56 @@
1
- import clsx from 'clsx'
2
- import { ComponentPropsWithoutRef, CSSProperties, ReactNode, Ref } from 'react'
1
+ import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
3
2
 
4
- import { useComposedRefs } from '../_util/composeRefs'
3
+ import { useComposedRefs } from '../../compose-refs'
4
+ import { cssLength } from '../_util/css-length'
5
+ import { Placement } from '../_util/Placement'
5
6
 
6
7
  import { useXYPadContext } from './context'
7
8
 
9
+ import type { CSSVariables } from '../../css-variables'
10
+
8
11
  export interface XYPadAreaProps {
12
+ /** Sets `--width`; the size the theme gives it stands when omitted. */
9
13
  width?: number | string
14
+ /** Sets `--height`; the size the theme gives it stands when omitted. */
10
15
  height?: number | string
16
+ /** Sets `--color`, for the theme to colour the area with. */
11
17
  color?: string
12
- className?: string
13
- style?: CSSProperties
14
- /** `<XYPad.Thumb />` goes here. */
15
- children?: ReactNode
16
- ref?: Ref<HTMLDivElement>
17
- }
18
18
 
19
- export function Area({
20
- width = 120,
21
- height = 120,
22
- color,
23
- children,
24
- className,
25
- style,
26
- ref,
27
- ...props
28
- }: XYPadAreaProps &
29
- Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>) {
30
- const { areaRef } = useXYPadContext()
31
-
32
- // The area is what the pointer position is normalized against, so the
33
- // context ref is composed with any ref the caller passed.
34
- const composedRef = useComposedRefs<HTMLDivElement>(ref, areaRef)
35
-
36
- return (
37
- <div
38
- ref={composedRef}
39
- className={clsx('tremolo-xy-pad-area', className)}
40
- style={{
41
- ...{ '--color': color },
42
- width: width,
43
- height: height,
44
- ...style,
45
- }}
46
- {...props}
47
- >
48
- {children}
49
- </div>
50
- )
19
+ style?: CSSProperties & CSSVariables<'width' | 'height' | 'color'>
51
20
  }
21
+
22
+ type Props = XYPadAreaProps &
23
+ Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>
24
+
25
+ export const Area = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
26
+ function Area(
27
+ { width, height, color, children, className, style, ...props },
28
+ forwardedRef,
29
+ ) {
30
+ const { areaRef } = useXYPadContext()
31
+
32
+ // The area is what the pointer position is normalized against, so the
33
+ // context ref is composed with any ref the caller passed.
34
+ const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, areaRef)
35
+
36
+ return (
37
+ <div
38
+ ref={composedRef}
39
+ className={className}
40
+ style={
41
+ {
42
+ '--color': color,
43
+ '--width': cssLength(width),
44
+ '--height': cssLength(height),
45
+ // The thumb inside is placed against this box.
46
+ position: 'relative',
47
+ ...style,
48
+ } as CSSProperties
49
+ }
50
+ {...props}
51
+ >
52
+ <Placement name="XYPad.Area">{children}</Placement>
53
+ </div>
54
+ )
55
+ },
56
+ )