@tremolo-ui/react 0.6.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 (48) hide show
  1. package/dist/index.cjs +252 -58
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +550 -133
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +550 -133
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +218 -39
  8. package/dist/index.js.map +1 -1
  9. package/package.json +4 -4
  10. package/src/components/DropZone/index.tsx +100 -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 +144 -0
  14. package/src/components/Knob/index.tsx +8 -5
  15. package/src/components/NumberInput/DecrementStepper.tsx +2 -13
  16. package/src/components/NumberInput/IncrementStepper.tsx +2 -14
  17. package/src/components/NumberInput/InputField.tsx +5 -11
  18. package/src/components/NumberInput/Stepper.tsx +4 -19
  19. package/src/components/NumberInput/StepperButton.tsx +13 -8
  20. package/src/components/NumberInput/context.tsx +6 -7
  21. package/src/components/NumberInput/index.tsx +4 -13
  22. package/src/components/Piano/index.tsx +10 -13
  23. package/src/components/PointsEditor/Background.tsx +2 -8
  24. package/src/components/PointsEditor/Container.tsx +2 -8
  25. package/src/components/PointsEditor/Point.tsx +7 -5
  26. package/src/components/PointsEditor/SelectionBox.tsx +2 -8
  27. package/src/components/PointsEditor/context.tsx +6 -6
  28. package/src/components/PointsEditor/index.tsx +9 -11
  29. package/src/components/Slider/Marks.tsx +6 -9
  30. package/src/components/Slider/MarksOption.tsx +7 -2
  31. package/src/components/Slider/Thumb.tsx +9 -15
  32. package/src/components/Slider/Track.tsx +6 -13
  33. package/src/components/Slider/index.tsx +5 -12
  34. package/src/components/Slider/type.ts +4 -2
  35. package/src/components/XYPad/Area.tsx +5 -10
  36. package/src/components/XYPad/Thumb.tsx +5 -11
  37. package/src/components/XYPad/index.tsx +3 -5
  38. package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
  39. package/src/components/_util/decimal-digits.ts +23 -0
  40. package/src/components/_util/visually-hidden.ts +25 -0
  41. package/src/css-variables.ts +23 -0
  42. package/src/hooks/_internal/useCheckSteps.ts +2 -3
  43. package/src/hooks/useDrag.ts +13 -4
  44. package/src/hooks/useDragValue.ts +17 -2
  45. package/src/hooks/useDropZone.ts +78 -0
  46. package/src/hooks/useWheel.ts +4 -1
  47. package/src/index.ts +12 -9
  48. package/src/input-event.ts +2 -2
@@ -1,4 +1,4 @@
1
- import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'
1
+ import { ComponentPropsWithoutRef, forwardRef } from 'react'
2
2
 
3
3
  import { useComposedRefs } from '../../compose-refs'
4
4
  import { useDragValue } from '../../hooks/useDragValue'
@@ -8,13 +8,7 @@ import { Placement } from '../_util/Placement'
8
8
  import { usePointsEditorContext } from './context'
9
9
  import { AXIS } from './Point'
10
10
 
11
- export interface PointsEditorContainerProps {
12
- /** `<PointsEditor.Point />` goes here. */
13
- children?: ReactNode
14
- }
15
-
16
- type Props = PointsEditorContainerProps &
17
- Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorContainerProps>
11
+ type Props = ComponentPropsWithoutRef<'div'>
18
12
 
19
13
  export const Container = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
