@tremolo-ui/react 0.6.0 → 0.8.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 (57) hide show
  1. package/dist/index.cjs +522 -997
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +601 -329
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +601 -329
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +487 -953
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/src/components/DropZone/index.tsx +101 -0
  11. package/src/components/FileInput/Trigger.tsx +39 -0
  12. package/src/components/FileInput/context.tsx +24 -0
  13. package/src/components/FileInput/index.tsx +139 -0
  14. package/src/components/Knob/ActiveLine.tsx +5 -3
  15. package/src/components/Knob/InactiveLine.tsx +6 -4
  16. package/src/components/Knob/SVGRoot.tsx +3 -3
  17. package/src/components/Knob/Thumb.tsx +3 -2
  18. package/src/components/Knob/context.tsx +3 -74
  19. package/src/components/Knob/index.tsx +27 -21
  20. package/src/components/NumberInput/DecrementStepper.tsx +2 -13
  21. package/src/components/NumberInput/IncrementStepper.tsx +2 -14
  22. package/src/components/NumberInput/InputField.tsx +17 -38
  23. package/src/components/NumberInput/Stepper.tsx +44 -107
  24. package/src/components/NumberInput/StepperButton.tsx +16 -12
  25. package/src/components/NumberInput/context.tsx +8 -8
  26. package/src/components/NumberInput/index.tsx +41 -81
  27. package/src/components/Piano/index.tsx +39 -149
  28. package/src/components/PointsEditor/Background.tsx +2 -8
  29. package/src/components/PointsEditor/Container.tsx +17 -34
  30. package/src/components/PointsEditor/Point.tsx +41 -86
  31. package/src/components/PointsEditor/SelectionBox.tsx +2 -8
  32. package/src/components/PointsEditor/context.tsx +10 -61
  33. package/src/components/PointsEditor/index.tsx +31 -315
  34. package/src/components/Slider/Marks.tsx +8 -13
  35. package/src/components/Slider/MarksOption.tsx +11 -17
  36. package/src/components/Slider/Thumb.tsx +9 -15
  37. package/src/components/Slider/Track.tsx +7 -13
  38. package/src/components/Slider/index.tsx +33 -39
  39. package/src/components/XYPad/Area.tsx +26 -53
  40. package/src/components/XYPad/Thumb.tsx +8 -12
  41. package/src/components/XYPad/context.tsx +1 -3
  42. package/src/components/XYPad/index.tsx +35 -50
  43. package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
  44. package/src/css-variables.ts +23 -0
  45. package/src/hooks/_internal/useCheckSteps.ts +9 -147
  46. package/src/hooks/useDrag.ts +13 -4
  47. package/src/hooks/useDragValue.ts +17 -2
  48. package/src/hooks/useDropZone.ts +78 -0
  49. package/src/hooks/useLongPress.ts +35 -51
  50. package/src/hooks/useMIDIAccess.ts +0 -8
  51. package/src/hooks/useMIDIInput.ts +0 -3
  52. package/src/hooks/useWheel.ts +4 -1
  53. package/src/index.ts +14 -36
  54. package/src/components/Piano/keyboard-shortcuts.ts +0 -61
  55. package/src/components/Slider/type.ts +0 -44
  56. package/src/components/_util/css-length.ts +0 -13
  57. package/src/input-event.ts +0 -32
@@ -1,11 +1,9 @@
1
1
  import {
2
- AriaAttributes,
3
2
  ComponentPropsWithoutRef,
4
- CSSProperties,
5
3
  forwardRef,
6
- ReactNode,
7
4
  useImperativeHandle,
8
5
  useRef,
6
+ CSSProperties,
9
7
  } from 'react'
10
8
 
11
9
  import { useCheckPlacement } from '../_util/Placement'
@@ -13,6 +11,8 @@ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
13
11
 
14
12
  import { useSliderContext } from './context'
15
13
 
