@tremolo-ui/react 0.3.0 → 0.5.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/index.cjs +1162 -1554
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +58 -93
- package/dist/index.d.cts +592 -278
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +592 -278
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1163 -1555
- package/dist/index.js.map +1 -1
- package/package.json +12 -13
- package/src/components/AnimationCanvas/index.tsx +84 -155
- package/src/components/Knob/ActiveLine.tsx +6 -6
- package/src/components/Knob/InactiveLine.tsx +9 -11
- package/src/components/Knob/SVGRoot.tsx +9 -18
- package/src/components/Knob/context.tsx +57 -89
- package/src/components/Knob/index.css +6 -2
- package/src/components/Knob/index.tsx +95 -83
- package/src/components/NumberInput/DecrementStepper.tsx +17 -50
- package/src/components/NumberInput/IncrementStepper.tsx +17 -50
- package/src/components/NumberInput/InputField.tsx +123 -0
- package/src/components/NumberInput/Stepper.tsx +90 -12
- package/src/components/NumberInput/context.tsx +74 -146
- package/src/components/NumberInput/index.css +38 -102
- package/src/components/NumberInput/index.tsx +252 -98
- package/src/components/NumberInput/stepperButton.tsx +90 -0
- package/src/components/Piano/index.tsx +272 -288
- package/src/components/Piano/keyboardShortcuts.ts +34 -4
- package/src/components/PointsEditor/Background.tsx +8 -2
- package/src/components/PointsEditor/Container.tsx +19 -9
- package/src/components/PointsEditor/Point.tsx +117 -52
- package/src/components/PointsEditor/context.tsx +28 -74
- package/src/components/PointsEditor/index.css +5 -0
- package/src/components/PointsEditor/index.tsx +77 -43
- package/src/components/Slider/{Scale.tsx → Marks.tsx} +8 -8
- package/src/components/Slider/{ScaleOption.tsx → MarksOption.tsx} +16 -18
- package/src/components/Slider/Thumb.tsx +55 -68
- package/src/components/Slider/Track.tsx +15 -15
- package/src/components/Slider/context.tsx +36 -51
- package/src/components/Slider/index.css +18 -5
- package/src/components/Slider/index.tsx +136 -170
- package/src/components/Slider/type.ts +4 -4
- package/src/components/XYPad/Area.tsx +16 -7
- package/src/components/XYPad/Thumb.tsx +62 -74
- package/src/components/XYPad/context.tsx +46 -0
- package/src/components/XYPad/index.css +12 -0
- package/src/components/XYPad/index.tsx +193 -250
- package/src/hooks/useDrag.ts +41 -76
- package/src/hooks/useDragValue.ts +149 -0
- package/src/hooks/useEventListener.ts +1 -1
- package/src/hooks/useWheel.ts +51 -0
- package/src/index.ts +15 -13
- package/src/components/AnimationCanvas/canvas.ts +0 -46
- package/src/components/DragObserver/index.tsx +0 -58
- package/src/components/NumberInput/InternalInput.tsx +0 -221
- 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/key.tsx +0 -205
- package/src/components/WheelObserver/index.tsx +0 -48
- package/src/components/_util/type.ts +0 -1
- package/src/hooks/useDragWithElement.ts +0 -72
- package/src/hooks/usePianoDrag.ts +0 -75
- package/src/hooks/useRefCallbackEvent.ts +0 -34
- /package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +0 -0
|
@@ -1,162 +1,90 @@
|
|
|
1
|
-
import { createContext,
|
|
2
|
-
import { createStore, useStore } from 'zustand'
|
|
1
|
+
import { createContext, RefObject, useContext } from 'react'
|
|
3
2
|
|
|
4
|
-
import {
|
|
3
|
+
import type { InputEventOption, Scale, ValueRange } from '@tremolo-ui/functions'
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type State = {
|
|
9
|
-
value: string
|
|
10
|
-
valueAsNumber: number
|
|
11
|
-
step: number
|
|
5
|
+
export type NumberInputContextValue = {
|
|
6
|
+
value: number
|
|
12
7
|
|
|
8
|
+
/**
|
|
9
|
+
* The range the caller asked for. Left undefined when unbounded, which is
|
|
10
|
+
* what `aria-valuemin` / `aria-valuemax` and the steppers need to tell apart
|
|
11
|
+
* from a range that happens to sit at the edge of the safe integers.
|
|
12
|
+
*/
|
|
13
13
|
min?: number
|
|
14
14
|
max?: number
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
15
|
+
step: number
|
|
16
|
+
/** How the value is distributed across the travel. */
|
|
17
|
+
scale: Scale
|
|
18
|
+
|
|
19
|
+
disabled: boolean
|
|
20
|
+
readonly: boolean
|
|
21
|
+
clampValue: boolean
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The effective scaling for `applyDelta` and for clamping on commit, with the
|
|
25
|
+
* unbounded ends filled in. Ignores `min` / `max` when `clampValue` is off.
|
|
26
|
+
*/
|
|
27
|
+
range: ValueRange
|
|
28
|
+
|
|
29
|
+
keyboard: InputEventOption | null
|
|
30
|
+
/** Pixels of vertical drag on `Stepper` that move the value by one `step`. */
|
|
31
|
+
drag: number | null
|
|
32
|
+
|
|
33
|
+
/** What the input shows: the draft while editing, the formatted value otherwise. */
|
|
34
|
+
text: string
|
|
35
|
+
editing: boolean
|
|
36
|
+
/** Whether the value sits outside `min` / `max`. Not judged while editing. */
|
|
37
|
+
outOfRange: boolean
|
|
38
|
+
/** Whether a stepper would have no effect, for `aria-disabled`. */
|
|
39
|
+
atMin: boolean
|
|
40
|
+
atMax: boolean
|
|
41
|
+
|
|
42
|
+
format: (value: number) => string
|
|
43
|
+
parse: (text: string) => number
|
|
44
|
+
|
|
45
|
+
/** Typing. Replaces the draft and reports the parsed value, unclamped. */
|
|
46
|
+
setDraft: (text: string) => void
|
|
47
|
+
/** Blur or Enter. Clamps the draft, reports it, and drops the draft. */
|
|
48
|
+
commitDraft: () => void
|
|
49
|
+
/** Drops the draft and replaces the value: steppers, wheel, keyboard, drag. */
|
|
50
|
+
changeValue: (next: number) => void
|
|
51
|
+
/** Moves the value by one `option` in `direction`, normally +1 or -1. */
|
|
52
|
+
nudge: (direction: number, option: InputEventOption) => void
|
|
53
|
+
|
|
54
|
+
/** `InputField` registers itself here; `Root` focuses it through its methods. */
|
|
55
|
+
inputRef: RefObject<HTMLInputElement | null>
|
|
26
56
|
}
|
|
27
57
|
|
|
28
|
-
|
|
58
|
+
const NumberInputContext = createContext<NumberInputContextValue | null>(null)
|
|
29
59
|
|
|
30
|
-
|
|
60
|
+
export const NumberInputProvider = NumberInputContext.Provider
|
|
31
61
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
62
|
+
/**
|
|
63
|
+
* The only state behind this is the editing draft; `value` comes from the props
|
|
64
|
+
* of `Root` and everything else is derived during render.
|
|
65
|
+
*/
|
|
66
|
+
export function useNumberInputContext(): NumberInputContextValue
|
|
67
|
+
export function useNumberInputContext<T>(
|
|
68
|
+
selector: (state: NumberInputContextValue) => T,
|
|
69
|
+
): T
|
|
70
|
+
export function useNumberInputContext<T>(
|
|
71
|
+
selector?: (state: NumberInputContextValue) => T,
|
|
36
72
|
) {
|
|
37
|
-
|
|
73
|
+
const context = useContext(NumberInputContext)
|
|
74
|
+
if (!context)
|
|
75
|
+
throw new Error('Missing NumberInputContext.Provider in the tree')
|
|
76
|
+
return selector ? selector(context) : context
|
|
38
77
|
}
|
|
39
78
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
valueAsNumber: 0,
|
|
44
|
-
min: 0,
|
|
45
|
-
max: 0,
|
|
46
|
-
step: 1,
|
|
47
|
-
readonly: false,
|
|
48
|
-
keepWithinRange: true,
|
|
49
|
-
onChange: (_v: number, _t: string) => {},
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return createStore<State & Action>()((set) => ({
|
|
53
|
-
...DEFAULT_PROPS,
|
|
54
|
-
...initProps,
|
|
55
|
-
...{
|
|
56
|
-
value: parseValue(
|
|
57
|
-
String(initProps?.value || ''),
|
|
58
|
-
initProps?.units,
|
|
59
|
-
initProps?.digit,
|
|
60
|
-
).formatValue,
|
|
61
|
-
valueAsNumber: parseValue(
|
|
62
|
-
String(initProps?.value || ''),
|
|
63
|
-
initProps?.units,
|
|
64
|
-
initProps?.digit,
|
|
65
|
-
).rawValue,
|
|
66
|
-
},
|
|
67
|
-
increment: () =>
|
|
68
|
-
set((state) => {
|
|
69
|
-
if (state.readonly) return {}
|
|
70
|
-
const current = parseValue(
|
|
71
|
-
state.value,
|
|
72
|
-
state.units,
|
|
73
|
-
state.digit,
|
|
74
|
-
).rawValue
|
|
75
|
-
let next = current + (state.step ?? 1)
|
|
76
|
-
if (state.keepWithinRange) {
|
|
77
|
-
next = safeClamp(next, state.min, state.max)
|
|
78
|
-
}
|
|
79
|
-
const v = parseValue(String(next), state.units, state.digit).formatValue
|
|
80
|
-
state?.onChange?.(next, v)
|
|
81
|
-
return {
|
|
82
|
-
value: v,
|
|
83
|
-
valueAsNumber: next,
|
|
84
|
-
}
|
|
85
|
-
}),
|
|
86
|
-
decrement: () =>
|
|
87
|
-
set((state) => {
|
|
88
|
-
if (state.readonly) return {}
|
|
89
|
-
const current = parseValue(
|
|
90
|
-
state.value,
|
|
91
|
-
state.units,
|
|
92
|
-
state.digit,
|
|
93
|
-
).rawValue
|
|
94
|
-
let next = current - (state.step ?? 1)
|
|
95
|
-
if (state.keepWithinRange) {
|
|
96
|
-
next = safeClamp(next, state.min, state.max)
|
|
97
|
-
}
|
|
98
|
-
const v = parseValue(String(next), state.units, state.digit).formatValue
|
|
99
|
-
state?.onChange?.(next, v)
|
|
100
|
-
return {
|
|
101
|
-
value: v,
|
|
102
|
-
valueAsNumber: next,
|
|
103
|
-
}
|
|
104
|
-
}),
|
|
105
|
-
change: (value) =>
|
|
106
|
-
set((state) => {
|
|
107
|
-
if (state.readonly) return {}
|
|
108
|
-
const v = value ?? ''
|
|
109
|
-
return {
|
|
110
|
-
value: v,
|
|
111
|
-
valueAsNumber: parseValue(v, state.units, state.digit).rawValue,
|
|
112
|
-
}
|
|
113
|
-
}),
|
|
114
|
-
}))
|
|
79
|
+
/** Set by `Stepper` once a drag has actually moved, so the steppers stand down. */
|
|
80
|
+
export type StepperContextValue = {
|
|
81
|
+
draggingRef: RefObject<boolean>
|
|
115
82
|
}
|
|
116
83
|
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
type NumberInputProviderProps = React.PropsWithChildren<ProviderProps>
|
|
120
|
-
|
|
121
|
-
export function NumberInputProvider({
|
|
122
|
-
children,
|
|
123
|
-
...props
|
|
124
|
-
}: NumberInputProviderProps) {
|
|
125
|
-
const storeRef = useRef<NumberInputStore>(null)
|
|
126
|
-
if (!storeRef.current) {
|
|
127
|
-
storeRef.current = createNumberInputStore(props)
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
useEffect(() => {
|
|
131
|
-
if (storeRef.current) {
|
|
132
|
-
const parsed = parseValue(
|
|
133
|
-
String(props?.value || ''),
|
|
134
|
-
props?.units,
|
|
135
|
-
props?.digit,
|
|
136
|
-
)
|
|
137
|
-
storeRef.current.setState({
|
|
138
|
-
...props,
|
|
139
|
-
...{
|
|
140
|
-
value: parsed.formatValue,
|
|
141
|
-
valueAsNumber: parsed.rawValue,
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
} else {
|
|
145
|
-
storeRef.current = createNumberInputStore(props)
|
|
146
|
-
}
|
|
147
|
-
}, [props])
|
|
148
|
-
|
|
149
|
-
return (
|
|
150
|
-
<NumberInputContext.Provider value={storeRef.current}>
|
|
151
|
-
{children}
|
|
152
|
-
</NumberInputContext.Provider>
|
|
153
|
-
)
|
|
154
|
-
}
|
|
84
|
+
const StepperContext = createContext<StepperContextValue | null>(null)
|
|
155
85
|
|
|
156
|
-
export
|
|
157
|
-
|
|
158
|
-
):
|
|
159
|
-
|
|
160
|
-
if (!store) throw new Error('Missing NumberInputContext.Provider in the tree')
|
|
161
|
-
return useStore(store, selector)
|
|
86
|
+
export const StepperProvider = StepperContext.Provider
|
|
87
|
+
|
|
88
|
+
export function useStepperContext(): StepperContextValue | null {
|
|
89
|
+
return useContext(StepperContext)
|
|
162
90
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
.tremolo-number-input
|
|
1
|
+
.tremolo-number-input {
|
|
2
2
|
--border-radius: 4px;
|
|
3
3
|
--stepper-width: 20px;
|
|
4
|
+
--stepper-icon-size: 12px;
|
|
4
5
|
--active-color: oklch(59.5% 0.17548 266.373);
|
|
5
6
|
--border-color: oklch(81% 0 0);
|
|
6
7
|
|
|
@@ -9,70 +10,34 @@
|
|
|
9
10
|
position: relative;
|
|
10
11
|
width: 140px;
|
|
11
12
|
height: min-content;
|
|
12
|
-
border
|
|
13
|
-
border-
|
|
14
|
-
border-width: 0px;
|
|
13
|
+
border: 1px solid var(--border-color);
|
|
14
|
+
border-radius: var(--border-radius);
|
|
15
15
|
outline: none;
|
|
16
16
|
transition: all 0.1s;
|
|
17
17
|
|
|
18
|
-
&:
|
|
19
|
-
border-color: oklch(
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
&[data-variant='outline'] {
|
|
23
|
-
border-width: 1px;
|
|
24
|
-
border-radius: var(--border-radius);
|
|
25
|
-
|
|
26
|
-
&:hover {
|
|
27
|
-
--border-color: oklch(65.335% 0 0);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
&:focus-within {
|
|
31
|
-
--border-color: var(--active-color);
|
|
32
|
-
box-shadow: 0px 0px 0px 2px
|
|
33
|
-
color-mix(in srgb, var(--active-color) 10%, transparent);
|
|
34
|
-
}
|
|
18
|
+
&:hover {
|
|
19
|
+
--border-color: oklch(65.335% 0 0);
|
|
35
20
|
}
|
|
36
21
|
|
|
37
|
-
|
|
38
|
-
--
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
border-color: transparent;
|
|
42
|
-
background-color: var(--bg);
|
|
43
|
-
|
|
44
|
-
&:hover {
|
|
45
|
-
--bg: oklch(93.166% 0 0);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
&:focus-within {
|
|
49
|
-
background-color: inherit;
|
|
50
|
-
--border-color: var(--active-color);
|
|
51
|
-
box-shadow: 0px 0px 0px 2px
|
|
52
|
-
color-mix(in srgb, var(--active-color) 10%, transparent);
|
|
53
|
-
}
|
|
22
|
+
&:focus-within {
|
|
23
|
+
--border-color: var(--active-color);
|
|
24
|
+
box-shadow: 0px 0px 0px 2px
|
|
25
|
+
color-mix(in srgb, var(--active-color) 10%, transparent);
|
|
54
26
|
}
|
|
55
27
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
--border-color: oklch(65.335% 0 0);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
&:focus-within {
|
|
64
|
-
--border-color: var(--active-color);
|
|
65
|
-
box-shadow: 0px 2px 0px 0px
|
|
66
|
-
color-mix(in srgb, var(--active-color) 10%, transparent);
|
|
67
|
-
}
|
|
28
|
+
&:not(:focus-within):has(
|
|
29
|
+
.tremolo-number-input-field[data-out-of-range='true']
|
|
30
|
+
) {
|
|
31
|
+
border-color: oklch(64.245% 0.21266 25.499);
|
|
68
32
|
}
|
|
69
33
|
|
|
70
|
-
|
|
34
|
+
/* Only leave room for the stepper when one was composed in. */
|
|
35
|
+
&:not(:has(.tremolo-number-input-stepper)) {
|
|
71
36
|
--stepper-width: 0px;
|
|
72
37
|
}
|
|
73
38
|
}
|
|
74
39
|
|
|
75
|
-
.tremolo-number-input {
|
|
40
|
+
.tremolo-number-input-field {
|
|
76
41
|
display: block;
|
|
77
42
|
color: inherit;
|
|
78
43
|
width: calc(100% - 20px - var(--stepper-width));
|
|
@@ -100,26 +65,28 @@
|
|
|
100
65
|
width: var(--stepper-width);
|
|
101
66
|
transition: all 0.1s linear;
|
|
102
67
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
68
|
+
/* Kept out of the way until the input is being used. */
|
|
69
|
+
opacity: 0;
|
|
70
|
+
transform: translateX(40%);
|
|
107
71
|
}
|
|
108
72
|
|
|
109
|
-
:is(
|
|
110
|
-
.tremolo-number-input-wrapper:hover,
|
|
111
|
-
.tremolo-number-input-wrapper:focus-within
|
|
112
|
-
)
|
|
73
|
+
:is(.tremolo-number-input:hover, .tremolo-number-input:focus-within)
|
|
113
74
|
.tremolo-number-input-stepper {
|
|
114
75
|
opacity: 1;
|
|
115
76
|
transform: translateX(0);
|
|
116
77
|
}
|
|
117
78
|
|
|
118
|
-
.tremolo-number-input-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
79
|
+
.tremolo-number-input-stepper-icon {
|
|
80
|
+
width: var(--stepper-icon-size);
|
|
81
|
+
height: var(--stepper-icon-size);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
:is(
|
|
85
|
+
.tremolo-number-input-increment-stepper,
|
|
86
|
+
.tremolo-number-input-decrement-stepper
|
|
87
|
+
) {
|
|
88
|
+
border-inline-start: 1px solid oklch(84.522% 0 0);
|
|
89
|
+
font-size: 0.75rem;
|
|
123
90
|
display: flex;
|
|
124
91
|
justify-content: center;
|
|
125
92
|
align-items: center;
|
|
@@ -127,59 +94,28 @@
|
|
|
127
94
|
max-height: 50%;
|
|
128
95
|
user-select: none;
|
|
129
96
|
cursor: pointer;
|
|
130
|
-
border-top-right-radius: var(--border-radius);
|
|
131
97
|
|
|
132
98
|
&[aria-disabled='false']:active {
|
|
133
99
|
background-color: oklch(37.798% 0.03812 254.612 / 0.055);
|
|
134
100
|
}
|
|
135
101
|
|
|
136
|
-
&[aria-disabled='true']
|
|
137
|
-
cursor: not-allowed;
|
|
138
|
-
}
|
|
139
|
-
|
|
102
|
+
&[aria-disabled='true'],
|
|
140
103
|
&[aria-readonly='true'] {
|
|
141
104
|
cursor: not-allowed;
|
|
142
105
|
}
|
|
143
106
|
}
|
|
144
107
|
|
|
108
|
+
.tremolo-number-input-increment-stepper {
|
|
109
|
+
border-top-right-radius: var(--border-radius);
|
|
110
|
+
}
|
|
111
|
+
|
|
145
112
|
.tremolo-number-input-decrement-stepper {
|
|
146
|
-
border-inline-start-width: 1px;
|
|
147
|
-
border-inline-start-style: solid;
|
|
148
|
-
border-inline-start-color: oklch(84.522% 0 0);
|
|
149
113
|
border-top: 1px solid oklch(84.522% 0 0);
|
|
150
|
-
font-size: calc(0.75rem);
|
|
151
|
-
display: flex;
|
|
152
|
-
justify-content: center;
|
|
153
|
-
align-items: center;
|
|
154
|
-
flex: 1 1 0%;
|
|
155
|
-
max-height: 50%;
|
|
156
|
-
user-select: none;
|
|
157
|
-
cursor: pointer;
|
|
158
114
|
border-bottom-right-radius: var(--border-radius);
|
|
159
|
-
|
|
160
|
-
&[aria-disabled='false']:active {
|
|
161
|
-
background-color: oklch(37.798% 0.03812 254.612 / 0.055);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
&[aria-disabled='true'] {
|
|
165
|
-
cursor: not-allowed;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
&[aria-readonly='true'] {
|
|
169
|
-
cursor: not-allowed;
|
|
170
|
-
}
|
|
171
115
|
}
|
|
172
116
|
|
|
173
117
|
:where(.dark, [data-theme='dark']) {
|
|
174
|
-
.tremolo-number-input
|
|
118
|
+
.tremolo-number-input {
|
|
175
119
|
--border-color: oklch(52% 0 0);
|
|
176
|
-
|
|
177
|
-
&[data-variant='filled'] {
|
|
178
|
-
--bg: oklch(43.5% 0 0);
|
|
179
|
-
|
|
180
|
-
&:hover {
|
|
181
|
-
--bg: oklch(46.6% 0 0);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
120
|
}
|
|
185
121
|
}
|