@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tremolo-ui/react",
3
- "version": "0.6.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": "2.20.3",
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.6.0",
45
- "@tremolo-ui/functions": "^0.6.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
- linearScale,
19
- type Scale,
20
- type ValueRange,
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
- export interface NumberInputInputFieldProps {
16
- className?: string
17
- style?: CSSProperties
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
- type InputEventOption,
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
- export interface NumberInputStepperProps {
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, CSSProperties, ReactNode } from 'react'
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
- export interface StepperButtonProps {
8
- className?: string
9
- style?: CSSProperties
10
- /** Replaces the default arrow. Size it with the `--stepper-icon-size` variable. */
11
- children?: ReactNode
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 type {
4
- InputEventOption,
5
- ModifierState,
6
- ModifierValue,
7
- Scale,
8
- ValueRange,
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
- linearScale,
19
- selectInputEvent,
20
- type Scale,
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] = selectInputEvent(option, modifiers).option
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/functions'
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 & CSSVariables
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, ReactNode } from 'react'
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
- }: PointsEditorBackgroundProps &
14
- Omit<ComponentPropsWithoutRef<'div'>, keyof PointsEditorBackgroundProps>) {
8
+ }: ComponentPropsWithoutRef<'div'>) {
15
9
  return (
16
10
  <div
17
11
  className={className}