14
+ import type { CSSVariables } from '../../css-variables'
15
+
16
16
  export interface SliderThumbProps {
17
17
  /**
18
18
  * Sets `--color`, for the theme to colour the thumb with. The size is not
@@ -21,18 +21,7 @@ export interface SliderThumbProps {
21
21
  */
22
22
  color?: string
23
23
 
24
- className?: string
25
- style?: CSSProperties
26
- /**
27
- * Rendered inside the thumb. The thumb is one element either way, so what
28
- * is passed here is decoration on top of it rather than a replacement for
29
- * it — `className` and `style` are how its own appearance is changed.
30
- */
31
- children?: ReactNode
32
- 'aria-label'?: AriaAttributes['aria-label']
33
- 'aria-labelledby'?: AriaAttributes['aria-labelledby']
34
- 'aria-describedby'?: AriaAttributes['aria-describedby']
35
- 'aria-valuetext'?: AriaAttributes['aria-valuetext']
24
+ style?: CSSProperties & CSSVariables<'color' | 'translate'>
36
25
  }
37
26
 
38
27
  export interface SliderThumbMethods {
@@ -50,6 +39,11 @@ export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
50
39
  children,
51
40
  className,
52
41
  style,
42
+ // Pulled out of `props` so that they land on the range input rather than
43
+ // on the thumb: the input is the control, it takes the focus, and a name
44
+ // on a plain div would be ignored. No declaration in `SliderThumbProps`
45
+ // is needed for this — they are already on the div attributes it is
46
+ // merged with, with the same types.
53
47
  'aria-label': ariaLabel,
54
48
  'aria-labelledby': ariaLabelledby,
55
49
  'aria-describedby': ariaDescribedby,
@@ -1,18 +1,14 @@
1
- import {
2
- ComponentPropsWithoutRef,
3
- CSSProperties,
4
- forwardRef,
5
- ReactNode,
6
- } from 'react'
1
+ import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
7
2
 
8
- import { xor } from '@tremolo-ui/functions'
3
+ import { cssLength } from '@tremolo-ui/dom'
9
4
 
10
5
  import { useComposedRefs } from '../../compose-refs'
11
- import { cssLength } from '../_util/css-length'
12
6
  import { Placement } from '../_util/Placement'
13
7
 
14
8
  import { useSliderContext } from './context'
15
9
 
10
+ import type { CSSVariables } from '../../css-variables'
11
+
16
12
  export interface SliderTrackProps {
17
13
  /**
18
14
  * How long the track is along the axis the slider runs. Sets `--length`;
@@ -27,10 +23,8 @@ export interface SliderTrackProps {
27
23
  /** Colour of the rest of the track. Sets `--inactive`. */
28
24
  inactive?: string
29
25
 
30
- className?: string
31
- style?: CSSProperties
32
- /** `<Slider.Thumb />` goes here. */
33
- children?: ReactNode
26
+ style?: CSSProperties &
27
+ CSSVariables<'length' | 'thickness' | 'active' | 'inactive' | 'percent'>
34
28
  }
35
29
 
36
30
  type Props = SliderTrackProps &
@@ -65,7 +59,7 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
65
59
  data-orientation={vertical ? 'vertical' : 'horizontal'}
66
60
  // Which end the value grows from. `percent` is already the position on
67
61
  // screen, so this only says which side of it is the filled one.
68
- data-flipped={xor(vertical, reverse) ? '' : undefined}
62
+ data-flipped={vertical !== reverse ? '' : undefined}
69
63
  style={
70
64
  {
71
65
  '--active': active,
@@ -9,27 +9,26 @@ import React, {
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
- linearScale,
16
- toFixed,
14
+ arrowKeyDirection,
15
+ type AxisOptions,
16
+ DEFAULT_DRAG_SENSITIVITY,
17
+ DEFAULT_KEYBOARD_OPTIONS,
18
+ DEFAULT_WHEEL_OPTIONS,
17
19
  type InputEventOption,
18
20
  type ModifierValue,
19
21
  selectModifier,
20
- xor,
21
- type Scale,
22
- } from '@tremolo-ui/functions'
22
+ valuePercent,
23
+ wheelDirection,
24
+ type XY,
25
+ } from '@tremolo-ui/dom'
26
+ import { linearScale, type Scale } from '@tremolo-ui/functions'
23
27
 
24
28
  import { useComposedRefs } from '../../compose-refs'
25
29
  import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
26
30
  import { useDragValue } from '../../hooks/useDragValue'
27
31
  import { useWheel } from '../../hooks/useWheel'
28
- import {
29
- DEFAULT_DRAG_SENSITIVITY,
30
- DEFAULT_KEYBOARD_OPTIONS,
31
- DEFAULT_WHEEL_OPTIONS,
32
- } from '../../input-event'
33
32
 
34
33
  import { SliderProvider } from './context'
35
34
  import { Marks } from './Marks'
@@ -147,8 +146,6 @@ export interface SliderProps {
147
146
  * The parts carry `data-readonly` while it is set.
148
147
  */
149
148
  readonly?: boolean
150
- className?: string
151
- style?: CSSProperties
152
149
  /** Called with the new value when a drag, the wheel or an arrow key moves it. */
153
150
  onChange?: (value: number) => void
154
151
  /** Called when a drag starts, with the value where the track was pressed. */
@@ -220,18 +217,13 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
220
217
  const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
221
218
  const inactive = disabled || readonly
222
219
 
223
- const p = toFixed(scale.normalize(value, min, max) * 100)
224
- const rev = toFixed(100 - p)
225
- // NOTE
220
+ // Measured from the left or the top, as CSS places things:
226
221
  // normal -> normal (right)
227
- // vertical -> rev (up)
228
- // reverse -> rev (left)
222
+ // vertical -> reversed (up)
223
+ // reverse -> reversed (left)
229
224
  // vertical & reverse -> normal (down)
230
- const displayReversed = useMemo(
231
- () => xor(vertical, reverse),
232
- [vertical, reverse],
233
- )
234
- const percent = displayReversed ? rev : p
225
+ const displayReversed = vertical !== reverse
226
+ const percent = valuePercent(value, { min, max, scale }, displayReversed)
235
227
 
236
228
  // --- internal functions ---
237
229
  // The pointer is normalized against the track on both axes; only the one
@@ -246,14 +238,19 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
246
238
 
247
239
  const handleKeyDown = useCallback(
248
240
  (event: React.KeyboardEvent<HTMLDivElement>) => {
249
- const key = event.key
250
- if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
251
- return
241
+ const direction = arrowKeyDirection(event.key)
242
+ if (direction === null) return
252
243
  event.preventDefault()
253
244
  if (!keyboard || !onChange || inactive) return
254
- let direction = key === 'ArrowRight' || key === 'ArrowUp' ? 1 : -1
255
- if (reverse) direction *= -1
256
- onChange(applyDelta(value, direction, keyboard, axis, event))
245
+ onChange(
246
+ applyDelta(
247
+ value,
248
+ reverse ? -direction : direction,
249
+ keyboard,
250
+ axis,
251
+ event,
252
+ ),
253
+ )
257
254
  },
258
255
  [keyboard, onChange, inactive, reverse, value, axis],
259
256
  )
@@ -286,16 +283,14 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
286
283
 
287
284
  const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
288
285
  if (!wheel || !onChange || inactive) return
286
+ // A notch the slider does not read — a sideways scroll on a vertical
287
+ // slider — is left to the page rather than swallowed.
288
+ const direction = wheelDirection(event, { horizontal: !vertical })
289
+ if (direction === null) return
289
290
  event.preventDefault()
290
- let direction
291
- if (!vertical && event.deltaX !== 0) {
292
- direction = event.deltaX > 0 ? 1 : -1
293
- } else {
294
- if (event.deltaY === 0) return
295
- direction = event.deltaY > 0 ? -1 : 1
296
- }
297
- if (reverse) direction *= -1
298
- onChange(applyDelta(value, direction, wheel, axis, event))
291
+ onChange(
292
+ applyDelta(value, reverse ? -direction : direction, wheel, axis, event),
293
+ )
299
294
  }, WHEEL_OPTIONS)
300
295
 
301
296
  // Composed once, so React attaches the refs a single time instead of
@@ -401,4 +396,3 @@ export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
401
396
  export { type SliderTrackProps } from './Track'
402
397
  export { type SliderMarksProps } from './Marks'
403
398
  export { type SliderMarksOptionProps } from './MarksOption'
404
- export { type MarksOptions } from './type'
@@ -1,61 +1,34 @@
1
- import {
2
- ComponentPropsWithoutRef,
3
- CSSProperties,
4
- forwardRef,
5
- ReactNode,
6
- } from 'react'
1
+ import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
7
2
 
8
3
  import { useComposedRefs } from '../../compose-refs'
9
- import { cssLength } from '../_util/css-length'
10
4
  import { Placement } from '../_util/Placement'
11
5
 
12
6
  import { useXYPadContext } from './context'
13
7
 
14
- export interface XYPadAreaProps {
15
- /** Sets `--width`; the size the theme gives it stands when omitted. */
16
- width?: number | string
17
- /** Sets `--height`; the size the theme gives it stands when omitted. */
18
- height?: number | string
19
- /** Sets `--color`, for the theme to colour the area with. */
20
- color?: string
21
- className?: string
22
- style?: CSSProperties
23
- /** `<XYPad.Thumb />` goes here. */
24
- children?: ReactNode
25
- }
8
+ export const Area = /* @__PURE__ */ forwardRef<
9
+ HTMLDivElement,
10
+ ComponentPropsWithoutRef<'div'>
11
+ >(function Area({ children, className, style, ...props }, forwardedRef) {
12
+ const { areaRef } = useXYPadContext()
26
13
 
27
- type Props = XYPadAreaProps &
28
- Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>
14
+ // The area is what the pointer position is normalized against, so the
15
+ // context ref is composed with any ref the caller passed.
16
+ const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, areaRef)
29
17
 
30
- export const Area = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
31
- function Area(
32
- { width, height, color, children, className, style, ...props },
33
- forwardedRef,
34
- ) {
35
- const { areaRef } = useXYPadContext()
36
-
37
- // The area is what the pointer position is normalized against, so the
38
- // context ref is composed with any ref the caller passed.
39
- const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, areaRef)
40
-
41
- return (
42
- <div
43
- ref={composedRef}
44
- className={className}
45
- style={
46
- {
47
- '--color': color,
48
- '--width': cssLength(width),
49
- '--height': cssLength(height),
50
- // The thumb inside is placed against this box.
51
- position: 'relative',
52
- ...style,
53
- } as CSSProperties
54
- }
55
- {...props}
56
- >
57
- <Placement name="XYPad.Area">{children}</Placement>
58
- </div>
59
- )
60
- },
61
- )
18
+ return (
19
+ <div
20
+ ref={composedRef}
21
+ className={className}
22
+ style={
23
+ {
24
+ // The thumb inside is placed against this box.
25
+ position: 'relative',
26
+ ...style,
27
+ } as CSSProperties
28
+ }
29
+ {...props}
30
+ >
31
+ <Placement name="XYPad.Area">{children}</Placement>
32
+ </div>
33
+ )
34
+ })
@@ -1,17 +1,20 @@
1
1
  import {
2
2
  AriaAttributes,
3
3
  ComponentPropsWithoutRef,
4
- CSSProperties,
5
4
  forwardRef,
6
- ReactNode,
7
5
  useImperativeHandle,
8
6
  useRef,
7
+ CSSProperties,
9
8
  } from 'react'
