@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.
Files changed (90) hide show
  1. package/dist/compose-refs.cjs +72 -0
  2. package/dist/compose-refs.cjs.map +1 -0
  3. package/dist/compose-refs.d.cts +43 -0
  4. package/dist/compose-refs.d.cts.map +1 -0
  5. package/dist/compose-refs.d.ts +43 -0
  6. package/dist/compose-refs.d.ts.map +1 -0
  7. package/dist/compose-refs.js +70 -0
  8. package/dist/compose-refs.js.map +1 -0
  9. package/dist/index.cjs +2272 -1650
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1461 -475
  12. package/dist/index.d.cts.map +1 -1
  13. package/dist/index.d.ts +1461 -475
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +2258 -1648
  16. package/dist/index.js.map +1 -1
  17. package/package.json +32 -31
  18. package/src/components/AnimationCanvas/index.tsx +162 -194
  19. package/src/components/Knob/ActiveLine.tsx +9 -8
  20. package/src/components/Knob/InactiveLine.tsx +11 -14
  21. package/src/components/Knob/SVGRoot.tsx +10 -17
  22. package/src/components/Knob/Thumb.tsx +33 -11
  23. package/src/components/Knob/context.tsx +73 -89
  24. package/src/components/Knob/index.tsx +227 -105
  25. package/src/components/NumberInput/DecrementStepper.tsx +21 -50
  26. package/src/components/NumberInput/IncrementStepper.tsx +22 -50
  27. package/src/components/NumberInput/InputField.tsx +206 -0
  28. package/src/components/NumberInput/Stepper.tsx +150 -21
  29. package/src/components/NumberInput/StepperButton.tsx +95 -0
  30. package/src/components/NumberInput/context.tsx +104 -149
  31. package/src/components/NumberInput/index.tsx +461 -110
  32. package/src/components/Piano/index.tsx +413 -263
  33. package/src/components/Piano/keyboard-shortcuts.ts +61 -0
  34. package/src/components/PointsEditor/Background.tsx +19 -4
  35. package/src/components/PointsEditor/Container.tsx +92 -22
  36. package/src/components/PointsEditor/Point.tsx +293 -53
  37. package/src/components/PointsEditor/SelectionBox.tsx +54 -0
  38. package/src/components/PointsEditor/context.tsx +97 -75
  39. package/src/components/PointsEditor/index.tsx +497 -62
  40. package/src/components/Slider/Marks.tsx +84 -0
  41. package/src/components/Slider/MarksOption.tsx +115 -0
  42. package/src/components/Slider/Thumb.tsx +99 -54
  43. package/src/components/Slider/Track.tsx +73 -62
  44. package/src/components/Slider/context.tsx +40 -53
  45. package/src/components/Slider/index.tsx +237 -190
  46. package/src/components/Slider/type.ts +27 -9
  47. package/src/components/XYPad/Area.tsx +52 -33
  48. package/src/components/XYPad/Thumb.tsx +121 -60
  49. package/src/components/XYPad/context.tsx +49 -0
  50. package/src/components/XYPad/index.tsx +338 -246
  51. package/src/components/_util/Placement.tsx +68 -0
  52. package/src/components/_util/VisuallyHiddenRangeInput.tsx +24 -0
  53. package/src/components/_util/css-length.ts +13 -0
  54. package/src/{components/_util/composeRefs.tsx → compose-refs/index.tsx} +33 -5
  55. package/src/hooks/{useCallbackRef.ts → _internal/useCallbackRef.ts} +3 -1
  56. package/src/hooks/_internal/useCheckSteps.ts +170 -0
  57. package/src/hooks/useAnimationFrame.ts +17 -7
  58. package/src/hooks/useDrag.ts +39 -9
  59. package/src/hooks/useDragValue.ts +194 -0
  60. package/src/hooks/useEventListener.ts +11 -7
  61. package/src/hooks/useLongPress.ts +42 -8
  62. package/src/hooks/useMIDIAccess.ts +82 -11
  63. package/src/hooks/useMIDIInput.ts +45 -13
  64. package/src/hooks/useMIDIMessage.ts +23 -5
  65. package/src/hooks/useWheel.ts +29 -9
  66. package/src/index.ts +50 -27
  67. package/src/input-event.ts +32 -0
  68. package/dist/index.css +0 -536
  69. package/src/Globals.d.ts +0 -3
  70. package/src/components/AnimationCanvas/canvas.ts +0 -46
  71. package/src/components/Knob/index.css +0 -77
  72. package/src/components/NumberInput/InternalInput.tsx +0 -216
  73. package/src/components/NumberInput/index.css +0 -185
  74. package/src/components/NumberInput/type.ts +0 -58
  75. package/src/components/Piano/KeyLabel.tsx +0 -51
  76. package/src/components/Piano/context.tsx +0 -73
  77. package/src/components/Piano/index.css +0 -90
  78. package/src/components/Piano/key.tsx +0 -205
  79. package/src/components/Piano/keyboardShortcuts.ts +0 -31
  80. package/src/components/PointsEditor/index.css +0 -30
  81. package/src/components/Slider/Scale.tsx +0 -52
  82. package/src/components/Slider/ScaleOption.tsx +0 -103
  83. package/src/components/Slider/index.css +0 -112
  84. package/src/components/XYPad/index.css +0 -48
  85. package/src/components/_util/index.ts +0 -28
  86. package/src/components/_util/type.ts +0 -1
  87. package/src/hooks/useDragWithElement.ts +0 -100
  88. package/src/hooks/usePianoDrag.ts +0 -75
  89. package/src/hooks/useRefCallbackEvent.ts +0 -34
  90. package/src/styles/global.css +0 -19
