@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.
- package/dist/compose-refs.cjs +72 -0
- package/dist/compose-refs.cjs.map +1 -0
- package/dist/compose-refs.d.cts +43 -0
- package/dist/compose-refs.d.cts.map +1 -0
- package/dist/compose-refs.d.ts +43 -0
- package/dist/compose-refs.d.ts.map +1 -0
- package/dist/compose-refs.js +70 -0
- package/dist/compose-refs.js.map +1 -0
- package/dist/index.cjs +2272 -1650
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1461 -475
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1461 -475
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2258 -1648
- package/dist/index.js.map +1 -1
- package/package.json +32 -31
- package/src/components/AnimationCanvas/index.tsx +162 -194
- package/src/components/Knob/ActiveLine.tsx +9 -8
- package/src/components/Knob/InactiveLine.tsx +11 -14
- package/src/components/Knob/SVGRoot.tsx +10 -17
- package/src/components/Knob/Thumb.tsx +33 -11
- package/src/components/Knob/context.tsx +73 -89
- package/src/components/Knob/index.tsx +227 -105
- package/src/components/NumberInput/DecrementStepper.tsx +21 -50
- package/src/components/NumberInput/IncrementStepper.tsx +22 -50
- package/src/components/NumberInput/InputField.tsx +206 -0
- package/src/components/NumberInput/Stepper.tsx +150 -21
- package/src/components/NumberInput/StepperButton.tsx +95 -0
- package/src/components/NumberInput/context.tsx +104 -149
- package/src/components/NumberInput/index.tsx +461 -110
- package/src/components/Piano/index.tsx +413 -263
- package/src/components/Piano/keyboard-shortcuts.ts +61 -0
- package/src/components/PointsEditor/Background.tsx +19 -4
- package/src/components/PointsEditor/Container.tsx +92 -22
- package/src/components/PointsEditor/Point.tsx +293 -53
- package/src/components/PointsEditor/SelectionBox.tsx +54 -0
- package/src/components/PointsEditor/context.tsx +97 -75
- package/src/components/PointsEditor/index.tsx +497 -62
- package/src/components/Slider/Marks.tsx +84 -0
- package/src/components/Slider/MarksOption.tsx +115 -0
- package/src/components/Slider/Thumb.tsx +99 -54
- package/src/components/Slider/Track.tsx +73 -62
- package/src/components/Slider/context.tsx +40 -53
- package/src/components/Slider/index.tsx +237 -190
- package/src/components/Slider/type.ts +27 -9
- package/src/components/XYPad/Area.tsx +52 -33
- package/src/components/XYPad/Thumb.tsx +121 -60
- package/src/components/XYPad/context.tsx +49 -0
- package/src/components/XYPad/index.tsx +338 -246
- package/src/components/_util/Placement.tsx +68 -0
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
- package/src/components/_util/css-length.ts +13 -0
- package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
- package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
- package/src/hooks/_internal/useCheckSteps.ts +170 -0
- package/src/hooks/useAnimationFrame.ts +17 -7
- package/src/hooks/useDrag.ts +39 -9
- package/src/hooks/useDragValue.ts +194 -0
- package/src/hooks/useEventListener.ts +11 -7
- package/src/hooks/useLongPress.ts +42 -8
- package/src/hooks/useMIDIAccess.ts +82 -11
- package/src/hooks/useMIDIInput.ts +45 -13
- package/src/hooks/useMIDIMessage.ts +23 -5
- package/src/hooks/useWheel.ts +29 -9
- package/src/index.ts +50 -27
- package/src/input-event.ts +32 -0
- package/dist/index.css +0 -536
- package/src/Globals.d.ts +0 -3
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/Knob/index.css +0 -77
- package/src/components/NumberInput/InternalInput.tsx +0 -216
- package/src/components/NumberInput/index.css +0 -185
- package/src/components/NumberInput/type.ts +0 -58
- package/src/components/Piano/KeyLabel.tsx +0 -51
- package/src/components/Piano/context.tsx +0 -73
- package/src/components/Piano/index.css +0 -90
- package/src/components/Piano/key.tsx +0 -205
- package/src/components/Piano/keyboardShortcuts.ts +0 -31
- package/src/components/PointsEditor/index.css +0 -30
- package/src/components/Slider/Scale.tsx +0 -52
- package/src/components/Slider/ScaleOption.tsx +0 -103
- package/src/components/Slider/index.css +0 -112
- package/src/components/XYPad/index.css +0 -48
- package/src/components/_util/index.ts +0 -28
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -100
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- package/src/styles/global.css +0 -19
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, CSSProperties, useCallback } from 'react'
|
|
2
|
+
|
|
3
|
+
import { toFixed, xor } from '@tremolo-ui/functions'
|
|
4
|
+
|
|
5
|
+
import { cssLength } from '../_util/css-length'
|
|
6
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
7
|
+
|
|
8
|
+
import { useSliderContext } from './context'
|
|
9
|
+
|
|
10
|
+
export interface SliderMarksOptionProps {
|
|
11
|
+
/** Where the mark sits, on the same scale as the thumb. */
|
|
12
|
+
value: number
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Draw the mark itself. Turn it off to show the label alone.
|
|
16
|
+
* @default true
|
|
17
|
+
*/
|
|
18
|
+
mark?: boolean
|
|
19
|
+
/**
|
|
20
|
+
* Text shown next to the mark. Leave it out to show the value; `null` leaves
|
|
21
|
+
* the label out, and an empty string draws an empty one.
|
|
22
|
+
*/
|
|
23
|
+
label?: number | string | null
|
|
24
|
+
/** Thickness of the mark. Sets `--thickness`. */
|
|
25
|
+
thickness?: number | string
|
|
26
|
+
/** Length of the mark. Sets `--length`. */
|
|
27
|
+
length?: number | string
|
|
28
|
+
/** Space between the mark and the label. Sets `--gap`. */
|
|
29
|
+
gap?: number | string
|
|
30
|
+
/** Width of the label. Sets `--label-width`. */
|
|
31
|
+
labelWidth?: number | string
|
|
32
|
+
/** Classes for the mark and the label inside the option. */
|
|
33
|
+
classes?: {
|
|
34
|
+
mark?: string
|
|
35
|
+
label?: string
|
|
36
|
+
}
|
|
37
|
+
/** Styles for the mark and the label inside the option. */
|
|
38
|
+
styles?: {
|
|
39
|
+
mark?: CSSProperties
|
|
40
|
+
label?: CSSProperties
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function MarksOption({
|
|
45
|
+
value,
|
|
46
|
+
mark = true,
|
|
47
|
+
label,
|
|
48
|
+
thickness,
|
|
49
|
+
length,
|
|
50
|
+
gap,
|
|
51
|
+
labelWidth,
|
|
52
|
+
classes,
|
|
53
|
+
styles,
|
|
54
|
+
className,
|
|
55
|
+
style,
|
|
56
|
+
...props
|
|
57
|
+
}: SliderMarksOptionProps &
|
|
58
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderMarksOptionProps>) {
|
|
59
|
+
useCheckPlacement('Slider.MarksOption', 'Slider.Marks')
|
|
60
|
+
|
|
61
|
+
const min = useSliderContext((s) => s.min)
|
|
62
|
+
const max = useSliderContext((s) => s.max)
|
|
63
|
+
const scale = useSliderContext((s) => s.scale)
|
|
64
|
+
const vertical = useSliderContext((s) => s.vertical)
|
|
65
|
+
const reverse = useSliderContext((s) => s.reverse)
|
|
66
|
+
|
|
67
|
+
const calcPercent = useCallback(
|
|
68
|
+
(value: number) => {
|
|
69
|
+
// The marks have to sit on the same curve the thumb runs along.
|
|
70
|
+
const percent = scale.normalize(value, min, max) * 100
|
|
71
|
+
return toFixed(xor(vertical, reverse) ? 100 - percent : percent)
|
|
72
|
+
},
|
|
73
|
+
[vertical, reverse, max, min, scale],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
className={className}
|
|
79
|
+
style={
|
|
80
|
+
{
|
|
81
|
+
// The mark and the label read these, so they are set once here.
|
|
82
|
+
'--thickness': cssLength(thickness),
|
|
83
|
+
'--length': cssLength(length),
|
|
84
|
+
'--gap': cssLength(gap),
|
|
85
|
+
'--label-width': cssLength(labelWidth),
|
|
86
|
+
// The mechanics of the position below, as on the thumb.
|
|
87
|
+
position: 'absolute',
|
|
88
|
+
translate: vertical
|
|
89
|
+
? 'var(--translate, 0 -50%)'
|
|
90
|
+
: 'var(--translate, -50% 0)',
|
|
91
|
+
zIndex: 10,
|
|
92
|
+
...style,
|
|
93
|
+
// Where the mark belongs on the track: the value, not a style.
|
|
94
|
+
left: !vertical ? `${calcPercent(value)}%` : undefined,
|
|
95
|
+
top: vertical ? `${calcPercent(value)}%` : undefined,
|
|
96
|
+
} as CSSProperties
|
|
97
|
+
}
|
|
98
|
+
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
99
|
+
{...props}
|
|
100
|
+
>
|
|
101
|
+
{mark && (
|
|
102
|
+
<div
|
|
103
|
+
className={classes?.mark}
|
|
104
|
+
style={styles?.mark}
|
|
105
|
+
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
106
|
+
></div>
|
|
107
|
+
)}
|
|
108
|
+
{label !== null && (
|
|
109
|
+
<div className={classes?.label} style={styles?.label}>
|
|
110
|
+
{label ?? value}
|
|
111
|
+
</div>
|
|
112
|
+
)}
|
|
113
|
+
</div>
|
|
114
|
+
)
|
|
115
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import {
|
|
2
|
+
AriaAttributes,
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
3
4
|
CSSProperties,
|
|
4
5
|
forwardRef,
|
|
5
6
|
ReactNode,
|
|
@@ -7,21 +8,31 @@ import {
|
|
|
7
8
|
useRef,
|
|
8
9
|
} from 'react'
|
|
9
10
|
|
|
11
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
12
|
+
import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
13
|
+
|
|
10
14
|
import { useSliderContext } from './context'
|
|
11
15
|
|
|
12
16
|
export interface SliderThumbProps {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Sets `--color`, for the theme to colour the thumb with. The size is not
|
|
19
|
+
* set here but by `--thumb-size` on `Slider.Root`, so that the root can
|
|
20
|
+
* leave the matching room around the track.
|
|
21
|
+
*/
|
|
17
22
|
color?: string
|
|
18
23
|
|
|
19
24
|
className?: string
|
|
20
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
|
+
*/
|
|
21
31
|
children?: ReactNode
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
32
|
+
'aria-label'?: AriaAttributes['aria-label']
|
|
33
|
+
'aria-labelledby'?: AriaAttributes['aria-labelledby']
|
|
34
|
+
'aria-describedby'?: AriaAttributes['aria-describedby']
|
|
35
|
+
'aria-valuetext'?: AriaAttributes['aria-valuetext']
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
export interface SliderThumbMethods {
|
|
@@ -29,65 +40,99 @@ export interface SliderThumbMethods {
|
|
|
29
40
|
blur: () => void
|
|
30
41
|
}
|
|
31
42
|
|
|
32
|
-
|
|
43
|
+
type Props = SliderThumbProps &
|
|
44
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderThumbProps>
|
|
33
45
|
|
|
34
|
-
export const Thumb = forwardRef<SliderThumbMethods,
|
|
35
|
-
(
|
|
46
|
+
export const Thumb = /* @__PURE__ */ forwardRef<SliderThumbMethods, Props>(
|
|
47
|
+
function Thumb(
|
|
36
48
|
{
|
|
37
|
-
size,
|
|
38
|
-
width = defaultThumbSize,
|
|
39
|
-
height = defaultThumbSize,
|
|
40
49
|
color,
|
|
41
50
|
children,
|
|
42
51
|
className,
|
|
43
52
|
style,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
53
|
+
'aria-label': ariaLabel,
|
|
54
|
+
'aria-labelledby': ariaLabelledby,
|
|
55
|
+
'aria-describedby': ariaDescribedby,
|
|
56
|
+
'aria-valuetext': ariaValuetext,
|
|
57
|
+
...props
|
|
58
|
+
},
|
|
59
|
+
forwardedRef,
|
|
60
|
+
) {
|
|
61
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
|
62
|
+
const {
|
|
63
|
+
value,
|
|
64
|
+
min,
|
|
65
|
+
max,
|
|
66
|
+
step,
|
|
67
|
+
vertical,
|
|
68
|
+
disabled,
|
|
69
|
+
readonly,
|
|
70
|
+
onChange,
|
|
71
|
+
percent,
|
|
72
|
+
thumbRef,
|
|
73
|
+
} = useSliderContext()
|
|
74
|
+
|
|
75
|
+
// The thumb is positioned against the track.
|
|
76
|
+
useCheckPlacement('Slider.Thumb', 'Slider.Track')
|
|
77
|
+
|
|
78
|
+
const methods = () => ({
|
|
79
|
+
focus() {
|
|
80
|
+
if (!disabled) inputRef.current?.focus()
|
|
81
|
+
},
|
|
82
|
+
blur() {
|
|
83
|
+
inputRef.current?.blur()
|
|
84
|
+
},
|
|
85
|
+
})
|
|
52
86
|
|
|
53
|
-
useImperativeHandle(
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
wrapperRef.current?.focus()
|
|
57
|
-
},
|
|
58
|
-
blur() {
|
|
59
|
-
wrapperRef.current?.blur()
|
|
60
|
-
},
|
|
61
|
-
}
|
|
62
|
-
}, [])
|
|
87
|
+
useImperativeHandle(forwardedRef, methods, [disabled])
|
|
88
|
+
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
89
|
+
useImperativeHandle(thumbRef, methods, [disabled])
|
|
63
90
|
|
|
64
91
|
return (
|
|
65
92
|
<div
|
|
66
|
-
className=
|
|
93
|
+
className={className}
|
|
94
|
+
data-disabled={disabled ? '' : undefined}
|
|
95
|
+
data-readonly={readonly ? '' : undefined}
|
|
96
|
+
{...props}
|
|
67
97
|
style={{
|
|
68
|
-
|
|
69
|
-
|
|
98
|
+
...{ '--color': color },
|
|
99
|
+
// Not a style but the mechanics of the position below: the thumb is
|
|
100
|
+
// placed by a percentage of the track, which needs it out of flow
|
|
101
|
+
// and measured from its own centre. `--translate` is there for a
|
|
102
|
+
// thumb that should hang off its edge instead.
|
|
103
|
+
position: 'absolute',
|
|
104
|
+
translate: 'var(--translate, -50% -50%)',
|
|
105
|
+
zIndex: 100,
|
|
106
|
+
...style,
|
|
107
|
+
// Where the thumb sits is the component's decision, not a style: a
|
|
108
|
+
// `left` from the caller would take it off the track, so it is
|
|
109
|
+
// written after theirs.
|
|
110
|
+
top: vertical ? `${percent}%` : '50%',
|
|
111
|
+
left: !vertical ? `${percent}%` : '50%',
|
|
70
112
|
}}
|
|
71
113
|
>
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
114
|
+
<VisuallyHiddenRangeInput
|
|
115
|
+
ref={inputRef}
|
|
116
|
+
value={value}
|
|
117
|
+
min={min}
|
|
118
|
+
max={max}
|
|
119
|
+
step={step}
|
|
120
|
+
disabled={disabled}
|
|
121
|
+
aria-readonly={readonly}
|
|
122
|
+
aria-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
123
|
+
aria-label={ariaLabel}
|
|
124
|
+
aria-labelledby={ariaLabelledby}
|
|
125
|
+
aria-describedby={ariaDescribedby}
|
|
126
|
+
aria-valuetext={ariaValuetext}
|
|
127
|
+
onChange={(event) => {
|
|
128
|
+
if (readonly) {
|
|
129
|
+
event.currentTarget.value = String(value)
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
onChange?.(event.currentTarget.valueAsNumber)
|
|
133
|
+
}}
|
|
134
|
+
/>
|
|
135
|
+
{children}
|
|
91
136
|
</div>
|
|
92
137
|
)
|
|
93
138
|
},
|
|
@@ -1,79 +1,90 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
4
|
+
forwardRef,
|
|
5
|
+
ReactNode,
|
|
6
|
+
} from 'react'
|
|
3
7
|
|
|
4
|
-
import {
|
|
8
|
+
import { xor } from '@tremolo-ui/functions'
|
|
5
9
|
|
|
6
|
-
import {
|
|
10
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
11
|
+
import { cssLength } from '../_util/css-length'
|
|
12
|
+
import { Placement } from '../_util/Placement'
|
|
7
13
|
|
|
8
|
-
|
|
9
|
-
export const defaultThickness = 10
|
|
14
|
+
import { useSliderContext } from './context'
|
|
10
15
|
|
|
11
16
|
export interface SliderTrackProps {
|
|
17
|
+
/**
|
|
18
|
+
* How long the track is along the axis the slider runs. Sets `--length`;
|
|
19
|
+
* the size the theme gives it stands when this is omitted.
|
|
20
|
+
*/
|
|
12
21
|
length?: number | string
|
|
22
|
+
/** How thick the track is across that axis. Sets `--thickness`. */
|
|
13
23
|
thickness?: number | string
|
|
14
24
|
|
|
25
|
+
/** Colour of the part from `min` to the value. Sets `--active`. */
|
|
15
26
|
active?: string
|
|
27
|
+
/** Colour of the rest of the track. Sets `--inactive`. */
|
|
16
28
|
inactive?: string
|
|
17
29
|
|
|
18
|
-
defaultStyle?: boolean
|
|
19
|
-
|
|
20
30
|
className?: string
|
|
21
31
|
style?: CSSProperties
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/** @internal */
|
|
25
|
-
__thumb?: ReactElement
|
|
26
|
-
/** @internal */
|
|
27
|
-
__percent?: number
|
|
32
|
+
/** `<Slider.Thumb />` goes here. */
|
|
33
|
+
children?: ReactNode
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
thickness = defaultThickness,
|
|
33
|
-
active,
|
|
34
|
-
inactive,
|
|
35
|
-
children,
|
|
36
|
-
className,
|
|
37
|
-
style,
|
|
38
|
-
defaultStyle = true,
|
|
39
|
-
__thumb,
|
|
40
|
-
__percent = 0,
|
|
41
|
-
...props
|
|
42
|
-
}: SliderTrackProps &
|
|
43
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderTrackProps>) {
|
|
44
|
-
const vertical = useSliderContext((s) => s.vertical)
|
|
45
|
-
const reverse = useSliderContext((s) => s.reverse)
|
|
46
|
-
const disabled = useSliderContext((s) => s.disabled)
|
|
36
|
+
type Props = SliderTrackProps &
|
|
37
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof SliderTrackProps>
|
|
47
38
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
39
|
+
export const Track = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
40
|
+
function Track(
|
|
41
|
+
{
|
|
42
|
+
length,
|
|
43
|
+
thickness,
|
|
44
|
+
active,
|
|
45
|
+
inactive,
|
|
46
|
+
children,
|
|
47
|
+
className,
|
|
48
|
+
style,
|
|
49
|
+
...props
|
|
50
|
+
},
|
|
51
|
+
forwardedRef,
|
|
52
|
+
) {
|
|
53
|
+
const { vertical, reverse, disabled, percent, trackRef } =
|
|
54
|
+
useSliderContext()
|
|
53
55
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
56
|
+
// The track is what the pointer position is normalized against, so the
|
|
57
|
+
// context ref is composed with any ref the caller passed.
|
|
58
|
+
const composedRef = useComposedRefs<HTMLDivElement>(forwardedRef, trackRef)
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
ref={composedRef}
|
|
63
|
+
className={className}
|
|
64
|
+
data-disabled={disabled ? '' : undefined}
|
|
65
|
+
data-orientation={vertical ? 'vertical' : 'horizontal'}
|
|
66
|
+
// Which end the value grows from. `percent` is already the position on
|
|
67
|
+
// screen, so this only says which side of it is the filled one.
|
|
68
|
+
data-flipped={xor(vertical, reverse) ? '' : undefined}
|
|
69
|
+
style={
|
|
70
|
+
{
|
|
71
|
+
'--active': active,
|
|
72
|
+
'--inactive': inactive,
|
|
73
|
+
'--length': cssLength(length),
|
|
74
|
+
'--thickness': cssLength(thickness),
|
|
75
|
+
// Where the value sits, for the theme to paint the fill with. The
|
|
76
|
+
// component draws nothing itself: this is the one number CSS cannot
|
|
77
|
+
// work out on its own.
|
|
78
|
+
'--percent': `${percent}%`,
|
|
79
|
+
// The thumb inside is placed against this box.
|
|
80
|
+
position: 'relative',
|
|
81
|
+
...style,
|
|
82
|
+
} as CSSProperties
|
|
83
|
+
}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
<Placement name="Slider.Track">{children}</Placement>
|
|
87
|
+
</div>
|
|
88
|
+
)
|
|
89
|
+
},
|
|
90
|
+
)
|
|
@@ -1,64 +1,51 @@
|
|
|
1
|
-
import { createContext,
|
|
2
|
-
import { createStore, useStore } from 'zustand'
|
|
1
|
+
import { createContext, RefObject, useContext } from 'react'
|
|
3
2
|
|
|
4
|
-
type
|
|
3
|
+
import type { Scale } from '@tremolo-ui/functions'
|
|
4
|
+
|
|
5
|
+
import type { SliderThumbMethods } from './Thumb'
|
|
6
|
+
|
|
7
|
+
export type SliderContextValue = {
|
|
8
|
+
value: number
|
|
5
9
|
min: number
|
|
6
10
|
max: number
|
|
7
11
|
step: number
|
|
8
|
-
|
|
12
|
+
/** How the value is distributed across the travel. */
|
|
13
|
+
scale: Scale
|
|
9
14
|
vertical: boolean
|
|
10
15
|
reverse: boolean
|
|
11
16
|
disabled: boolean
|
|
12
17
|
readonly: boolean
|
|
18
|
+
onChange?: (value: number) => void
|
|
19
|
+
/**
|
|
20
|
+
* Position of the thumb, 0-100, already accounting for the display direction
|
|
21
|
+
* implied by `vertical` and `reverse`.
|
|
22
|
+
*/
|
|
23
|
+
percent: number
|
|
24
|
+
|
|
25
|
+
/** `Track` registers its element here; `Root` normalizes the pointer against it. */
|
|
26
|
+
trackRef: RefObject<HTMLDivElement | null>
|
|
27
|
+
/** `Thumb` registers itself here; `Root` focuses it when a drag starts. */
|
|
28
|
+
thumbRef: RefObject<SliderThumbMethods | null>
|
|
13
29
|
}
|
|
14
30
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const SliderContext = createContext<SliderStore | null>(null)
|
|
36
|
-
|
|
37
|
-
type SliderProviderProps = React.PropsWithChildren<Partial<State>>
|
|
38
|
-
|
|
39
|
-
export function SliderProvider({ children, ...props }: SliderProviderProps) {
|
|
40
|
-
const storeRef = useRef<SliderStore>(null)
|
|
41
|
-
if (!storeRef.current) {
|
|
42
|
-
storeRef.current = createSliderStore(props)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
useEffect(() => {
|
|
46
|
-
if (storeRef.current) {
|
|
47
|
-
storeRef.current.setState(props)
|
|
48
|
-
} else {
|
|
49
|
-
storeRef.current = createSliderStore(props)
|
|
50
|
-
}
|
|
51
|
-
}, [props])
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<SliderContext.Provider value={storeRef.current}>
|
|
55
|
-
{children}
|
|
56
|
-
</SliderContext.Provider>
|
|
57
|
-
)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function useSliderContext<T>(selector: (state: State) => T): T {
|
|
61
|
-
const store = useContext(SliderContext)
|
|
62
|
-
if (!store) throw new Error('Missing SliderContext.Provider in the tree')
|
|
63
|
-
return useStore(store, selector)
|
|
31
|
+
const SliderContext = /* @__PURE__ */ createContext<SliderContextValue | null>(
|
|
32
|
+
null,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export const SliderProvider = SliderContext.Provider
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Everything here is derived during render, so there is no state to keep in
|
|
39
|
+
* sync: `value` comes from the props of `Root` and the rest follows from it.
|
|
40
|
+
*/
|
|
41
|
+
export function useSliderContext(): SliderContextValue
|
|
42
|
+
export function useSliderContext<T>(
|
|
43
|
+
selector: (state: SliderContextValue) => T,
|
|
44
|
+
): T
|
|
45
|
+
export function useSliderContext<T>(
|
|
46
|
+
selector?: (state: SliderContextValue) => T,
|
|
47
|
+
) {
|
|
48
|
+
const context = useContext(SliderContext)
|
|
49
|
+
if (!context) throw new Error('Missing SliderContext.Provider in the tree')
|
|
50
|
+
return selector ? selector(context) : context
|
|
64
51
|
}
|