@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.
- package/dist/index.cjs +252 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +550 -133
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +550 -133
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +218 -39
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/components/DropZone/index.tsx +100 -0
- package/src/components/FileInput/Trigger.tsx +39 -0
- package/src/components/FileInput/context.tsx +24 -0
- package/src/components/FileInput/index.tsx +144 -0
- package/src/components/Knob/index.tsx +8 -5
- package/src/components/NumberInput/DecrementStepper.tsx +2 -13
- package/src/components/NumberInput/IncrementStepper.tsx +2 -14
- package/src/components/NumberInput/InputField.tsx +5 -11
- package/src/components/NumberInput/Stepper.tsx +4 -19
- package/src/components/NumberInput/StepperButton.tsx +13 -8
- package/src/components/NumberInput/context.tsx +6 -7
- package/src/components/NumberInput/index.tsx +4 -13
- package/src/components/Piano/index.tsx +10 -13
- package/src/components/PointsEditor/Background.tsx +2 -8
- package/src/components/PointsEditor/Container.tsx +2 -8
- package/src/components/PointsEditor/Point.tsx +7 -5
- package/src/components/PointsEditor/SelectionBox.tsx +2 -8
- package/src/components/PointsEditor/context.tsx +6 -6
- package/src/components/PointsEditor/index.tsx +9 -11
- package/src/components/Slider/Marks.tsx +6 -9
- package/src/components/Slider/MarksOption.tsx +7 -2
- package/src/components/Slider/Thumb.tsx +9 -15
- package/src/components/Slider/Track.tsx +6 -13
- package/src/components/Slider/index.tsx +5 -12
- package/src/components/Slider/type.ts +4 -2
- package/src/components/XYPad/Area.tsx +5 -10
- package/src/components/XYPad/Thumb.tsx +5 -11
- package/src/components/XYPad/index.tsx +3 -5
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +3 -15
- package/src/components/_util/decimal-digits.ts +23 -0
- package/src/components/_util/visually-hidden.ts +25 -0
- package/src/css-variables.ts +23 -0
- package/src/hooks/_internal/useCheckSteps.ts +2 -3
- package/src/hooks/useDrag.ts +13 -4
- package/src/hooks/useDragValue.ts +17 -2
- package/src/hooks/useDropZone.ts +78 -0
- package/src/hooks/useWheel.ts +4 -1
- package/src/index.ts +12 -9
- package/src/input-event.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tremolo-ui/react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "React component library for audio app",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"@testing-library/react": "16.3.3",
|
|
31
31
|
"@types/react": "19.3.0",
|
|
32
32
|
"@types/react-dom": "19.3.0",
|
|
33
|
-
"jotai": "
|
|
33
|
+
"jotai": "3.0.0",
|
|
34
34
|
"react": "19.3.0",
|
|
35
35
|
"react-dom": "19.3.0",
|
|
36
36
|
"storybook": "10.6.0",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"react-dom": "^18 || ^19"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@tremolo-ui/dom": "^0.
|
|
45
|
-
"@tremolo-ui/functions": "^0.
|
|
44
|
+
"@tremolo-ui/dom": "^0.7.0",
|
|
45
|
+
"@tremolo-ui/functions": "^0.7.0"
|
|
46
46
|
},
|
|
47
47
|
"files": [
|
|
48
48
|
"dist",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useComposedRefs } from '../../compose-refs'
|
|
4
|
+
import { useDropZone } from '../../hooks/useDropZone'
|
|
5
|
+
|
|
6
|
+
export interface DropZoneProps {
|
|
7
|
+
/**
|
|
8
|
+
* Which files to take, written the way the `accept` attribute of a file
|
|
9
|
+
* input is: a comma separated list of extensions (`.wav`), MIME types
|
|
10
|
+
* (`audio/wav`) and type groups (`audio/*`).
|
|
11
|
+
*
|
|
12
|
+
* What does not match goes to `onReject` rather than `onDrop`. While the
|
|
13
|
+
* drag is still in the air the element is marked `[data-invalid]`, as far as
|
|
14
|
+
* the browser lets it be told apart — a rule written as an extension cannot
|
|
15
|
+
* be decided until the drop.
|
|
16
|
+
*/
|
|
17
|
+
accept?: string
|
|
18
|
+
/**
|
|
19
|
+
* Take more than one file from a single drop. With it off, only the first
|
|
20
|
+
* accepted file is reported, as a file input without `multiple` does.
|
|
21
|
+
*
|
|
22
|
+
* @default false
|
|
23
|
+
*/
|
|
24
|
+
multiple?: boolean
|
|
25
|
+
/**
|
|
26
|
+
* Refuse the drop. The drag is still swallowed rather than let through: an
|
|
27
|
+
* unhandled drop makes the browser leave the page and open the file.
|
|
28
|
+
*
|
|
29
|
+
* @default false
|
|
30
|
+
*/
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Called with the dropped files that match `accept`.
|
|
35
|
+
*
|
|
36
|
+
* @param event the drop, for anything the files leave out — where on the
|
|
37
|
+
* element it landed, most of all.
|
|
38
|
+
*/
|
|
39
|
+
onDrop?: (files: File[], event: DragEvent) => void
|
|
40
|
+
/**
|
|
41
|
+
* Called with the dropped files that do not match `accept`, so that the
|
|
42
|
+
* reason can be shown.
|
|
43
|
+
*/
|
|
44
|
+
onReject?: (files: File[], event: DragEvent) => void
|
|
45
|
+
|
|
46
|
+
/** What the zone shows. */
|
|
47
|
+
children: ReactNode
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
type Props = DropZoneProps &
|
|
51
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof DropZoneProps>
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* An area that takes files dropped onto it.
|
|
55
|
+
*
|
|
56
|
+
* To make an element you already have into a drop target, without wrapping it
|
|
57
|
+
* in anything, use the `useDropZone` hook this is built on.
|
|
58
|
+
*/
|
|
59
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
60
|
+
(
|
|
61
|
+
{
|
|
62
|
+
accept,
|
|
63
|
+
multiple = false,
|
|
64
|
+
disabled = false,
|
|
65
|
+
onDrop,
|
|
66
|
+
onReject,
|
|
67
|
+
children,
|
|
68
|
+
...props
|
|
69
|
+
},
|
|
70
|
+
ref,
|
|
71
|
+
) => {
|
|
72
|
+
const { refCallback, over, invalid } = useDropZone<HTMLDivElement>({
|
|
73
|
+
accept,
|
|
74
|
+
multiple,
|
|
75
|
+
disabled,
|
|
76
|
+
onDrop,
|
|
77
|
+
onReject,
|
|
78
|
+
})
|
|
79
|
+
const composedRef = useComposedRefs<HTMLDivElement>(ref, refCallback)
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
ref={composedRef}
|
|
84
|
+
data-dragover={over ? '' : undefined}
|
|
85
|
+
data-invalid={invalid ? '' : undefined}
|
|
86
|
+
data-disabled={disabled ? '' : undefined}
|
|
87
|
+
{...props}
|
|
88
|
+
>
|
|
89
|
+
{children}
|
|
90
|
+
</div>
|
|
91
|
+
)
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* An area that takes files dropped onto it.
|
|
97
|
+
*/
|
|
98
|
+
export const DropZone = {
|
|
99
|
+
Root,
|
|
100
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, forwardRef, ReactNode } from 'react'
|
|
2
|
+
|
|
3
|
+
import { useFileInputContext } from './context'
|
|
4
|
+
|
|
5
|
+
export interface FileInputTriggerProps {
|
|
6
|
+
/** What the trigger reads. It is the accessible name of the file input. */
|
|
7
|
+
children: ReactNode
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type Props = FileInputTriggerProps &
|
|
11
|
+
Omit<
|
|
12
|
+
ComponentPropsWithoutRef<'label'>,
|
|
13
|
+
keyof FileInputTriggerProps | 'htmlFor'
|
|
14
|
+
>
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Opens the file picker, and names the input for a screen reader.
|
|
18
|
+
*
|
|
19
|
+
* A `<label>` rather than a `<button>`: the browser forwards the click to the
|
|
20
|
+
* input on its own, the input stays the one thing in the tab order, and its
|
|
21
|
+
* accessible name comes from this text without an `aria-label` to keep in
|
|
22
|
+
* step. A disabled input ignores the click, so nothing has to be intercepted.
|
|
23
|
+
*/
|
|
24
|
+
export const Trigger = /* @__PURE__ */ forwardRef<HTMLLabelElement, Props>(
|
|
25
|
+
({ children, ...props }, ref) => {
|
|
26
|
+
const { inputId, disabled } = useFileInputContext()
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<label
|
|
30
|
+
ref={ref}
|
|
31
|
+
htmlFor={inputId}
|
|
32
|
+
data-disabled={disabled ? '' : undefined}
|
|
33
|
+
{...props}
|
|
34
|
+
>
|
|
35
|
+
{children}
|
|
36
|
+
</label>
|
|
37
|
+
)
|
|
38
|
+
},
|
|
39
|
+
)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createContext, useContext } from 'react'
|
|
2
|
+
|
|
3
|
+
export type FileInputContextValue = {
|
|
4
|
+
/** The id of the hidden `<input type="file">`, for `Trigger`'s `htmlFor`. */
|
|
5
|
+
inputId: string
|
|
6
|
+
disabled: boolean
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const FileInputContext =
|
|
10
|
+
/* @__PURE__ */ createContext<FileInputContextValue | null>(null)
|
|
11
|
+
|
|
12
|
+
export const FileInputProvider = FileInputContext.Provider
|
|
13
|
+
|
|
14
|
+
export function useFileInputContext(): FileInputContextValue
|
|
15
|
+
export function useFileInputContext<T>(
|
|
16
|
+
selector: (state: FileInputContextValue) => T,
|
|
17
|
+
): T
|
|
18
|
+
export function useFileInputContext<T>(
|
|
19
|
+
selector?: (state: FileInputContextValue) => T,
|
|
20
|
+
) {
|
|
21
|
+
const context = useContext(FileInputContext)
|
|
22
|
+
if (!context) throw new Error('Missing FileInputContext.Provider in the tree')
|
|
23
|
+
return selector ? selector(context) : context
|
|
24
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ChangeEvent,
|
|
3
|
+
ComponentPropsWithoutRef,
|
|
4
|
+
forwardRef,
|
|
5
|
+
ReactNode,
|
|
6
|
+
useId,
|
|
7
|
+
useMemo,
|
|
8
|
+
} from 'react'
|
|
9
|
+
|
|
10
|
+
import { matchesAccept } from '@tremolo-ui/dom'
|
|
11
|
+
|
|
12
|
+
import { visuallyHiddenStyle } from '../_util/visually-hidden'
|
|
13
|
+
|
|
14
|
+
import { FileInputProvider } from './context'
|
|
15
|
+
import { Trigger } from './Trigger'
|
|
16
|
+
|
|
17
|
+
export interface FileInputProps {
|
|
18
|
+
/**
|
|
19
|
+
* Which files to take, written the way the HTML attribute is: a comma
|
|
20
|
+
* separated list of extensions (`.wav`), MIME types (`audio/wav`) and type
|
|
21
|
+
* groups (`audio/*`).
|
|
22
|
+
*
|
|
23
|
+
* **The browser treats it as a hint to the picker, not a rule.** A person
|
|
24
|
+
* can switch the picker to "All Files", so what arrives is checked again
|
|
25
|
+
* here, and anything that does not match goes to `onReject` instead.
|
|
26
|
+
*/
|
|
27
|
+
accept?: string
|
|
28
|
+
/**
|
|
29
|
+
* Let more than one file be picked at a time.
|
|
30
|
+
* @default false
|
|
31
|
+
*/
|
|
32
|
+
multiple?: boolean
|
|
33
|
+
/**
|
|
34
|
+
* Refuse to open the picker, and mark every part with `[data-disabled]`.
|
|
35
|
+
* @default false
|
|
36
|
+
*/
|
|
37
|
+
disabled?: boolean
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Called with the files that were picked and match `accept`.
|
|
41
|
+
*
|
|
42
|
+
* It is not called at all when the picker is dismissed, or when every file
|
|
43
|
+
* picked was rejected — an empty array never arrives, so there is no need to
|
|
44
|
+
* tell "nothing was chosen" from "the choice was cleared".
|
|
45
|
+
*/
|
|
46
|
+
onChange?: (files: File[]) => void
|
|
47
|
+
/**
|
|
48
|
+
* Called with the files that were picked and do not match `accept`, so that
|
|
49
|
+
* the reason can be shown. Given alongside `onChange` when a selection held
|
|
50
|
+
* both.
|
|
51
|
+
*/
|
|
52
|
+
onReject?: (files: File[]) => void
|
|
53
|
+
|
|
54
|
+
/** Normally a `FileInput.Trigger`, and whatever shows what was picked. */
|
|
55
|
+
children: ReactNode
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type Props = FileInputProps &
|
|
59
|
+
Omit<ComponentPropsWithoutRef<'div'>, keyof FileInputProps>
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Hands over the files a person picked.
|
|
63
|
+
*
|
|
64
|
+
* The `<input type="file">` is here, kept out of sight but in the tab order,
|
|
65
|
+
* because it is the control: it takes the focus, opens the picker on Enter,
|
|
66
|
+
* and carries the accessible name. What is on screen is whatever the children
|
|
67
|
+
* draw.
|
|
68
|
+
*/
|
|
69
|
+
export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
|
|
70
|
+
(
|
|
71
|
+
{
|
|
72
|
+
accept,
|
|
73
|
+
multiple = false,
|
|
74
|
+
disabled = false,
|
|
75
|
+
onChange,
|
|
76
|
+
onReject,
|
|
77
|
+
children,
|
|
78
|
+
// Pulled out of `props` so that they land on the input rather than on
|
|
79
|
+
// the wrapper: it is the control, and a description is only announced
|
|
80
|
+
// from the element that takes the focus. No declaration in
|
|
81
|
+
// `FileInputProps` is needed for this — they are already on the div
|
|
82
|
+
// attributes it is merged with, with the same types.
|
|
83
|
+
'aria-label': ariaLabel,
|
|
84
|
+
'aria-labelledby': ariaLabelledby,
|
|
85
|
+
'aria-describedby': ariaDescribedby,
|
|
86
|
+
...props
|
|
87
|
+
},
|
|
88
|
+
ref,
|
|
89
|
+
) => {
|
|
90
|
+
const inputId = useId()
|
|
91
|
+
|
|
92
|
+
const context = useMemo(() => ({ inputId, disabled }), [inputId, disabled])
|
|
93
|
+
|
|
94
|
+
function handleChange(event: ChangeEvent<HTMLInputElement>) {
|
|
95
|
+
const picked = Array.from(event.target.files ?? [])
|
|
96
|
+
|
|
97
|
+
// Picking the same file twice fires no second change event while the
|
|
98
|
+
// value is still on the input, so it is cleared as soon as it is read.
|
|
99
|
+
// Nothing is lost: the files are already in hand.
|
|
100
|
+
event.target.value = ''
|
|
101
|
+
|
|
102
|
+
const accepted: File[] = []
|
|
103
|
+
const rejected: File[] = []
|
|
104
|
+
for (const file of picked) {
|
|
105
|
+
if (matchesAccept(file, accept)) accepted.push(file)
|
|
106
|
+
else rejected.push(file)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (rejected.length > 0) onReject?.(rejected)
|
|
110
|
+
if (accepted.length > 0) onChange?.(accepted)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<FileInputProvider value={context}>
|
|
115
|
+
<div ref={ref} data-disabled={disabled ? '' : undefined} {...props}>
|
|
116
|
+
<input
|
|
117
|
+
id={inputId}
|
|
118
|
+
type="file"
|
|
119
|
+
accept={accept}
|
|
120
|
+
multiple={multiple}
|
|
121
|
+
disabled={disabled}
|
|
122
|
+
style={visuallyHiddenStyle}
|
|
123
|
+
aria-label={ariaLabel}
|
|
124
|
+
aria-labelledby={ariaLabelledby}
|
|
125
|
+
aria-describedby={ariaDescribedby}
|
|
126
|
+
onChange={handleChange}
|
|
127
|
+
/>
|
|
128
|
+
{children}
|
|
129
|
+
</div>
|
|
130
|
+
</FileInputProvider>
|
|
131
|
+
)
|
|
132
|
+
},
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Picks files, and hands them over as `File`s.
|
|
137
|
+
*/
|
|
138
|
+
export const FileInput = {
|
|
139
|
+
Root,
|
|
140
|
+
Trigger,
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export { useFileInputContext, type FileInputContextValue } from './context'
|
|
144
|
+
export { type FileInputTriggerProps } from './Trigger'
|
|
@@ -9,16 +9,15 @@ import {
|
|
|
9
9
|
useRef,
|
|
10
10
|
} from 'react'
|
|
11
11
|
|
|
12
|
-
import type { AxisOptions, XY } from '@tremolo-ui/dom'
|
|
13
12
|
import {
|
|
14
13
|
applyDelta,
|
|
14
|
+
type AxisOptions,
|
|
15
15
|
type InputEventOption,
|
|
16
16
|
type ModifierValue,
|
|
17
17
|
selectModifier,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
} from '@tremolo-ui/functions'
|
|
18
|
+
type XY,
|
|
19
|
+
} from '@tremolo-ui/dom'
|
|
20
|
+
import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
22
21
|
|
|
23
22
|
import { useComposedRefs } from '../../compose-refs'
|
|
24
23
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
@@ -37,6 +36,8 @@ import { InactiveLine } from './InactiveLine'
|
|
|
37
36
|
import { SVGRoot } from './SVGRoot'
|
|
38
37
|
import { Thumb } from './Thumb'
|
|
39
38
|
|
|
39
|
+
import type { CSSVariables } from '../../css-variables'
|
|
40
|
+
|
|
40
41
|
const defaultExternalStyles: KnobProps['externalStyles'] = {
|
|
41
42
|
cursor: 'grabbing',
|
|
42
43
|
}
|
|
@@ -200,6 +201,8 @@ export interface KnobProps {
|
|
|
200
201
|
* </Knob.Root>
|
|
201
202
|
*/
|
|
202
203
|
children: ReactNode
|
|
204
|
+
|
|
205
|
+
style?: CSSProperties & CSSVariables<'knob-size'>
|
|
203
206
|
}
|
|
204
207
|
|
|
205
208
|
export interface KnobMethods {
|
|
@@ -1,22 +1,11 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef } from 'react'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
StepperArrow,
|
|
5
3
|
StepperButton,
|
|
6
|
-
StepperButtonProps,
|
|
4
|
+
type StepperButtonProps,
|
|
7
5
|
} from './StepperButton'
|
|
8
6
|
|
|
9
|
-
// An interface rather than an alias; see `NumberInputIncrementStepperProps`.
|
|
10
|
-
export interface NumberInputDecrementStepperProps extends StepperButtonProps {}
|
|
11
|
-
|
|
12
7
|
/** Lowers the value by one `step`, repeating while held. */
|
|
13
|
-
export function DecrementStepper(
|
|
14
|
-
props: NumberInputDecrementStepperProps &
|
|
15
|
-
Omit<
|
|
16
|
-
ComponentPropsWithoutRef<'div'>,
|
|
17
|
-
keyof NumberInputDecrementStepperProps
|
|
18
|
-
>,
|
|
19
|
-
) {
|
|
8
|
+
export function DecrementStepper(props: StepperButtonProps) {
|
|
20
9
|
return (
|
|
21
10
|
<StepperButton
|
|
22
11
|
direction={-1}
|
|
@@ -1,23 +1,11 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef } from 'react'
|
|
2
|
-
|
|
3
1
|
import {
|
|
4
2
|
StepperArrow,
|
|
5
3
|
StepperButton,
|
|
6
|
-
StepperButtonProps,
|
|
4
|
+
type StepperButtonProps,
|
|
7
5
|
} from './StepperButton'
|
|
8
6
|
|
|
9
|
-
// An interface rather than an alias, so that typedoc lists the props: the
|
|
10
|
-
// shared `StepperButtonProps` is not exported for it to follow.
|
|
11
|
-
export interface NumberInputIncrementStepperProps extends StepperButtonProps {}
|
|
12
|
-
|
|
13
7
|
/** Raises the value by one `step`, repeating while held. */
|
|
14
|
-
export function IncrementStepper(
|
|
15
|
-
props: NumberInputIncrementStepperProps &
|
|
16
|
-
Omit<
|
|
17
|
-
ComponentPropsWithoutRef<'div'>,
|
|
18
|
-
keyof NumberInputIncrementStepperProps
|
|
19
|
-
>,
|
|
20
|
-
) {
|
|
8
|
+
export function IncrementStepper(props: StepperButtonProps) {
|
|
21
9
|
return (
|
|
22
10
|
<StepperButton
|
|
23
11
|
direction={1}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
3
|
forwardRef,
|
|
5
4
|
useEffect,
|
|
6
5
|
useLayoutEffect,
|
|
@@ -12,16 +11,11 @@ import { useComposedRefs } from '../../compose-refs'
|
|
|
12
11
|
|
|
13
12
|
import { useNumberInputContext } from './context'
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
type Props = NumberInputInputFieldProps &
|
|
21
|
-
Omit<
|
|
22
|
-
ComponentPropsWithoutRef<'input'>,
|
|
23
|
-
keyof NumberInputInputFieldProps | 'type' | 'value' | 'defaultValue'
|
|
24
|
-
>
|
|
14
|
+
// The value belongs to `NumberInput.Root`, and the field is always text.
|
|
15
|
+
type Props = Omit<
|
|
16
|
+
ComponentPropsWithoutRef<'input'>,
|
|
17
|
+
'type' | 'value' | 'defaultValue'
|
|
18
|
+
>
|
|
25
19
|
|
|
26
20
|
/** The leading number of the displayed text, whatever the format put around it. */
|
|
27
21
|
const NUMBER_PREFIX = /^\s*-?[\d.,]*/
|
|
@@ -1,33 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
|
-
forwardRef,
|
|
5
|
-
ReactNode,
|
|
6
|
-
useMemo,
|
|
7
|
-
useRef,
|
|
8
|
-
} from 'react'
|
|
1
|
+
import { ComponentPropsWithoutRef, forwardRef, useMemo, useRef } from 'react'
|
|
9
2
|
|
|
10
3
|
import {
|
|
11
4
|
applyDelta,
|
|
5
|
+
type InputEventOption,
|
|
12
6
|
mapModifier,
|
|
13
7
|
selectModifier,
|
|
14
|
-
|
|
15
|
-
} from '@tremolo-ui/functions'
|
|
8
|
+
} from '@tremolo-ui/dom'
|
|
16
9
|
|
|
17
10
|
import { useComposedRefs } from '../../compose-refs'
|
|
18
11
|
import { useDrag } from '../../hooks/useDrag'
|
|
19
12
|
|
|
20
13
|
import { StepperProvider, useNumberInputContext } from './context'
|
|
21
14
|
|
|
22
|
-
|
|
23
|
-
className?: string
|
|
24
|
-
style?: CSSProperties
|
|
25
|
-
/** `<NumberInput.IncrementStepper />` and `<NumberInput.DecrementStepper />` go here. */
|
|
26
|
-
children?: ReactNode
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type Props = NumberInputStepperProps &
|
|
30
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof NumberInputStepperProps>
|
|
15
|
+
type Props = ComponentPropsWithoutRef<'div'>
|
|
31
16
|
|
|
32
17
|
/**
|
|
33
18
|
* The area the steppers sit in, and a drag handle in its own right: dragging it
|
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef,
|
|
1
|
+
import { ComponentPropsWithoutRef, ReactNode, CSSProperties } from 'react'
|
|
2
2
|
|
|
3
3
|
import { useLongPress } from '../../hooks/useLongPress'
|
|
4
4
|
|
|
5
5
|
import { useNumberInputContext, useStepperContext } from './context'
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
7
|
+
import type { CSSVariables } from '../../css-variables'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* `--stepper-icon-size` is read by the default arrow inside, and set on the
|
|
11
|
+
* button so that it inherits there.
|
|
12
|
+
*/
|
|
13
|
+
export type StepperButtonProps = Omit<
|
|
14
|
+
ComponentPropsWithoutRef<'div'>,
|
|
15
|
+
'style'
|
|
16
|
+
> & {
|
|
17
|
+
style?: CSSProperties & CSSVariables<'stepper-icon-size'>
|
|
12
18
|
}
|
|
13
19
|
|
|
14
|
-
type Props = StepperButtonProps
|
|
15
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof StepperButtonProps>
|
|
20
|
+
type Props = StepperButtonProps
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* Shared body of `IncrementStepper` and `DecrementStepper`: the two differ only
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import { createContext, RefObject, useContext } from 'react'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
InputEventOption,
|
|
5
|
-
ModifierState,
|
|
6
|
-
ModifierValue,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
} from '@tremolo-ui/functions'
|
|
3
|
+
import {
|
|
4
|
+
type InputEventOption,
|
|
5
|
+
type ModifierState,
|
|
6
|
+
type ModifierValue,
|
|
7
|
+
} from '@tremolo-ui/dom'
|
|
8
|
+
import { type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
10
9
|
|
|
11
10
|
export type NumberInputContextValue = {
|
|
12
11
|
value: number
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
ComponentPropsWithoutRef,
|
|
3
|
-
CSSProperties,
|
|
4
3
|
forwardRef,
|
|
5
4
|
ReactNode,
|
|
6
5
|
useCallback,
|
|
@@ -15,11 +14,9 @@ import {
|
|
|
15
14
|
InputEventOption,
|
|
16
15
|
ModifierState,
|
|
17
16
|
type ModifierValue,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
type ValueRange,
|
|
22
|
-
} from '@tremolo-ui/functions'
|
|
17
|
+
selectModifier,
|
|
18
|
+
} from '@tremolo-ui/dom'
|
|
19
|
+
import { linearScale, type Scale, type ValueRange } from '@tremolo-ui/functions'
|
|
23
20
|
|
|
24
21
|
import { useCheckSteps } from '../../hooks/_internal/useCheckSteps'
|
|
25
22
|
import { useWheel } from '../../hooks/useWheel'
|
|
@@ -219,8 +216,6 @@ export interface NumberInputProps {
|
|
|
219
216
|
*/
|
|
220
217
|
readonly?: boolean
|
|
221
218
|
|
|
222
|
-
className?: string
|
|
223
|
-
style?: CSSProperties
|
|
224
219
|
/**
|
|
225
220
|
* Called with the new value. While the user types, it is called for every
|
|
226
221
|
* entry that reads as a number, unclamped; committing the entry calls it
|
|
@@ -404,7 +399,7 @@ export const Root = /* @__PURE__ */ forwardRef<NumberInputMethods, Props>(
|
|
|
404
399
|
option: ModifierValue<InputEventOption>,
|
|
405
400
|
modifiers?: ModifierState,
|
|
406
401
|
) => {
|
|
407
|
-
const [mode] =
|
|
402
|
+
const [mode] = selectModifier(option, modifiers).value
|
|
408
403
|
changeValue(
|
|
409
404
|
applyDelta(
|
|
410
405
|
value,
|
|
@@ -532,7 +527,3 @@ export const NumberInput = {
|
|
|
532
527
|
}
|
|
533
528
|
|
|
534
529
|
export { useNumberInputContext, type NumberInputContextValue } from './context'
|
|
535
|
-
export { type NumberInputInputFieldProps } from './InputField'
|
|
536
|
-
export { type NumberInputStepperProps } from './Stepper'
|
|
537
|
-
export { type NumberInputIncrementStepperProps } from './IncrementStepper'
|
|
538
|
-
export { type NumberInputDecrementStepperProps } from './DecrementStepper'
|
|
@@ -11,23 +11,25 @@ import {
|
|
|
11
11
|
useState,
|
|
12
12
|
} from 'react'
|
|
13
13
|
|
|
14
|
-
import { createPianoInput, type PianoInputInstance } from '@tremolo-ui/dom'
|
|
15
14
|
import {
|
|
16
15
|
blackKeyWidth,
|
|
16
|
+
createPianoInput,
|
|
17
17
|
getNoteRangeArray,
|
|
18
|
-
isWhiteKey,
|
|
19
|
-
noteKey,
|
|
20
18
|
notePosition,
|
|
21
19
|
pianoWidth,
|
|
22
20
|
type NoteRange,
|
|
21
|
+
type PianoInputInstance,
|
|
23
22
|
type PianoLayout,
|
|
24
|
-
} from '@tremolo-ui/
|
|
23
|
+
} from '@tremolo-ui/dom'
|
|
24
|
+
import { isWhiteKey, noteKey } from '@tremolo-ui/functions'
|
|
25
25
|
|
|
26
26
|
import { useEventListener } from '../../hooks/useEventListener'
|
|
27
27
|
import { cssLength } from '../_util/css-length'
|
|
28
28
|
|
|
29
29
|
import { KeyboardShortcuts } from './keyboard-shortcuts'
|
|
30
30
|
|
|
31
|
+
import type { CSSVariables } from '../../css-variables'
|
|
32
|
+
|
|
31
33
|
type KeyboardShortcutsScope = 'root' | 'window'
|
|
32
34
|
|
|
33
35
|
function isEditableTarget(target: EventTarget | null) {
|
|
@@ -48,12 +50,6 @@ function shortcutKey(event: KeyboardEvent) {
|
|
|
48
50
|
return event.code || event.key
|
|
49
51
|
}
|
|
50
52
|
|
|
51
|
-
/**
|
|
52
|
-
* `style` that also takes CSS custom properties, which is how a key's colours
|
|
53
|
-
* are set: see `index.css` for the ones each key type reads.
|
|
54
|
-
*/
|
|
55
|
-
export type CSSVariables = Record<`--${string}`, string | number | undefined>
|
|
56
|
-
|
|
57
53
|
/**
|
|
58
54
|
* What {@link PianoProps.keyProps} may return for one key.
|
|
59
55
|
*
|
|
@@ -62,7 +58,8 @@ export type CSSVariables = Record<`--${string}`, string | number | undefined>
|
|
|
62
58
|
* mark a key out.
|
|
63
59
|
*/
|
|
64
60
|
export type KeyAttributes = Omit<ComponentPropsWithoutRef<'div'>, 'style'> & {
|
|
65
|
-
style?: CSSProperties &
|
|
61
|
+
style?: CSSProperties &
|
|
62
|
+
CSSVariables<'color' | 'bg' | 'active-color' | 'active-bg'>
|
|
66
63
|
} & Record<`data-${string}`, string | number | boolean | undefined>
|
|
67
64
|
|
|
68
65
|
/** What a key is, when {@link PianoProps.label} or `keyProps` is asked about it. */
|
|
@@ -161,8 +158,6 @@ export interface PianoProps {
|
|
|
161
158
|
*/
|
|
162
159
|
height?: number | string
|
|
163
160
|
|
|
164
|
-
style?: CSSProperties & CSSVariables
|
|
165
|
-
|
|
166
161
|
/**
|
|
167
162
|
* What to draw inside a key. `''`, `null` and `undefined` leave it bare, so
|
|
168
163
|
* a layout with gaps — {@link SHORTCUTS.HOME_ROW_NATURAL}, say — needs no
|
|
@@ -194,6 +189,8 @@ export interface PianoProps {
|
|
|
194
189
|
onPlayNote?: (note: number, velocity?: number) => void
|
|
195
190
|
/** Called once everything holding a note has let go of it. */
|
|
196
191
|
onStopNote?: (note: number) => void
|
|
192
|
+
|
|
193
|
+
style?: CSSProperties & CSSVariables<'height'>
|
|
197
194
|
}
|
|
198
195
|
|
|
199
196
|
export interface PianoMethods {
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
import { ComponentPropsWithoutRef
|
|
2
|
-
|
|
3
|
-
export interface PointsEditorBackgroundProps {
|
|
4
|
-
/** Whatever the points are placed over: a graph, a canvas, an image. */
|
|
5
|
-
children?: ReactNode
|
|
6
|
-
}
|
|
1
|
+
import { ComponentPropsWithoutRef } from 'react'
|
|
7
2
|
|
|
8
3
|
export function Background({
|
|
9
4
|
className,
|
|
10
5
|
children,
|
|
11
6
|
style,
|
|
12
7
|
...props
|
|
13
|
-
}:
|
|
14
|
-
Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorBackgroundProps>) {
|
|
8
|
+
}: ComponentPropsWithoutRef<'div'>) {
|
|
15
9
|
return (
|
|
16
10
|
<div
|
|
17
11
|
className={className}
|