@@ -1,162 +1,117 @@
1
- import { createContext, useContext, useEffect, useRef } from 'react'
2
- import { createStore, useStore } from 'zustand'
3
-
4
- import { clamp } from '@tremolo-ui/functions'
5
-
6
- import { parseValue, Units } from './type'
7
-
8
- type State = {
9
- value: string
10
- valueAsNumber: number
11
- step: number
12
-
1
+ import { createContext, RefObject, useContext } from 'react'
2
+
3
+ import type {
4
+ InputEventOption,
5
+ ModifierState,
6
+ ModifierValue,
7
+ Scale,
8
+ ValueRange,
9
+ } from '@tremolo-ui/functions'
10
+
11
+ export type NumberInputContextValue = {
12
+ value: number
13
+
14
+ /**
15
+ * The range the caller asked for. Left undefined when unbounded, which is
16
+ * what `aria-valuemin` / `aria-valuemax` and the steppers need to tell apart
17
+ * from a range that happens to sit at the edge of the safe integers.
18
+ */
13
19
  min?: number
14
20
  max?: number
15
- units?: string | Units
16
- digit?: number
17
- readonly?: boolean
18
- keepWithinRange?: boolean
19
- onChange?: (value: number, text: string) => void
20
- }
21
-
22
- type Action = {
23
- increment: () => void
24
- decrement: () => void
25
- change: (value: string) => void
21
+ step: number
22
+ /** How the value is distributed across the travel. */
23
+ scale: Scale
24
+
25
+ disabled: boolean
26
+ readonly: boolean
27
+ clampValue: boolean
28
+
29
+ /**
30
+ * The effective scaling for `applyDelta`, with unbounded ends filled in.
31
+ * Ignores `min` / `max` when `clampValue` is off.
32
+ */
33
+ range: ValueRange
34
+ /** The effective range for raw input, whose open ends cover finite numbers. */
35
+ rawRange: ValueRange
36
+
37
+ keyboard: ModifierValue<InputEventOption> | null
38
+ /** Pixels of vertical drag on `Stepper` that move the value by one `step`. */
39
+ drag: number | null
40
+ /** How much a `Stepper` drag counts, per modifier key. */
41
+ dragSensitivity: ModifierValue<number>
42
+ /** Whether a `Stepper` drag hides the pointer and reads its movement. */
43
+ pointerLock: boolean
44
+
45
+ /** What `InputField` selects when it takes focus. */
46
+ selectOnFocus: 'all' | 'number' | 'none'
47
+ /** Whether `InputField` shows the plain value while it has focus. */
48
+ unformatOnFocus: boolean
49
+ /** Whether `InputField` puts the caret back after an arrow key steps the value. */
50
+ keepCaretOnStep: boolean
51
+ /** Whether Enter leaves `InputField` as well as committing. */
52
+ blurOnEnter: boolean
53
+
54
+ /** What the input shows: the draft while editing, the formatted value otherwise. */
55
+ text: string
56
+ editing: boolean
57
+ /** Whether the value sits outside `min` / `max`. Not judged while editing. */
58
+ outOfRange: boolean
59
+ /** Whether a stepper would have no effect, for `aria-disabled`. */
60
+ atMin: boolean
61
+ atMax: boolean
62
+
63
+ format: (value: number) => string
64
+ parse: (text: string) => number
65
+
66
+ /** Typing. Replaces the draft and reports the parsed value, unclamped. */
67
+ setDraft: (text: string) => void
68
+ /** Blur or Enter. Optionally clamps the draft, reports it, and drops it. */
69
+ commitDraft: () => void
70
+ /** Drops the draft and replaces the value: steppers, wheel, keyboard, drag. */
71
+ changeValue: (next: number) => void
72
+ /** Moves the value by one `option` in `direction`, normally +1 or -1. */
73
+ nudge: (
74
+ direction: number,
75
+ option: ModifierValue<InputEventOption>,
76
+ modifiers?: ModifierState,
77
+ ) => void
78
+
79
+ /** `InputField` registers itself here; `Root` focuses it through its methods. */
80
+ inputRef: RefObject<HTMLInputElement | null>
26
81
  }
27
82
 
28
- type NumberInputStore = ReturnType<typeof createNumberInputStore>
83
+ const NumberInputContext =
84
+ /* @__PURE__ */ createContext<NumberInputContextValue | null>(null)
29
85
 
30
- type ProviderProps = Partial<Omit<State, 'value'> & { value: number | string }>
86
+ export const NumberInputProvider = NumberInputContext.Provider
31
87
 
32
- export function safeClamp(
33
- value: number,
34
- min = Number.MIN_SAFE_INTEGER,
35
- max = Number.MAX_SAFE_INTEGER,
88
+ /**
89
+ * The only state behind this is the editing draft; `value` comes from the props
90
+ * of `Root` and everything else is derived during render.
91
+ */
92
+ export function useNumberInputContext(): NumberInputContextValue
93
+ export function useNumberInputContext<T>(
94
+ selector: (state: NumberInputContextValue) => T,
95
+ ): T
96
+ export function useNumberInputContext<T>(
97
+ selector?: (state: NumberInputContextValue) => T,
36
98
  ) {
37
- return clamp(value, min, max)
99
+ const context = useContext(NumberInputContext)
100
+ if (!context)
101
+ throw new Error('Missing NumberInputContext.Provider in the tree')
102
+ return selector ? selector(context) : context
38
103
  }
39
104
 
40
- const createNumberInputStore = (initProps?: ProviderProps) => {
41
- const DEFAULT_PROPS: State = {
42
- value: '',
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
- }))
105
+ /** Set by `Stepper` once a drag has actually moved, so the steppers stand down. */
106
+ export type StepperContextValue = {
107
+ draggingRef: RefObject<boolean>
115
108
  }
116
109
 
117
- const NumberInputContext = createContext<NumberInputStore | null>(null)
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
- }
110
+ const StepperContext =
111
+ /* @__PURE__ */ createContext<StepperContextValue | null>(null)
155
112
 
156
- export function useNumberInputContext<T>(
157
- selector: (state: State & Action) => T,
158
- ): T {
159
- const store = useContext(NumberInputContext)
160
- if (!store) throw new Error('Missing NumberInputContext.Provider in the tree')
161
- return useStore(store, selector)
113
+ export const StepperProvider = StepperContext.Provider
114
+
115
+ export function useStepperContext(): StepperContextValue | null {
116
+ return useContext(StepperContext)
162
117
  }