@tremolo-ui/react 0.0.9 → 0.0.11

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 (148) hide show
  1. package/dist/cjs/index.js +434 -271
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/AnimationCanvas/index.d.ts +9 -4
  4. package/dist/cjs/types/components/AnimationCanvas/index.d.ts.map +1 -1
  5. package/dist/cjs/types/components/AnimationKnob/index.d.ts +2 -0
  6. package/dist/cjs/types/components/AnimationKnob/index.d.ts.map +1 -1
  7. package/dist/cjs/types/components/DragObserver/index.d.ts +3 -1
  8. package/dist/cjs/types/components/DragObserver/index.d.ts.map +1 -1
  9. package/dist/cjs/types/components/Knob/index.d.ts +2 -0
  10. package/dist/cjs/types/components/Knob/index.d.ts.map +1 -1
  11. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts +7 -0
  12. package/dist/cjs/types/components/NumberInput/DecrementStepper.d.ts.map +1 -0
  13. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts +7 -0
  14. package/dist/cjs/types/components/NumberInput/IncrementStepper.d.ts.map +1 -0
  15. package/dist/cjs/types/components/NumberInput/Stepper.d.ts +8 -0
  16. package/dist/cjs/types/components/NumberInput/Stepper.d.ts.map +1 -0
  17. package/dist/cjs/types/components/NumberInput/context.d.ts +27 -0
  18. package/dist/cjs/types/components/NumberInput/context.d.ts.map +1 -0
  19. package/dist/cjs/types/components/NumberInput/index.d.ts +31 -16
  20. package/dist/cjs/types/components/NumberInput/index.d.ts.map +1 -1
  21. package/dist/cjs/types/components/Piano/BlackKey.d.ts +1 -0
  22. package/dist/cjs/types/components/Piano/BlackKey.d.ts.map +1 -1
  23. package/dist/cjs/types/components/Piano/KeyLabel.d.ts +2 -0
  24. package/dist/cjs/types/components/Piano/KeyLabel.d.ts.map +1 -1
  25. package/dist/cjs/types/components/Piano/WhiteKey.d.ts +2 -0
  26. package/dist/cjs/types/components/Piano/WhiteKey.d.ts.map +1 -1
  27. package/dist/cjs/types/components/Piano/index.d.ts +6 -4
  28. package/dist/cjs/types/components/Piano/index.d.ts.map +1 -1
  29. package/dist/cjs/types/components/Piano/key.d.ts +8 -2
  30. package/dist/cjs/types/components/Piano/key.d.ts.map +1 -1
  31. package/dist/cjs/types/components/Piano/keyboardShortcuts.d.ts +2 -0
  32. package/dist/cjs/types/components/Piano/keyboardShortcuts.d.ts.map +1 -1
  33. package/dist/cjs/types/components/Slider/Scale.d.ts +2 -0
  34. package/dist/cjs/types/components/Slider/Scale.d.ts.map +1 -1
  35. package/dist/cjs/types/components/Slider/ScaleOption.d.ts +2 -0
  36. package/dist/cjs/types/components/Slider/ScaleOption.d.ts.map +1 -1
  37. package/dist/cjs/types/components/Slider/Thumb.d.ts +4 -0
  38. package/dist/cjs/types/components/Slider/Thumb.d.ts.map +1 -1
  39. package/dist/cjs/types/components/Slider/Track.d.ts +4 -1
  40. package/dist/cjs/types/components/Slider/Track.d.ts.map +1 -1
  41. package/dist/cjs/types/components/Slider/context.d.ts +7 -12
  42. package/dist/cjs/types/components/Slider/context.d.ts.map +1 -1
  43. package/dist/cjs/types/components/Slider/index.d.ts +8 -5
  44. package/dist/cjs/types/components/Slider/index.d.ts.map +1 -1
  45. package/dist/cjs/types/components/WheelObserver/index.d.ts +1 -0
  46. package/dist/cjs/types/components/WheelObserver/index.d.ts.map +1 -1
  47. package/dist/cjs/types/components/XYPad/Area.d.ts +2 -0
  48. package/dist/cjs/types/components/XYPad/Area.d.ts.map +1 -1
  49. package/dist/cjs/types/components/XYPad/Thumb.d.ts +3 -0
  50. package/dist/cjs/types/components/XYPad/Thumb.d.ts.map +1 -1
  51. package/dist/cjs/types/components/XYPad/index.d.ts +4 -0
  52. package/dist/cjs/types/components/XYPad/index.d.ts.map +1 -1
  53. package/dist/cjs/types/hooks/useAnimationFrame.d.ts +3 -0
  54. package/dist/cjs/types/hooks/useAnimationFrame.d.ts.map +1 -0
  55. package/dist/cjs/types/hooks/useDrag.d.ts +15 -0
  56. package/dist/cjs/types/hooks/useDrag.d.ts.map +1 -0
  57. package/dist/cjs/types/hooks/useRefCallbackEvent.d.ts +1 -4
  58. package/dist/cjs/types/hooks/useRefCallbackEvent.d.ts.map +1 -1
  59. package/dist/esm/index.js +432 -262
  60. package/dist/esm/index.js.map +1 -1
  61. package/dist/esm/types/components/AnimationCanvas/index.d.ts +9 -4
  62. package/dist/esm/types/components/AnimationCanvas/index.d.ts.map +1 -1
  63. package/dist/esm/types/components/AnimationKnob/index.d.ts +2 -0
  64. package/dist/esm/types/components/AnimationKnob/index.d.ts.map +1 -1
  65. package/dist/esm/types/components/DragObserver/index.d.ts +3 -1
  66. package/dist/esm/types/components/DragObserver/index.d.ts.map +1 -1
  67. package/dist/esm/types/components/Knob/index.d.ts +2 -0
  68. package/dist/esm/types/components/Knob/index.d.ts.map +1 -1
  69. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts +7 -0
  70. package/dist/esm/types/components/NumberInput/DecrementStepper.d.ts.map +1 -0
  71. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts +7 -0
  72. package/dist/esm/types/components/NumberInput/IncrementStepper.d.ts.map +1 -0
  73. package/dist/esm/types/components/NumberInput/Stepper.d.ts +8 -0
  74. package/dist/esm/types/components/NumberInput/Stepper.d.ts.map +1 -0
  75. package/dist/esm/types/components/NumberInput/context.d.ts +27 -0
  76. package/dist/esm/types/components/NumberInput/context.d.ts.map +1 -0
  77. package/dist/esm/types/components/NumberInput/index.d.ts +31 -16
  78. package/dist/esm/types/components/NumberInput/index.d.ts.map +1 -1
  79. package/dist/esm/types/components/Piano/BlackKey.d.ts +1 -0
  80. package/dist/esm/types/components/Piano/BlackKey.d.ts.map +1 -1
  81. package/dist/esm/types/components/Piano/KeyLabel.d.ts +2 -0
  82. package/dist/esm/types/components/Piano/KeyLabel.d.ts.map +1 -1
  83. package/dist/esm/types/components/Piano/WhiteKey.d.ts +2 -0
  84. package/dist/esm/types/components/Piano/WhiteKey.d.ts.map +1 -1
  85. package/dist/esm/types/components/Piano/index.d.ts +6 -4
  86. package/dist/esm/types/components/Piano/index.d.ts.map +1 -1
  87. package/dist/esm/types/components/Piano/key.d.ts +8 -2
  88. package/dist/esm/types/components/Piano/key.d.ts.map +1 -1
  89. package/dist/esm/types/components/Piano/keyboardShortcuts.d.ts +2 -0
  90. package/dist/esm/types/components/Piano/keyboardShortcuts.d.ts.map +1 -1
  91. package/dist/esm/types/components/Slider/Scale.d.ts +2 -0
  92. package/dist/esm/types/components/Slider/Scale.d.ts.map +1 -1
  93. package/dist/esm/types/components/Slider/ScaleOption.d.ts +2 -0
  94. package/dist/esm/types/components/Slider/ScaleOption.d.ts.map +1 -1
  95. package/dist/esm/types/components/Slider/Thumb.d.ts +4 -0
  96. package/dist/esm/types/components/Slider/Thumb.d.ts.map +1 -1
  97. package/dist/esm/types/components/Slider/Track.d.ts +4 -1
  98. package/dist/esm/types/components/Slider/Track.d.ts.map +1 -1
  99. package/dist/esm/types/components/Slider/context.d.ts +7 -12
  100. package/dist/esm/types/components/Slider/context.d.ts.map +1 -1
  101. package/dist/esm/types/components/Slider/index.d.ts +8 -5
  102. package/dist/esm/types/components/Slider/index.d.ts.map +1 -1
  103. package/dist/esm/types/components/WheelObserver/index.d.ts +1 -0
  104. package/dist/esm/types/components/WheelObserver/index.d.ts.map +1 -1
  105. package/dist/esm/types/components/XYPad/Area.d.ts +2 -0
  106. package/dist/esm/types/components/XYPad/Area.d.ts.map +1 -1
  107. package/dist/esm/types/components/XYPad/Thumb.d.ts +3 -0
  108. package/dist/esm/types/components/XYPad/Thumb.d.ts.map +1 -1
  109. package/dist/esm/types/components/XYPad/index.d.ts +4 -0
  110. package/dist/esm/types/components/XYPad/index.d.ts.map +1 -1
  111. package/dist/esm/types/hooks/useAnimationFrame.d.ts +3 -0
  112. package/dist/esm/types/hooks/useAnimationFrame.d.ts.map +1 -0
  113. package/dist/esm/types/hooks/useDrag.d.ts +15 -0
  114. package/dist/esm/types/hooks/useDrag.d.ts.map +1 -0
  115. package/dist/esm/types/hooks/useRefCallbackEvent.d.ts +1 -4
  116. package/dist/esm/types/hooks/useRefCallbackEvent.d.ts.map +1 -1
  117. package/dist/styles/index.css +110 -14
  118. package/package.json +41 -37
  119. package/src/components/AnimationCanvas/index.tsx +14 -5
  120. package/src/components/AnimationKnob/index.tsx +24 -42
  121. package/src/components/DragObserver/index.tsx +14 -52
  122. package/src/components/Knob/index.tsx +23 -40
  123. package/src/components/NumberInput/DecrementStepper.tsx +52 -0
  124. package/src/components/NumberInput/IncrementStepper.tsx +52 -0
  125. package/src/components/NumberInput/Stepper.tsx +25 -0
  126. package/src/components/NumberInput/context.tsx +150 -0
  127. package/src/components/NumberInput/index.css +102 -14
  128. package/src/components/NumberInput/index.tsx +164 -55
  129. package/src/components/Piano/BlackKey.tsx +13 -10
  130. package/src/components/Piano/KeyLabel.tsx +2 -0
  131. package/src/components/Piano/WhiteKey.tsx +14 -10
  132. package/src/components/Piano/index.tsx +14 -12
  133. package/src/components/Piano/key.ts +8 -2
  134. package/src/components/Piano/keyboardShortcuts.ts +2 -0
  135. package/src/components/Slider/Scale.tsx +8 -6
  136. package/src/components/Slider/ScaleOption.tsx +8 -6
  137. package/src/components/Slider/Thumb.tsx +9 -3
  138. package/src/components/Slider/Track.tsx +25 -15
  139. package/src/components/Slider/context.tsx +50 -51
  140. package/src/components/Slider/index.css +8 -0
  141. package/src/components/Slider/index.tsx +20 -10
  142. package/src/components/WheelObserver/index.tsx +1 -0
  143. package/src/components/XYPad/Area.tsx +2 -0
  144. package/src/components/XYPad/Thumb.tsx +3 -0
  145. package/src/components/XYPad/index.tsx +6 -2
  146. package/src/hooks/useAnimationFrame.ts +18 -0
  147. package/src/hooks/useDrag.ts +92 -0
  148. package/src/hooks/useRefCallbackEvent.ts +4 -7