10
9
 
10
+ import { toXY, type XYInput } from '@tremolo-ui/dom'
11
+
11
12
  import { useCheckPlacement } from '../_util/Placement'
12
13
  import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
13
14
 
14
- import { toXY, useXYPadContext, type XYInput } from './context'
15
+ import { useXYPadContext } from './context'
16
+
17
+ import type { CSSVariables } from '../../css-variables'
15
18
 
16
19
  export interface XYPadThumbProps {
17
20
  /**
@@ -21,15 +24,6 @@ export interface XYPadThumbProps {
21
24
  */
22
25
  color?: string
23
26
 
24
- className?: string
25
- style?: CSSProperties
26
- /**
27
- * Rendered inside the thumb. The thumb is one element either way, so what
28
- * is passed here is decoration on top of it rather than a replacement for
29
- * it — `className` and `style` are how its own appearance is changed.
30
- */
31
- children?: ReactNode
32
-
33
27
  /**
34
28
  * The accessible name of each axis, as `[x, y]`. There are two range inputs
35
29
  * inside the thumb, so each takes a name of its own; a single string names
@@ -45,6 +39,8 @@ export interface XYPadThumbProps {
45
39
  * `[x, y]` or one for both.
46
40
  */
47
41
  'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
42
+
43
+ style?: CSSProperties & CSSVariables<'color' | 'translate'>
48
44
  }