20
14
  function Container({ children, className, style, ...props }, forwardedRef) {
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  ComponentPropsWithoutRef,
3
3
  CSSProperties,
4
- ReactNode,
5
4
  useCallback,
6
5
  useEffect,
7
6
  useId,
@@ -11,12 +10,12 @@ import {
11
10
 
12
11
  import {
13
12
  applyDelta,
14
- clamp,
15
13
  type InputEventOption,
16
14
  type ModifierState,
17
15
  type ModifierValue,
18
16
  selectModifier,
19
- } from '@tremolo-ui/functions'
17
+ } from '@tremolo-ui/dom'
18
+ import { clamp } from '@tremolo-ui/functions'
20
19
 
21
20
  import { useComposedRefs } from '../../compose-refs'
22
21
  import { useDragValue } from '../../hooks/useDragValue'
@@ -26,6 +25,8 @@ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
26
25
 
27
26
  import { type PointRegistration, usePointsEditorContext } from './context'
28
27
 
28
+ import type { CSSVariables } from '../../css-variables'
29
+
29
30
  export type PointBaseType = { x: number; y: number }
30
31
 
31
32
  export function clampPoint(
@@ -45,8 +46,6 @@ export interface PointsEditorPointProps<T extends PointBaseType> {
45
46
  * growing downwards.
46
47
  */
47
48
  value: T
48
- /** Drawn inside the point: the theme's own dot stands when it is left out. */
49
- children?: ReactNode
50
49
  /**
51
50
  * How the selection refers to this point. One is generated when it is left
52
51
  * out, which lasts as long as the point is mounted — give your own if the
@@ -94,6 +93,9 @@ export interface PointsEditorPointProps<T extends PointBaseType> {
94
93
  onDragStart?: (value: PointBaseType) => void
95
94
  /** Called when that drag ends, with where the point is. */
96
95
  onDragEnd?: (value: PointBaseType) => void
96
+
97
+ style?: CSSProperties &
98
+ CSSVariables<'width' | 'height' | 'color' | 'translate'>
97
99
  }
98
100
 
99
101
  /**
@@ -1,14 +1,9 @@
1
- import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'
1
+ import { ComponentPropsWithoutRef, CSSProperties } from 'react'
2
2
 
3
3
  import { useCheckPlacement } from '../_util/Placement'
4
4
 
5
5
  import { usePointsEditorContext } from './context'
6
6
 
7
- export interface PointsEditorSelectionBoxProps {
8
- /** Drawn inside the box, which is only there while a drag is running. */
9
- children?: ReactNode
10
- }
11
-
12
7
  /**
13
8
  * The box a drag on empty space draws, and what it covers is selected.
14
9
  *
@@ -20,8 +15,7 @@ export function SelectionBox({
20
15
  className,
21
16
  style,
22
17
  ...props
23
- }: PointsEditorSelectionBoxProps &
24
- Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorSelectionBoxProps>) {
18
+ }: ComponentPropsWithoutRef<'div'>) {
25
19
  useCheckPlacement('PointsEditor.SelectionBox', 'PointsEditor.Container')
26
20
 
27
21
  const selectionBox = usePointsEditorContext((s) => s.selectionBox)
@@ -1,11 +1,11 @@
1
1
  import { createContext, CSSProperties, RefObject, useContext } from 'react'
2
2
 
3
- import type { SelectionBoxRect } from '@tremolo-ui/dom'
4
- import type {
5
- InputEventOption,
6
- ModifierState,
7
- ModifierValue,
8
- } from '@tremolo-ui/functions'
3
+ import {
4
+ type InputEventOption,
5
+ type ModifierState,
6
+ type ModifierValue,
7
+ type SelectionBoxRect,
8
+ } from '@tremolo-ui/dom'
9
9
 
10
10
  import type { PointBaseType } from './Point'
11
11
 
@@ -12,19 +12,16 @@ import {
12
12
  } from 'react'
13
13
 
14
14
  import {
15
+ applyDelta,
15
16
  createSelectionBox,
17
+ type InputEventOption,
18
+ type ModifierState,
19
+ type ModifierValue,
16
20
  type SelectionBoxInstance,
17
21
  type SelectionBoxRect,
18
22
  type XY,
19
23
  } from '@tremolo-ui/dom'
20
- import {
21
- applyDelta,
22
- clamp,
23
- type InputEventOption,
24
- type ModifierState,
25
- type ModifierValue,
26
- toPrecision,
27
- } from '@tremolo-ui/functions'
24
+ import { clamp, toPrecision } from '@tremolo-ui/functions'
28
25
 
29
26
  import { DEFAULT_DRAG_SENSITIVITY } from '../../input-event'
30
27
  import { cssLength } from '../_util/css-length'
@@ -35,6 +32,8 @@ import { type PointRegistration, PointsEditorProvider } from './context'
35
32
  import { AXIS, Point, type PointBaseType } from './Point'
36
33
  import { SelectionBox } from './SelectionBox'
37
34
 
35
+ import type { CSSVariables } from '../../css-variables'
36
+
38
37
  /** One array for every editor with selection turned off, so memos hold still. */
39
38
  const EMPTY: readonly string[] = []
40
39
 
@@ -213,6 +212,8 @@ export interface PointsEditorProps {
213
212
  * </PointsEditor.Root>
214
213
  */
215
214
  children: ReactNode
215
+
216
+ style?: CSSProperties & CSSVariables<'width' | 'height'>
216
217
  }
217
218
 
218
219
  type Props = PointsEditorProps &
@@ -554,9 +555,6 @@ export {
554
555
  usePointsEditorContext,
555
556
  type PointsEditorContextValue,
556
557
  } from './context'
557
- export { type PointsEditorBackgroundProps } from './Background'
558
- export { type PointsEditorContainerProps } from './Container'
559
- export { type PointsEditorSelectionBoxProps } from './SelectionBox'
560
558
  export {
561
559
  clampPoint,
562
560
  type PointBaseType,
@@ -1,6 +1,4 @@
1
- import { ComponentPropsWithoutRef, CSSProperties, ReactNode } from 'react'
2
-
3
- import { xor } from '@tremolo-ui/functions'
1
+ import { ComponentPropsWithoutRef, CSSProperties } from 'react'
4
2
 
5
3
  import { cssLength } from '../_util/css-length'
6
4
  import { Placement } from '../_util/Placement'
@@ -9,6 +7,8 @@ import { useSliderContext } from './context'
9
7
  import { MarksOption } from './MarksOption'
10
8
  import { generateOptionsList, MarksOptions } from './type'
11
9
 
10
+ import type { CSSVariables } from '../../css-variables'
11
+
12
12
  export interface SliderMarksProps {
13
13
  /**
14
14
  * Space between the marks and the track. Sets `--gap`; the theme's own
@@ -24,11 +24,8 @@ export interface SliderMarksProps {
24
24
  * `'step'` makes one per step, which is a great many for a fine `step`.
25
25
  */
26
26
  options?: MarksOptions
27
- /**
28
- * `<Slider.MarksOption />` goes here, one per mark. Ignored while `options`
29
- * is set.
30
- */
31
- children?: ReactNode
27
+
28
+ style?: CSSProperties & CSSVariables<'gap'>
32
29
  }
33
30
 
34
31
  export function Marks({
@@ -49,7 +46,7 @@ export function Marks({
49
46
  const optionsList = options
50
47
  ? generateOptionsList(options, min, max, step)
51
48
  : []
52
- if (xor(vertical, reverse)) optionsList.reverse()
49
+ if (vertical !== reverse) optionsList.reverse()
53
50
 
54
51
  return (
55
52
  <div
@@ -1,12 +1,14 @@
1
1
  import { ComponentPropsWithoutRef, CSSProperties, useCallback } from 'react'
2
2
 
3
- import { toFixed, xor } from '@tremolo-ui/functions'
3
+ import { toFixed } from '@tremolo-ui/functions'
4
4
 
5
5
  import { cssLength } from '../_util/css-length'
6
6
  import { useCheckPlacement } from '../_util/Placement'
7
7
 
8
8
  import { useSliderContext } from './context'
9
9
 
10
+ import type { CSSVariables } from '../../css-variables'
11
+
10
12
  export interface SliderMarksOptionProps {
11
13
  /** Where the mark sits, on the same scale as the thumb. */
12
14
  value: number
@@ -39,6 +41,9 @@ export interface SliderMarksOptionProps {
39
41
  mark?: CSSProperties
40
42
  label?: CSSProperties
41
43
  }
44
+
45
+ style?: CSSProperties &
46
+ CSSVariables<'thickness' | 'length' | 'gap' | 'label-width' | 'translate'>
42
47
  }
43
48
 
44
49
  export function MarksOption({
@@ -68,7 +73,7 @@ export function MarksOption({
68
73
  (value: number) => {
69
74
  // The marks have to sit on the same curve the thumb runs along.
70
75
  const percent = scale.normalize(value, min, max) * 100
71
- return toFixed(xor(vertical, reverse) ? 100 - percent : percent)
76
+ return toFixed(vertical !== reverse ? 100 - percent : percent)
72
77
  },
73
78
  [vertical, reverse, max, min, scale],
74
79
  )
@@ -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,11 +1,4 @@
1
- import {
2
- ComponentPropsWithoutRef,
3
- CSSProperties,
4
- forwardRef,
5
- ReactNode,
6
- } from 'react'
7
-
8
- import { xor } from '@tremolo-ui/functions'
1
+ import { ComponentPropsWithoutRef, CSSProperties, forwardRef } from 'react'
9
2
 
10
3
  import { useComposedRefs } from '../../compose-refs'
11
4
  import { cssLength } from '../_util/css-length'
@@ -13,6 +6,8 @@ import { Placement } from '../_util/Placement'
13
6
 
14
7
  import { useSliderContext } from './context'
15
8
 
9
+ import type { CSSVariables } from '../../css-variables'
10
+
16
11
  export interface SliderTrackProps {
17
12
  /**
18
13
  * How long the track is along the axis the slider runs. Sets `--length`;
@@ -27,10 +22,8 @@ export interface SliderTrackProps {
27
22
  /** Colour of the rest of the track. Sets `--inactive`. */
28
23
  inactive?: string
29
24
 
30
- className?: string
31
- style?: CSSProperties
32
- /** `<Slider.Thumb />` goes here. */
33
- children?: ReactNode
25
+ style?: CSSProperties &
26
+ CSSVariables<'length' | 'thickness' | 'active' | 'inactive' | 'percent'>
34
27
  }
35
28
 
36
29
  type Props = SliderTrackProps &
@@ -65,7 +58,7 @@ export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
65
58
  data-orientation={vertical ? 'vertical' : 'horizontal'}
66
59
  // Which end the value grows from. `percent` is already the position on
67
60
  // screen, so this only says which side of it is the filled one.
68
- data-flipped={xor(vertical, reverse) ? '' : undefined}
61
+ data-flipped={vertical !== reverse ? '' : undefined}
69
62
  style={
70
63
  {
71
64
  '--active': active,
@@ -9,17 +9,15 @@ 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
+ type AxisOptions,
17
15
  type InputEventOption,
18
16
  type ModifierValue,
19
17
  selectModifier,
20
- xor,
21
- type Scale,
22
- } from '@tremolo-ui/functions'
18
+ type XY,
19
+ } from '@tremolo-ui/dom'
20
+ import { linearScale, type Scale, toFixed } from '@tremolo-ui/functions'
23
21
 
24
22
  import { useComposedRefs } from '../../compose-refs'
25
23
  import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
@@ -147,8 +145,6 @@ export interface SliderProps {
147
145
  * The parts carry `data-readonly` while it is set.
148
146
  */
149
147
  readonly?: boolean
150
- className?: string
151
- style?: CSSProperties
152
148
  /** Called with the new value when a drag, the wheel or an arrow key moves it. */
153
149
  onChange?: (value: number) => void
154
150
  /** Called when a drag starts, with the value where the track was pressed. */
@@ -227,10 +223,7 @@ export const Root = /* @__PURE__ */ forwardRef<SliderMethods, Props>(
227
223
  // vertical -> rev (up)
228
224
  // reverse -> rev (left)
229
225
  // vertical & reverse -> normal (down)
230
- const displayReversed = useMemo(
231
- () => xor(vertical, reverse),
232
- [vertical, reverse],
233
- )
226
+ const displayReversed = vertical !== reverse
234
227
  const percent = displayReversed ? rev : p
235
228
 
236
229
  // --- internal functions ---
@@ -1,4 +1,6 @@
1
- import { decimalPart, toFixed } from '@tremolo-ui/functions'
1
+ import { toFixed } from '@tremolo-ui/functions'
2
+
3
+ import { decimalDigits } from '../_util/decimal-digits'
2
4
 
3
5
  /**
4
6
  * How `Slider.Marks` fills itself in when it is given no children: one option
@@ -36,7 +38,7 @@ export function generateOptionsList(
36
38
  for (let i = 0; i < count; i++) {
37
39
  const value = toFixed(
38
40
  interval * (Math.ceil(min / interval) + i),
39
- decimalPart(interval)?.length,
41
+ decimalDigits(interval),
40
42
  )
41
43
  optionsList.push({ value: value, mark: mark, label: label })
42
44
  }
@@ -1,9 +1,4 @@
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
4
  import { cssLength } from '../_util/css-length'
@@ -11,6 +6,8 @@ import { Placement } from '../_util/Placement'
11
6
 
12
7
  import { useXYPadContext } from './context'
13
8
 
9
+ import type { CSSVariables } from '../../css-variables'
10
+
14
11
  export interface XYPadAreaProps {
15
12
  /** Sets `--width`; the size the theme gives it stands when omitted. */
16
13
  width?: number | string
@@ -18,10 +15,8 @@ export interface XYPadAreaProps {
18
15
  height?: number | string
19
16
  /** Sets `--color`, for the theme to colour the area with. */
20
17
  color?: string
21
- className?: string
22
- style?: CSSProperties
23
- /** `<XYPad.Thumb />` goes here. */
24
- children?: ReactNode
18
+
19
+ style?: CSSProperties & CSSVariables<'width' | 'height' | 'color'>
25
20
  }
26
21
 
27
22
  type Props = XYPadAreaProps &
@@ -1,11 +1,10 @@
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
 
11
10
  import { useCheckPlacement } from '../_util/Placement'
@@ -13,6 +12,8 @@ import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
13
12
 
14
13
  import { toXY, useXYPadContext, type XYInput } from './context'
15
14
 
15
+ import type { CSSVariables } from '../../css-variables'
16
+
16
17
  export interface XYPadThumbProps {
17
18
  /**
18
19
  * Sets `--color`, for the theme to colour the thumb with. The size is not
@@ -21,15 +22,6 @@ export interface XYPadThumbProps {
21
22
  */
22
23
  color?: string
23
24
 
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
25
  /**
34
26
  * The accessible name of each axis, as `[x, y]`. There are two range inputs
35
27
  * inside the thumb, so each takes a name of its own; a single string names
@@ -45,6 +37,8 @@ export interface XYPadThumbProps {
45
37
  * `[x, y]` or one for both.
46
38
  */
47
39
  'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
40
+
41
+ style?: CSSProperties & CSSVariables<'color' | 'translate'>
48
42
  }
49
43
 
50
44
  export interface XYPadThumbMethods {
@@ -10,17 +10,15 @@ 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
+ type AxisOptions,
18
16
  InputEventOption,
19
17
  ModifierState,
20
18
  type ModifierValue,
21
19
  selectModifier,
22
- type Scale,
23
- } from '@tremolo-ui/functions'
20
+ } from '@tremolo-ui/dom'
21
+ import { linearScale, type Scale, toFixed } from '@tremolo-ui/functions'
24
22
 
25
23
  import { useComposedRefs } from '../../compose-refs'
26
24
  import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
@@ -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 './visually-hidden'
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
+ * How many digits a number carries after the decimal point.
3
+ *
4
+ * `Slider.Marks` builds its labels by multiplying an interval, which leaves
5
+ * binary debris in the last digits (`0.1 * 3` is `0.30000000000000004`). The
6
+ * count is what tells `toFixed` how far to round that back.
7
+ *
8
+ * The exponent form has to be handled separately: `String(1e-7)` is `'1e-7'`,
9
+ * which has no decimal point at all, so reading the text after the point
10
+ * reports no digits and the interval rounds away to whole numbers.
11
+ */
12
+ export function decimalDigits(x: number): number {
13
+ if (!Number.isFinite(x)) return 0
14
+
15
+ const text = String(x)
16
+ const e = text.indexOf('e')
17
+ if (e === -1) return text.split('.')[1]?.length ?? 0
18
+
19
+ const fraction = text.slice(0, e).split('.')[1]?.length ?? 0
20
+ // A negative exponent pushes the point further right, a positive one pulls
21
+ // it back past the digits that are there.
22
+ return Math.max(0, fraction - Number(text.slice(e + 1)))
23
+ }
@@ -0,0 +1,25 @@
1
+ import type { CSSProperties } from 'react'
2
+
3
+ /**
4
+ * Take an element out of sight while leaving it in the accessibility tree and
5
+ * in the tab order.
6
+ *
7
+ * `display: none` and `visibility: hidden` would remove it from both, and the
8
+ * native control underneath a headless component is what carries the ARIA and
9
+ * the keyboard behaviour. `pointer-events: none` is safe because nothing is
10
+ * ever clicked here directly: a `<label>` forwards its click, and a drag is
11
+ * handled by the part that is visible.
12
+ */
13
+ export const visuallyHiddenStyle: CSSProperties = {
14
+ position: 'absolute',
15
+ width: 1,
16
+ height: 1,
17
+ padding: 0,
18
+ margin: -1,
19
+ overflow: 'hidden',
20
+ clip: 'rect(0, 0, 0, 0)',
21
+ clipPath: 'inset(50%)',
22
+ whiteSpace: 'nowrap',
23
+ border: 0,
24
+ pointerEvents: 'none',
25
+ }
@@ -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
+ }
@@ -2,12 +2,11 @@ import { useEffect } from 'react'
2
2
 
3
3
  import {
4
4
  applyDelta,
5
- linearScale,
6
5
  type InputEventOption,
7
6
  type ModifierState,
8
7
  type ModifierValue,
9
- type ValueRange,
10
- } from '@tremolo-ui/functions'
8
+ } from '@tremolo-ui/dom'
9
+ import { linearScale, type ValueRange } from '@tremolo-ui/functions'
11
10
 
12
11
  /** Positions probed across the travel. The ends are left out so that the
13
12
  * clamp at `min` and `max` cannot be mistaken for a press that does nothing. */
@@ -4,10 +4,14 @@ import { createDrag, type DragState } from '@tremolo-ui/dom'
4
4
 
5
5
  import { useCallbackRef } from './_internal/useCallbackRef'
6
6
 
7
- interface UseDragProps {
7
+ export interface UseDragOptions {
8
8
  /**
9
- * Threshold at which the onDrag event fires.
10
- * Prevents onDrag events from firing, for example, when double-clicking.
9
+ * Pixels the pointer has to travel before the drag counts as one.
10
+ *
11
+ * A click moves the pointer by a pixel or two, so without a threshold a
12
+ * double click reports a drag between the two presses.
13
+ *
14
+ * @default 1
11
15
  */
12
16
  threshold?: number
13
17
 
@@ -24,6 +28,9 @@ interface UseDragProps {
24
28
  pointerLock?: boolean
25
29
 
26
30
  /**
31
+ * Called on every move once the drag has started, with the pointer position
32
+ * relative to the element and how far it moved since the last call.
33
+ *
27
34
  * @param state the whole drag, for anything the four numbers leave out —
28
35
  * the pointer event and its modifier keys, most of all.
29
36
  */
@@ -34,7 +41,9 @@ interface UseDragProps {
34
41
  deltaY: number,
35
42
  state: DragState,
36
43
  ) => void
44
+ /** Called once the pointer has moved past `threshold`, not on pointerdown. */
37
45
  onDragStart?: (state: DragState) => void
46
+ /** Called when the pointer is released, only if the drag ever started. */
38
47
  onDragEnd?: (state: DragState) => void
39
48
  }
40
49
 
@@ -50,7 +59,7 @@ export function useDrag<T extends Element>({
50
59
  onDrag,
51
60
  onDragStart,
52
61
  onDragEnd,
53
- }: UseDragProps): (node: T | null) => void {
62
+ }: UseDragOptions): (node: T | null) => void {
54
63
  const dragHandler = useCallbackRef(onDrag)
55
64
  const dragStartHandler = useCallbackRef(onDragStart)
56
65
  const dragEndHandler = useCallbackRef(onDragEnd)