@@ -10,10 +10,11 @@ import {
10
10
  stepValue,
11
11
  } from '@tremolo-ui/functions'
12
12
 
13
- import { useEventListener } from '../../hooks/useEventListener'
13
+ import { useDrag } from '../../hooks/useDrag'
14
14
  import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
15
15
  import { addNoSelect, removeNoSelect } from '../_util'
16
16
 
17
+ /** @category Knob */
17
18
  export interface KnobProps {
18
19
  // required
19
20
  value: number
@@ -69,6 +70,7 @@ export interface KnobProps {
69
70
  onChange?: (value: number) => void
70
71
  }
71
72
 
73
+ /** @category Knob */
72
74
  export function Knob({
73
75
  value,
74
76
  min,
@@ -93,8 +95,7 @@ export function Knob({
93
95
  classes,
94
96
  ...props
95
97
  }: KnobProps & Omit<ComponentPropsWithoutRef<'svg'>, keyof KnobProps>) {
96
- // -- state and ref ---
97
- const dragOffsetY = useRef<number | undefined>(undefined)
98
+ const valueRef = useRef(0)
98
99
 
99
100
  // --- interpret props ---
100
101
  const padding = 8 // %
@@ -105,27 +106,15 @@ export function Knob({
105
106
  const s = normalizeValue(startValue, min, max, skew)
106
107
 
107
108
  // --- internal functions ---
108
- const handleEvent = (
109
- event:
110
- | MouseEvent
111
- | React.MouseEvent<HTMLOrSVGElement, MouseEvent>
112
- | TouchEvent,
113
- ) => {
114
- if (!onChange || readonly) return
115
- const isTouch = event instanceof TouchEvent
116
- if (isTouch && event.cancelable) event.preventDefault()
117
- if (dragOffsetY.current) {
118
- if (bodyNoSelect) addNoSelect()
119
- const screenY = isTouch ? event.touches[0].screenY : event.screenY
120
- const delta = dragOffsetY.current - screenY
121
- if (Math.abs(delta) < 1) return
122
- dragOffsetY.current = screenY
123
- const n = normalizeValue(value, min, max, skew)
124
- const v = rawValue(n + delta / 100, min, max, skew)
109
+ const onDrag = useCallback(
110
+ (_x: number, y: number) => {
111
+ if (!onChange || readonly) return
112
+ const v = rawValue(valueRef.current - y / 100, min, max, skew)
125
113
  const v2 = clamp(stepValue(v, step), min, max)
126
114
  onChange(v2)
127
- }
128
- }
115
+ },
116
+ [max, min, onChange, readonly, skew, step],
117
+ )
129
118
 
130
119
  const handleKeyDown = useCallback(
131
120
  (event: React.KeyboardEvent<HTMLOrSVGElement>) => {
@@ -149,6 +138,17 @@ export function Knob({
149
138
  )
150
139
 
151
140
  // --- hooks ---
141
+ const [touchMoveRefCallback, pointerDownHandler] = useDrag<SVGElement>({
142
+ onDrag: onDrag,
143
+ onDragStart: () => {
144
+ valueRef.current = normalizeValue(value, min, max, skew)
145
+ if (bodyNoSelect) addNoSelect()
146
+ },
147
+ onDragEnd: () => {
148
+ if (bodyNoSelect) removeNoSelect()
149
+ },
150
+ })
151
+
152
152
  const wheelRefCallback = useRefCallbackEvent(
153
153
  'wheel',
154
154
  (event) => {
@@ -168,20 +168,6 @@ export function Knob({
168
168
  [wheel, value, min, max, skew, step, onChange],
169
169
  )
170
170
 
171
- const touchMoveRefCallback = useRefCallbackEvent(
172
- 'touchmove',
173
- handleEvent,
174
- { passive: false },
175
- [value, min, max, skew, step, onChange, bodyNoSelect],
176
- )
177
-
178
- useEventListener(window, 'mousemove', handleEvent)
179
-
180
- useEventListener(window, 'pointerup', () => {
181
- dragOffsetY.current = undefined
182
- if (bodyNoSelect) removeNoSelect()
183
- })
184
-
185
171
  const center = size / 2
186
172
  const r1 = -135
187
173
  const r2 = -135 + Math.min(p, s) * 270
@@ -211,10 +197,7 @@ export function Knob({
211
197
  aria-valuemax={max}
212
198
  aria-disabled={disabled}
213
199
  aria-readonly={readonly}
214
- onPointerDown={(event) => {
215
- dragOffsetY.current = event.screenY
216
- handleEvent(event)
217
- }}
200
+ onPointerDown={pointerDownHandler}
218
201
  onDoubleClick={() => {
219
202
  if (enableDoubleClickDefault && onChange) {
220
203
  onChange(defaultValue)
@@ -0,0 +1,52 @@
1
+ import clsx from 'clsx'
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
+
4
+ import { useNumberInputContext } from './context'
5
+
6
+ export interface DecrementStepperProps {
7
+ size?: number
8
+ children?: ReactNode
9
+ }
10
+
11
+ export function DecrementStepper({
12
+ size = 12,
13
+ children,
14
+ className,
15
+ ...props
16
+ }: DecrementStepperProps &
17
+ Omit<ComponentPropsWithoutRef<'div'>, keyof DecrementStepperProps>) {
18
+ const min = useNumberInputContext((s) => s.min)
19
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
20
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange)
21
+ const decrement = useNumberInputContext((s) => s.decrement)
22
+
23
+ // TODO: add long press event
24
+
25
+ return (
26
+ <div
27
+ className={clsx('tremolo-number-input-decrement-stepper', className)}
28
+ role="button"
29
+ tabIndex={-1}
30
+ aria-disabled={
31
+ keepWithinRange && valueAsNumber <= (min ?? Number.MIN_SAFE_INTEGER)
32
+ }
33
+ onPointerDown={decrement}
34
+ {...props}
35
+ >
36
+ {children || (
37
+ <svg
38
+ width={size}
39
+ height={size}
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ strokeWidth="2"
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ >
47
+ <polyline points="6 9 12 15 18 9" />
48
+ </svg>
49
+ )}
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,52 @@
1
+ import clsx from 'clsx'
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
+
4
+ import { useNumberInputContext } from './context'
5
+
6
+ export interface IncrementStepperProps {
7
+ size?: number
8
+ children?: ReactNode
9
+ }
10
+
11
+ export function IncrementStepper({
12
+ size = 12,
13
+ children,
14
+ className,
15
+ ...props
16
+ }: IncrementStepperProps &
17
+ Omit<ComponentPropsWithoutRef<'div'>, keyof IncrementStepperProps>) {
18
+ const max = useNumberInputContext((s) => s.max)
19
+ const valueAsNumber = useNumberInputContext((s) => s.valueAsNumber)
20
+ const keepWithinRange = useNumberInputContext((s) => s.keepWithinRange)
21
+ const increment = useNumberInputContext((s) => s.increment)
22
+
23
+ // TODO: add long press event
24
+
25
+ return (
26
+ <div
27
+ className={clsx('tremolo-number-input-increment-stepper', className)}
28
+ role="button"
29
+ tabIndex={-1}
30
+ aria-disabled={
31
+ keepWithinRange && valueAsNumber >= (max ?? Number.MAX_SAFE_INTEGER)
32
+ }
33
+ onPointerDown={increment}
34
+ {...props}
35
+ >
36
+ {children || (
37
+ <svg
38
+ width={size}
39
+ height={size}
40
+ viewBox="0 0 24 24"
41
+ fill="none"
42
+ stroke="currentColor"
43
+ strokeWidth="2"
44
+ strokeLinecap="round"
45
+ strokeLinejoin="round"
46
+ >
47
+ <polyline points="18 15 12 9 6 15" />
48
+ </svg>
49
+ )}
50
+ </div>
51
+ )
52
+ }
@@ -0,0 +1,25 @@
1
+ import clsx from 'clsx'
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react'
3
+
4
+ export interface StepperProps {
5
+ /** Display only when hovering. */
6
+ dynamic?: boolean
7
+ children?: ReactNode
8
+ }
9
+
10
+ export function Stepper({
11
+ dynamic = true,
12
+ children,
13
+ className,
14
+ ...props
15
+ }: StepperProps & Omit<ComponentPropsWithoutRef<'div'>, keyof StepperProps>) {
16
+ return (
17
+ <div
18
+ className={clsx('tremolo-number-input-stepper', className)}
19
+ data-dynamic={dynamic}
20
+ {...props}
21
+ >
22
+ {children}
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,150 @@
1
+ import { createContext, useContext, useEffect, useRef } from 'react'
2
+ import { createStore, useStore } from 'zustand'
3
+
4
+ import { clamp } from '@tremolo-ui/functions'
5
+ import { parseValue, Units } from '@tremolo-ui/functions/NumberInput'
6
+
7
+ type State = {
8
+ value: string
9
+ valueAsNumber: number
10
+ min?: number
11
+ max?: number
12
+ step?: number
13
+ keepWithinRange?: boolean
14
+ units?: string | Units
15
+ digit?: number
16
+ onChange?: (value: number, text: string) => void
17
+ }
18
+
19
+ type Action = {
20
+ increment: () => void
21
+ decrement: () => void
22
+ change: (value: string) => void
23
+ }
24
+
25
+ type NumberInputStore = ReturnType<typeof createNumberInputStore>
26
+
27
+ type ProviderProps = Partial<Omit<State, 'value'> & { value: number | string }>
28
+
29
+ export function safeClamp(
30
+ value: number,
31
+ min = Number.MIN_SAFE_INTEGER,
32
+ max = Number.MAX_SAFE_INTEGER,
33
+ ) {
34
+ return clamp(value, min, max)
35
+ }
36
+
37
+ const createNumberInputStore = (initProps?: ProviderProps) => {
38
+ const DEFAULT_PROPS: State = {
39
+ value: '',
40
+ valueAsNumber: 0,
41
+ min: 0,
42
+ max: 0,
43
+ step: 1,
44
+ keepWithinRange: true,
45
+ onChange: (_v: number, _t: string) => {},
46
+ }
47
+
48
+ return createStore<State & Action>()((set) => ({
49
+ ...DEFAULT_PROPS,
50
+ ...initProps,
51
+ ...{
52
+ value: parseValue(
53
+ String(initProps?.value || ''),
54
+ initProps?.units,
55
+ initProps?.digit,
56
+ ).formatValue,
57
+ valueAsNumber: parseValue(
58
+ String(initProps?.value || ''),
59
+ initProps?.units,
60
+ initProps?.digit,
61
+ ).rawValue,
62
+ },
63
+ increment: () =>
64
+ set((state) => {
65
+ let next =
66
+ parseValue(state.value, state.units, state.digit).rawValue +
67
+ (state.step ?? 1)
68
+ if (state.keepWithinRange) {
69
+ next = safeClamp(next, state.min, state.max)
70
+ }
71
+ const v = parseValue(String(next), state.units, state.digit).formatValue
72
+ state?.onChange?.(next, v)
73
+ return {
74
+ value: v,
75
+ valueAsNumber: next,
76
+ }
77
+ }),
78
+ decrement: () =>
79
+ set((state) => {
80
+ let next =
81
+ parseValue(state.value, state.units, state.digit).rawValue -
82
+ (state.step ?? 1)
83
+ if (state.keepWithinRange) {
84
+ next = safeClamp(next, state.min, state.max)
85
+ }
86
+ const v = parseValue(String(next), state.units, state.digit).formatValue
87
+ state?.onChange?.(next, v)
88
+ return {
89
+ value: v,
90
+ valueAsNumber: next,
91
+ }
92
+ }),
93
+ change: (value) =>
94
+ set((state) => {
95
+ const v = value ?? ''
96
+ return {
97
+ value: v,
98
+ valueAsNumber: parseValue(v, state.units, state.digit).rawValue,
99
+ }
100
+ }),
101
+ }))
102
+ }
103
+
104
+ const NumberInputContext = createContext<NumberInputStore | null>(null)
105
+
106
+ type NumberInputProviderProps = React.PropsWithChildren<ProviderProps>
107
+
108
+ export function NumberInputProvider({
109
+ children,
110
+ ...props
111
+ }: NumberInputProviderProps) {
112
+ const storeRef = useRef<NumberInputStore>(null)
113
+ if (!storeRef.current) {
114
+ storeRef.current = createNumberInputStore(props)
115
+ }
116
+
117
+ useEffect(() => {
118
+ if (storeRef.current) {
119
+ const parsed = parseValue(
120
+ String(props?.value || ''),
121
+ props?.units,
122
+ props?.digit,
123
+ )
124
+ storeRef.current.setState({
125
+ ...props,
126
+ ...{
127
+ value: parsed.formatValue,
128
+ valueAsNumber: parsed.rawValue,
129
+ },
130
+ })
131
+ } else {
132
+ storeRef.current = createNumberInputStore(props)
133
+ }
134
+ }, [props])
135
+
136
+ return (
137
+ <NumberInputContext.Provider value={storeRef.current}>
138
+ {children}
139
+ </NumberInputContext.Provider>
140
+ )
141
+ }
142
+
143
+ /** @category NumberInput */
144
+ export function useNumberInputContext<T>(
145
+ selector: (state: State & Action) => T,
146
+ ): T {
147
+ const store = useContext(NumberInputContext)
148
+ if (!store) throw new Error('Missing NumberInputContext.Provider in the tree')
149
+ return useStore(store, selector)
150
+ }
@@ -1,26 +1,114 @@
1
- .tremolo-number-input {
2
- display: inline-block;
3
- background-color: inherit;
4
- font: inherit;
5
- margin: 0px;
6
- padding-top: 6px;
7
- padding-bottom: 6px;
8
- padding-left: 10px;
9
- padding-right: 10px;
1
+ .tremolo-number-input-wrapper {
2
+ --tremolo-number-input-border-radius: 4px;
3
+ --tremolo-number-input-stepper-width: 20px;
4
+
5
+ display: inline-flex;
6
+ flex-direction: row;
7
+ position: relative;
8
+
10
9
  border-style: solid;
11
10
  border-width: 1px;
12
11
  border-color: #bbb;
13
- border-radius: 4px;
14
- outline: 0px;
15
- transition: all 0.1s linear;
16
- appearance: textfield;
12
+ border-radius: var(--tremolo-number-input-border-radius);
13
+
14
+ width: 140px;
15
+ height: min-content;
17
16
 
18
17
  &:hover {
19
18
  border-color: var(--tremolo-theme-color);
20
19
  }
21
20
 
22
- &:focus {
21
+ &:not(:focus-within):has(> .tremolo-number-input[data-error='true']) {
22
+ border-color: #f34343;
23
+ }
24
+
25
+ &:focus-within {
23
26
  border-color: var(--tremolo-theme-color);
24
27
  box-shadow: 0px 0px 0px 2px rgba(var(--tremolo-theme-color-rgb), 0.1);
25
28
  }
29
+
30
+ &[data-stepper='false'] {
31
+ --tremolo-number-input-stepper-width: 0px;
32
+ }
33
+ }
34
+
35
+ .tremolo-number-input {
36
+ display: block;
37
+ width: calc(100% - 20px - var(--tremolo-number-input-stepper-width));
38
+ background-color: inherit;
39
+ font: inherit;
40
+ padding: 6px 10px;
41
+
42
+ margin: 0;
43
+ border: none;
44
+ outline: none;
45
+ appearance: textfield;
46
+ }
47
+
48
+ .tremolo-number-input-stepper {
49
+ display: flex;
50
+ flex-direction: column;
51
+ height: 100%;
52
+ position: absolute;
53
+ top: 0;
54
+ right: 0;
55
+ width: var(--tremolo-number-input-stepper-width);
56
+ transition: all 0.1s linear;
57
+
58
+ &[data-dynamic='true'] {
59
+ opacity: 0;
60
+ transform: translateX(40%);
61
+ }
62
+ }
63
+
64
+ .tremolo-number-input-wrapper:hover .tremolo-number-input-stepper {
65
+ opacity: 1;
66
+ transform: translateX(0);
67
+ }
68
+
69
+ .tremolo-number-input-increment-stepper {
70
+ border-inline-start-width: 1px;
71
+ border-inline-start-style: solid;
72
+ border-inline-start-color: #ccc;
73
+ font-size: calc(0.75rem);
74
+ display: flex;
75
+ justify-content: center;
76
+ align-items: center;
77
+ flex: 1 1 0%;
78
+ max-height: 50%;
79
+ user-select: none;
80
+ cursor: pointer;
81
+ border-top-right-radius: var(--tremolo-number-input-border-radius);
82
+
83
+ &[aria-disabled='false']:active {
84
+ background-color: #3443560e;
85
+ }
86
+
87
+ &[aria-disabled='true'] {
88
+ cursor: not-allowed;
89
+ }
90
+ }
91
+
92
+ .tremolo-number-input-decrement-stepper {
93
+ border-inline-start-width: 1px;
94
+ border-inline-start-style: solid;
95
+ border-inline-start-color: #ccc;
96
+ border-top: 1px solid #ccc;
97
+ font-size: calc(0.75rem);
98
+ display: flex;
99
+ justify-content: center;
100
+ align-items: center;
101
+ flex: 1 1 0%;
102
+ max-height: 50%;
103
+ user-select: none;
104
+ cursor: pointer;
105
+ border-bottom-right-radius: var(--tremolo-number-input-border-radius);
106
+
107
+ &[aria-disabled='false']:active {
108
+ background-color: #3443560e;
109
+ }
110
+
111
+ &[aria-disabled='true'] {
112
+ cursor: not-allowed;
113
+ }
26
114
  }