@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
|
@@ -1,42 +1,61 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import {
|
|
2
|
+
ComponentPropsWithoutRef,
|
|
3
|
+
CSSProperties,
|
|
4
|
+
forwardRef,
|
|
5
|
+
ReactNode,
|
|
6
|
+
} from 'react'
|
|
7
|
+
|
|
8
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
9
|
+
import { cssLength } from '../_util/css-length'
|
|
10
|
+
import { Placement } from '../_util/Placement'
|
|
11
|
+
|
|
12
|
+
import { useXYPadContext } from './context'
|
|
3
13
|
|
|
4
14
|
export interface XYPadAreaProps {
|
|
15
|
+
/** Sets `--width`; the size the theme gives it stands when omitted. */
|
|
5
16
|
width?: number | string
|
|
17
|
+
/** Sets `--height`; the size the theme gives it stands when omitted. */
|
|
6
18
|
height?: number | string
|
|
19
|
+
/** Sets `--color`, for the theme to colour the area with. */
|
|
7
20
|
color?: string
|
|
8
21
|
className?: string
|
|
9
22
|
style?: CSSProperties
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
/** inherit */
|
|
13
|
-
__thumb?: ReactElement
|
|
23
|
+
/** `<XYPad.Thumb />` goes here. */
|
|
24
|
+
children?: ReactNode
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
27
|
+
type Props = XYPadAreaProps &
|
|
28
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>
|
|
29
|
+
|
|
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
|
+
)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import clsx from 'clsx'
|
|
2
1
|
import {
|
|
2
|
+
AriaAttributes,
|
|
3
3
|
ComponentPropsWithoutRef,
|
|
4
4
|
CSSProperties,
|
|
5
5
|
forwardRef,
|
|
@@ -8,24 +8,43 @@ import {
|
|
|
8
8
|
useRef,
|
|
9
9
|
} from 'react'
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
width?: number | string
|
|
14
|
-
height?: number | string
|
|
11
|
+
import { useCheckPlacement } from '../_util/Placement'
|
|
12
|
+
import { VisuallyHiddenRangeInput } from '../_util/VisuallyHiddenRangeInput'
|
|
15
13
|
|
|
16
|
-
|
|
14
|
+
import { toXY, useXYPadContext, type XYInput } from './context'
|
|
17
15
|
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export interface XYPadThumbProps {
|
|
17
|
+
/**
|
|
18
|
+
* Sets `--color`, for the theme to colour the thumb with. The size is not
|
|
19
|
+
* set here but by `--thumb-size` on `XYPad.Root`, so that the root can leave
|
|
20
|
+
* the matching room around the area.
|
|
21
|
+
*/
|
|
22
|
+
color?: string
|
|
20
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
|
+
*/
|
|
21
31
|
children?: ReactNode
|
|
22
32
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The accessible name of each axis, as `[x, y]`. There are two range inputs
|
|
35
|
+
* inside the thumb, so each takes a name of its own; a single string names
|
|
36
|
+
* them both, which is rarely what you want.
|
|
37
|
+
*/
|
|
38
|
+
'aria-label'?: XYInput<AriaAttributes['aria-label']>
|
|
39
|
+
/** The ids of what labels each axis, as `[x, y]` or one for both. */
|
|
40
|
+
'aria-labelledby'?: XYInput<AriaAttributes['aria-labelledby']>
|
|
41
|
+
/** The ids of what describes each axis, as `[x, y]` or one for both. */
|
|
42
|
+
'aria-describedby'?: XYInput<AriaAttributes['aria-describedby']>
|
|
43
|
+
/**
|
|
44
|
+
* What the value of each axis means, when the number does not say it, as
|
|
45
|
+
* `[x, y]` or one for both.
|
|
46
|
+
*/
|
|
47
|
+
'aria-valuetext'?: XYInput<AriaAttributes['aria-valuetext']>
|
|
29
48
|
}
|
|
30
49
|
|
|
31
50
|
export interface XYPadThumbMethods {
|
|
@@ -36,66 +55,108 @@ export interface XYPadThumbMethods {
|
|
|
36
55
|
type Props = XYPadThumbProps &
|
|
37
56
|
Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadThumbProps>
|
|
38
57
|
|
|
39
|
-
export const Thumb = forwardRef<XYPadThumbMethods, Props>(
|
|
40
|
-
(
|
|
58
|
+
export const Thumb = /* @__PURE__ */ forwardRef<XYPadThumbMethods, Props>(
|
|
59
|
+
function Thumb(
|
|
41
60
|
{
|
|
42
|
-
size,
|
|
43
|
-
width = 22,
|
|
44
|
-
height = 22,
|
|
45
61
|
color,
|
|
46
62
|
children,
|
|
47
|
-
wrapperClassName,
|
|
48
|
-
wrapperStyle,
|
|
49
63
|
className,
|
|
50
64
|
style,
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
65
|
+
'aria-label': ariaLabel,
|
|
66
|
+
'aria-labelledby': ariaLabelledby,
|
|
67
|
+
'aria-describedby': ariaDescribedby,
|
|
68
|
+
'aria-valuetext': ariaValuetext,
|
|
54
69
|
...props
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
)
|
|
58
|
-
const
|
|
70
|
+
},
|
|
71
|
+
forwardedRef,
|
|
72
|
+
) {
|
|
73
|
+
const xInputRef = useRef<HTMLInputElement>(null)
|
|
74
|
+
const yInputRef = useRef<HTMLInputElement>(null)
|
|
75
|
+
const {
|
|
76
|
+
value,
|
|
77
|
+
min,
|
|
78
|
+
max,
|
|
79
|
+
step,
|
|
80
|
+
disabled,
|
|
81
|
+
readonly,
|
|
82
|
+
onChange,
|
|
83
|
+
percent,
|
|
84
|
+
thumbRef,
|
|
85
|
+
} = useXYPadContext()
|
|
86
|
+
|
|
87
|
+
// The thumb is positioned against the area.
|
|
88
|
+
useCheckPlacement('XYPad.Thumb', 'XYPad.Area')
|
|
59
89
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
90
|
+
// Per axis, since the thumb holds one input for each.
|
|
91
|
+
const labels = toXY(ariaLabel)
|
|
92
|
+
const labelledby = toXY(ariaLabelledby)
|
|
93
|
+
const describedby = toXY(ariaDescribedby)
|
|
94
|
+
const valueText = toXY(ariaValuetext)
|
|
95
|
+
|
|
96
|
+
const methods = () => ({
|
|
97
|
+
focus() {
|
|
98
|
+
if (!disabled) xInputRef.current?.focus()
|
|
99
|
+
},
|
|
100
|
+
blur() {
|
|
101
|
+
xInputRef.current?.blur()
|
|
102
|
+
yInputRef.current?.blur()
|
|
103
|
+
},
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
useImperativeHandle(forwardedRef, methods, [disabled])
|
|
107
|
+
// Root focuses the thumb when a drag starts, wherever the user placed it.
|
|
108
|
+
useImperativeHandle(thumbRef, methods, [disabled])
|
|
70
109
|
|
|
71
110
|
return (
|
|
72
111
|
<div
|
|
73
|
-
className={
|
|
112
|
+
className={className}
|
|
113
|
+
data-disabled={disabled ? '' : undefined}
|
|
114
|
+
data-readonly={readonly ? '' : undefined}
|
|
115
|
+
{...props}
|
|
74
116
|
style={{
|
|
75
|
-
...
|
|
76
|
-
|
|
117
|
+
...{ '--color': color },
|
|
118
|
+
// Not a style but the mechanics of the position below: the thumb is
|
|
119
|
+
// placed by a percentage of the area, which needs it out of flow and
|
|
120
|
+
// measured from its own centre.
|
|
121
|
+
position: 'absolute',
|
|
122
|
+
translate: 'var(--translate, -50% -50%)',
|
|
123
|
+
zIndex: 100,
|
|
124
|
+
...style,
|
|
125
|
+
// Where the thumb sits is the component's decision, not a style: a
|
|
126
|
+
// `left` from the caller would take it off the area, so it is written
|
|
127
|
+
// after theirs.
|
|
128
|
+
left: `${percent[0]}%`,
|
|
129
|
+
top: `${percent[1]}%`,
|
|
77
130
|
}}
|
|
78
|
-
{...props}
|
|
79
131
|
>
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
aria-readonly={
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
132
|
+
{([0, 1] as const).map((axis) => (
|
|
133
|
+
<VisuallyHiddenRangeInput
|
|
134
|
+
key={axis}
|
|
135
|
+
ref={axis === 0 ? xInputRef : yInputRef}
|
|
136
|
+
data-axis={axis === 0 ? 'x' : 'y'}
|
|
137
|
+
value={value[axis]}
|
|
138
|
+
min={min[axis]}
|
|
139
|
+
max={max[axis]}
|
|
140
|
+
step={step[axis]}
|
|
141
|
+
disabled={disabled}
|
|
142
|
+
aria-readonly={readonly}
|
|
143
|
+
aria-orientation={axis === 0 ? 'horizontal' : 'vertical'}
|
|
144
|
+
aria-label={labels[axis] ?? (axis === 0 ? 'x' : 'y')}
|
|
145
|
+
aria-labelledby={labelledby[axis]}
|
|
146
|
+
aria-describedby={describedby[axis]}
|
|
147
|
+
aria-valuetext={valueText[axis]}
|
|
148
|
+
onChange={(event) => {
|
|
149
|
+
if (readonly) {
|
|
150
|
+
event.currentTarget.value = String(value[axis])
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
const next = [...value] as [number, number]
|
|
154
|
+
next[axis] = event.currentTarget.valueAsNumber
|
|
155
|
+
onChange?.(next)
|
|
96
156
|
}}
|
|
97
|
-
|
|
98
|
-
)}
|
|
157
|
+
/>
|
|
158
|
+
))}
|
|
159
|
+
{children}
|
|
99
160
|
</div>
|
|
100
161
|
)
|
|
101
162
|
},
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createContext, RefObject, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
import { toXY, type XY, type XYInput } from '@tremolo-ui/dom'
|
|
4
|
+
import type { Scale } from '@tremolo-ui/functions'
|
|
5
|
+
|
|
6
|
+
import type { XYPadThumbMethods } from './Thumb'
|
|
7
|
+
|
|
8
|
+
export { toXY, type XY, type XYInput }
|
|
9
|
+
|
|
10
|
+
export type XYPadContextValue = {
|
|
11
|
+
value: XY<number>
|
|
12
|
+
min: XY<number>
|
|
13
|
+
max: XY<number>
|
|
14
|
+
step: XY<number>
|
|
15
|
+
/** How the value of each axis is distributed across the travel. */
|
|
16
|
+
scale: XY<Scale>
|
|
17
|
+
reverse: XY<boolean>
|
|
18
|
+
disabled: boolean
|
|
19
|
+
readonly: boolean
|
|
20
|
+
onChange?: (value: XY<number>) => void
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Position of the thumb, 0-100 per axis, already accounting for `reverse`.
|
|
24
|
+
*/
|
|
25
|
+
percent: XY<number>
|
|
26
|
+
|
|
27
|
+
/** `Area` registers its element here; `Root` normalizes the pointer against it. */
|
|
28
|
+
areaRef: RefObject<HTMLDivElement | null>
|
|
29
|
+
/** `Thumb` registers itself here; `Root` focuses it when a drag starts. */
|
|
30
|
+
thumbRef: RefObject<XYPadThumbMethods | null>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const XYPadContext = /* @__PURE__ */ createContext<XYPadContextValue | null>(
|
|
34
|
+
null,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
export const XYPadProvider = XYPadContext.Provider
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Everything here is derived during render, so there is no state to keep in
|
|
41
|
+
* sync: `value` comes from the props of `Root` and the rest follows from it.
|
|
42
|
+
*/
|
|
43
|
+
export function useXYPadContext(): XYPadContextValue
|
|
44
|
+
export function useXYPadContext<T>(selector: (state: XYPadContextValue) => T): T
|
|
45
|
+
export function useXYPadContext<T>(selector?: (state: XYPadContextValue) => T) {
|
|
46
|
+
const context = useContext(XYPadContext)
|
|
47
|
+
if (!context) throw new Error('Missing XYPadContext.Provider in the tree')
|
|
48
|
+
return selector ? selector(context) : context
|
|
49
|
+
}
|