49
45
 
50
46
  export interface XYPadThumbMethods {
@@ -1,12 +1,10 @@
1
1
  import { createContext, RefObject, useContext } from 'react'
2
2
 
3
- import { toXY, type XY, type XYInput } from '@tremolo-ui/dom'
3
+ import { type XY } from '@tremolo-ui/dom'
4
4
  import type { Scale } from '@tremolo-ui/functions'
5
5
 
6
6
  import type { XYPadThumbMethods } from './Thumb'
7
7
 
8
- export { toXY, type XY, type XYInput }
9
-
10
8
  export type XYPadContextValue = {
11
9
  value: XY<number>
12
10
  min: XY<number>
@@ -10,30 +10,32 @@ import {
10
10
  useRef,
11
11
  } from 'react'
12
12
 
13
- import type { AxisOptions } from '@tremolo-ui/dom'
14
13
  import {
15
14
  applyDelta,
16
- linearScale,
17
- toFixed,
15
+ arrowKeyMove,
16
+ type AxisOptions,
17
+ DEFAULT_DRAG_SENSITIVITY,
18
+ DEFAULT_KEYBOARD_OPTIONS,
19
+ DEFAULT_WHEEL_OPTIONS,
18
20
  InputEventOption,
19
21
  ModifierState,
20
22
  type ModifierValue,
21
23
  selectModifier,
22
- type Scale,
23
- } from '@tremolo-ui/functions'
24
+ toXY,
25
+ valuePercent,
26
+ wheelMove,
27
+ type XY,
28
+ type XYInput,
29
+ } from '@tremolo-ui/dom'
30
+ import { linearScale, type Scale } from '@tremolo-ui/functions'
24
31
 
25
32
  import { useComposedRefs } from '../../compose-refs'
26
33
  import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
27
34
  import { useDragValue } from '../../hooks/useDragValue'
28
35
  import { useWheel } from '../../hooks/useWheel'
29
- import {
30
- DEFAULT_DRAG_SENSITIVITY,
31
- DEFAULT_KEYBOARD_OPTIONS,
32
- DEFAULT_WHEEL_OPTIONS,
33
- } from '../../input-event'
34
36
 
35
37
  import { Area } from './Area'
36
- import { toXY, XY, XYInput, XYPadProvider } from './context'
38
+ import { XYPadProvider } from './context'
37
39
  import { Thumb, XYPadThumbMethods } from './Thumb'
38
40
 
39
41
  const defaultExternalStyles: XYPadProps['externalStyles'] = {
@@ -229,14 +231,17 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
229
231
  const scale = useMemo(() => toXY(_scale), [_scale])
230
232
  const reverse = useMemo(() => toXY(_reverse), [_reverse])
231
233
 
232
- const percent = useMemo((): XY<number> => {
233
- const normalized = [0, 1].map((i) =>
234
- scale[i].normalize(value[i], min[i], max[i]),
235
- )
236
- return [0, 1].map((i) =>
237
- toFixed((reverse[i] ? 1 - normalized[i] : normalized[i]) * 100),
238
- ) as XY<number>
239
- }, [value, min, max, scale, reverse])
234
+ const percent = useMemo(
235
+ (): XY<number> =>
236
+ [0, 1].map((i) =>
237
+ valuePercent(
238
+ value[i],
239
+ { min: min[i], max: max[i], scale: scale[i] },
240
+ reverse[i],
241
+ ),
242
+ ) as XY<number>,
243
+ [value, min, max, scale, reverse],
244
+ )
240
245
 
241
246
  // --- internal functions ---
242
247
  // `AxisOptions` extends `ValueRange`, so the same pair also describes the
@@ -293,21 +298,15 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
293
298
 
294
299
  const handleKeyDown = useCallback(
295
300
  (event: React.KeyboardEvent<HTMLDivElement>) => {
296
- const key = event.key
297
- if (!['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown'].includes(key))
298
- return
299
-
300
301
  // The key picks the axis, whichever of the two inputs holds the
301
- // focus: the pad is one control to the person moving it, and the focus
302
- // lands on the x input, so reading the axis off the input would leave
303
- // the y axis with no keys at all.
304
- const i: 0 | 1 = key === 'ArrowRight' || key === 'ArrowLeft' ? 0 : 1
302
+ // focus: the focus lands on the x input, so reading the axis off the
303
+ // input would leave the y axis with no keys at all.
304
+ const move = arrowKeyMove(event.key)
305
+ if (!move) return
305
306
  event.preventDefault()
306
307
  if (!onChange || inactive || !keyboard) return
307
- let direction = 1
308
- if (key === 'ArrowLeft' || key === 'ArrowUp') direction *= -1
309
- if (reverse[i]) direction *= -1
310
- onChange(nudge(i, direction, keyboard, event))
308
+ const { axis: i, direction } = move
309
+ onChange(nudge(i, reverse[i] ? -direction : direction, keyboard, event))
311
310
  },
312
311
  [onChange, inactive, keyboard, reverse, nudge],
313
312
  )
@@ -339,19 +338,11 @@ export const Root = /* @__PURE__ */ forwardRef<XYPadMethods, Props>(
339
338
 
340
339
  const wheelRefCallback = useWheel<HTMLDivElement>((event) => {
341
340
  if (!onChange || inactive || !wheel) return
342
- // Browsers turn shift+wheel into horizontal scrolling: `deltaY` comes
343
- // out empty and `deltaX` carries the movement. Reading whichever axis
344
- // moved keeps shift working as the x-axis modifier — and picks up a
345
- // trackpad's own horizontal gesture, which never had a modifier.
346
- const horizontal = event.deltaX !== 0
347
- const delta = horizontal ? event.deltaX : event.deltaY
348
- if (delta === 0) return
349
- const i: 0 | 1 = horizontal || event.shiftKey ? 0 : 1
341
+ const move = wheelMove(event)
342
+ if (!move) return
350
343
  event.preventDefault()
351
- let direction = 1
352
- if (delta < 0) direction *= -1
353
- if (reverse[i]) direction *= -1
354
- onChange(nudge(i, direction, wheel, event))
344
+ const { axis: i, direction } = move
345
+ onChange(nudge(i, reverse[i] ? -direction : direction, wheel, event))
355
346
  }, WHEEL_OPTIONS)
356
347
 
357
348
  // Composed once, so React attaches the refs a single time instead of
@@ -446,10 +437,4 @@ export const XYPad = {
446
437
  }
447
438
 
448
439
  export { type XYPadThumbProps, type XYPadThumbMethods } from './Thumb'
449
- export { type XYPadAreaProps } from './Area'
450
- export {
451
- useXYPadContext,
452
- type XYPadContextValue,
453
- type XY,
454
- type XYInput,
455
- } from './context'
440
+ export { useXYPadContext, type XYPadContextValue } from './context'
@@ -1,18 +1,6 @@
1
- import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
1
+ import { ComponentPropsWithoutRef, forwardRef } from 'react'
2
2
 
3
- const hiddenStyle: CSSProperties = {
4
- position: 'absolute',
5
- width: 1,
6
- height: 1,
7
- padding: 0,
8
- margin: -1,
9
- overflow: 'hidden',
10
- clip: 'rect(0, 0, 0, 0)',
11
- clipPath: 'inset(50%)',
12
- whiteSpace: 'nowrap',
13
- border: 0,
14
- pointerEvents: 'none',
15
- }
3
+ import { visuallyHiddenStyle } from '@tremolo-ui/dom'
16
4
 
17
5
  type Props = Omit<ComponentPropsWithoutRef<'input'>, 'type' | 'style'>
18
6
 
@@ -20,5 +8,5 @@ export const VisuallyHiddenRangeInput = /* @__PURE__ */ forwardRef<
20
8
  HTMLInputElement,
21
9
  Props
22
10
  >((props, ref) => (
23
- <input {...props} ref={ref} type="range" style={hiddenStyle} />
11
+ <input {...props} ref={ref} type="range" style={visuallyHiddenStyle} />
24
12
  ))
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CSS custom properties, for a `style` object.
3
+ *
4
+ * React's `CSSProperties` has no key starting with `--`, so
5
+ * `style={{ '--color': 'red' }}` is refused as an unknown property even though
6
+ * the browser takes it. Every part accepts this alongside `CSSProperties`,
7
+ * which is how a variable the component or a theme reads is set inline.
8
+ *
9
+ * `T` names the custom properties the part itself writes or reads, without the
10
+ * leading `--`, so that an editor can suggest them. Any other `--` name is
11
+ * still accepted — a theme reads variables of its own, such as the colours of
12
+ * each kind of key.
13
+ *
14
+ * @example
15
+ * CSSVariables<'thickness' | 'length'>
16
+ * // { '--thickness'?: string | number, '--length'?: string | number,
17
+ * // [name: `--${string}`]: string | number | undefined }
18
+ */
19
+ export type CSSVariables<T extends string = never> = {
20
+ [K in T as `--${K}`]?: string | number
21
+ } & {
22
+ [K in `--${string}`]?: string | number
23
+ }