@tremolo-ui/react 0.0.3 → 0.0.4

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 (129) hide show
  1. package/dist/cjs/index.js +113 -119
  2. package/dist/cjs/index.js.map +1 -1
  3. package/dist/cjs/types/components/AnimationCanvas/canvas.d.ts.map +1 -0
  4. package/dist/cjs/types/components/AnimationCanvas/index.d.ts +20 -0
  5. package/dist/cjs/types/components/AnimationCanvas/index.d.ts.map +1 -0
  6. package/dist/{esm/types/packages/react/src → cjs/types}/components/Knob/index.d.ts +16 -4
  7. package/dist/cjs/types/components/Knob/index.d.ts.map +1 -0
  8. package/dist/cjs/types/{packages/react/src/components → components}/NumberInput/index.d.ts +3 -5
  9. package/dist/cjs/types/components/NumberInput/index.d.ts.map +1 -0
  10. package/dist/{esm/types/packages/react/src → cjs/types}/components/Slider/Thumb.d.ts +2 -2
  11. package/dist/cjs/types/components/Slider/Thumb.d.ts.map +1 -0
  12. package/dist/cjs/types/{packages/react/src/components → components}/Slider/Track.d.ts +1 -1
  13. package/dist/cjs/types/components/Slider/Track.d.ts.map +1 -0
  14. package/dist/cjs/types/{packages/react/src/components → components}/Slider/index.d.ts +5 -5
  15. package/dist/cjs/types/components/Slider/index.d.ts.map +1 -0
  16. package/dist/cjs/types/components/Slider/type.d.ts +7 -0
  17. package/dist/cjs/types/components/Slider/type.d.ts.map +1 -0
  18. package/dist/cjs/types/hooks/useCallbackRef.d.ts.map +1 -0
  19. package/dist/cjs/types/hooks/useEventListener.d.ts.map +1 -0
  20. package/dist/cjs/types/hooks/useRefCallbackEvent.d.ts.map +1 -0
  21. package/dist/cjs/types/index.d.ts +5 -0
  22. package/dist/cjs/types/index.d.ts.map +1 -0
  23. package/dist/cjs/types/system/pseudo.d.ts.map +1 -0
  24. package/dist/esm/index.js +111 -109
  25. package/dist/esm/index.js.map +1 -1
  26. package/dist/esm/types/components/AnimationCanvas/canvas.d.ts.map +1 -0
  27. package/dist/esm/types/components/AnimationCanvas/index.d.ts +20 -0
  28. package/dist/esm/types/components/AnimationCanvas/index.d.ts.map +1 -0
  29. package/dist/{cjs/types/packages/react/src → esm/types}/components/Knob/index.d.ts +16 -4
  30. package/dist/esm/types/components/Knob/index.d.ts.map +1 -0
  31. package/dist/esm/types/{packages/react/src/components → components}/NumberInput/index.d.ts +3 -5
  32. package/dist/esm/types/components/NumberInput/index.d.ts.map +1 -0
  33. package/dist/{cjs/types/packages/react/src → esm/types}/components/Slider/Thumb.d.ts +2 -2
  34. package/dist/esm/types/components/Slider/Thumb.d.ts.map +1 -0
  35. package/dist/esm/types/{packages/react/src/components → components}/Slider/Track.d.ts +1 -1
  36. package/dist/esm/types/components/Slider/Track.d.ts.map +1 -0
  37. package/dist/esm/types/{packages/react/src/components → components}/Slider/index.d.ts +5 -5
  38. package/dist/esm/types/components/Slider/index.d.ts.map +1 -0
  39. package/dist/esm/types/components/Slider/type.d.ts +7 -0
  40. package/dist/esm/types/components/Slider/type.d.ts.map +1 -0
  41. package/dist/esm/types/hooks/useCallbackRef.d.ts.map +1 -0
  42. package/dist/esm/types/hooks/useEventListener.d.ts.map +1 -0
  43. package/dist/esm/types/hooks/useRefCallbackEvent.d.ts.map +1 -0
  44. package/dist/esm/types/index.d.ts +5 -0
  45. package/dist/esm/types/index.d.ts.map +1 -0
  46. package/dist/esm/types/system/pseudo.d.ts.map +1 -0
  47. package/dist/styles/variables.css +4 -0
  48. package/package.json +19 -4
  49. package/src/Globals.d.ts +2 -0
  50. package/src/components/AnimationCanvas/canvas.ts +30 -0
  51. package/src/components/AnimationCanvas/index.tsx +169 -0
  52. package/src/components/Knob/index.tsx +292 -0
  53. package/src/components/NumberInput/index.tsx +165 -0
  54. package/src/components/Slider/Thumb.tsx +54 -0
  55. package/src/components/Slider/Track.tsx +20 -0
  56. package/src/components/Slider/index.tsx +326 -0
  57. package/src/components/Slider/type.ts +16 -0
  58. package/src/hooks/useCallbackRef.ts +20 -0
  59. package/src/hooks/useEventListener.ts +49 -0
  60. package/src/hooks/useRefCallbackEvent.ts +32 -0
  61. package/src/index.ts +4 -0
  62. package/src/styles/variables.css +4 -0
  63. package/src/system/pseudo.ts +55 -0
  64. package/dist/cjs/index.d.ts +0 -140
  65. package/dist/cjs/packages/react/tsconfig.tsbuildinfo +0 -1
  66. package/dist/cjs/types/packages/functions/components/NumberInput/type.d.ts +0 -14
  67. package/dist/cjs/types/packages/functions/components/NumberInput/type.d.ts.map +0 -1
  68. package/dist/cjs/types/packages/functions/components/Slider/type.d.ts +0 -32
  69. package/dist/cjs/types/packages/functions/components/Slider/type.d.ts.map +0 -1
  70. package/dist/cjs/types/packages/functions/math.d.ts +0 -12
  71. package/dist/cjs/types/packages/functions/math.d.ts.map +0 -1
  72. package/dist/cjs/types/packages/functions/types.d.ts +0 -2
  73. package/dist/cjs/types/packages/functions/types.d.ts.map +0 -1
  74. package/dist/cjs/types/packages/functions/util.d.ts +0 -6
  75. package/dist/cjs/types/packages/functions/util.d.ts.map +0 -1
  76. package/dist/cjs/types/packages/react/src/components/AnimationCanvas/canvas.d.ts.map +0 -1
  77. package/dist/cjs/types/packages/react/src/components/AnimationCanvas/index.d.ts +0 -20
  78. package/dist/cjs/types/packages/react/src/components/AnimationCanvas/index.d.ts.map +0 -1
  79. package/dist/cjs/types/packages/react/src/components/Knob/index.d.ts.map +0 -1
  80. package/dist/cjs/types/packages/react/src/components/NumberInput/index.d.ts.map +0 -1
  81. package/dist/cjs/types/packages/react/src/components/Slider/Thumb.d.ts.map +0 -1
  82. package/dist/cjs/types/packages/react/src/components/Slider/Track.d.ts.map +0 -1
  83. package/dist/cjs/types/packages/react/src/components/Slider/index.d.ts.map +0 -1
  84. package/dist/cjs/types/packages/react/src/components/Slider/type.d.ts +0 -6
  85. package/dist/cjs/types/packages/react/src/components/Slider/type.d.ts.map +0 -1
  86. package/dist/cjs/types/packages/react/src/hooks/useCallbackRef.d.ts.map +0 -1
  87. package/dist/cjs/types/packages/react/src/hooks/useEventListener.d.ts.map +0 -1
  88. package/dist/cjs/types/packages/react/src/hooks/useRefCallbackEvent.d.ts.map +0 -1
  89. package/dist/cjs/types/packages/react/src/index.d.ts +0 -7
  90. package/dist/cjs/types/packages/react/src/index.d.ts.map +0 -1
  91. package/dist/cjs/types/packages/react/src/system/pseudo.d.ts.map +0 -1
  92. package/dist/esm/index.d.ts +0 -140
  93. package/dist/esm/packages/react/tsconfig.tsbuildinfo +0 -1
  94. package/dist/esm/types/packages/functions/components/NumberInput/type.d.ts +0 -14
  95. package/dist/esm/types/packages/functions/components/NumberInput/type.d.ts.map +0 -1
  96. package/dist/esm/types/packages/functions/components/Slider/type.d.ts +0 -32
  97. package/dist/esm/types/packages/functions/components/Slider/type.d.ts.map +0 -1
  98. package/dist/esm/types/packages/functions/math.d.ts +0 -12
  99. package/dist/esm/types/packages/functions/math.d.ts.map +0 -1
  100. package/dist/esm/types/packages/functions/types.d.ts +0 -2
  101. package/dist/esm/types/packages/functions/types.d.ts.map +0 -1
  102. package/dist/esm/types/packages/functions/util.d.ts +0 -6
  103. package/dist/esm/types/packages/functions/util.d.ts.map +0 -1
  104. package/dist/esm/types/packages/react/src/components/AnimationCanvas/canvas.d.ts.map +0 -1
  105. package/dist/esm/types/packages/react/src/components/AnimationCanvas/index.d.ts +0 -20
  106. package/dist/esm/types/packages/react/src/components/AnimationCanvas/index.d.ts.map +0 -1
  107. package/dist/esm/types/packages/react/src/components/Knob/index.d.ts.map +0 -1
  108. package/dist/esm/types/packages/react/src/components/NumberInput/index.d.ts.map +0 -1
  109. package/dist/esm/types/packages/react/src/components/Slider/Thumb.d.ts.map +0 -1
  110. package/dist/esm/types/packages/react/src/components/Slider/Track.d.ts.map +0 -1
  111. package/dist/esm/types/packages/react/src/components/Slider/index.d.ts.map +0 -1
  112. package/dist/esm/types/packages/react/src/components/Slider/type.d.ts +0 -6
  113. package/dist/esm/types/packages/react/src/components/Slider/type.d.ts.map +0 -1
  114. package/dist/esm/types/packages/react/src/hooks/useCallbackRef.d.ts.map +0 -1
  115. package/dist/esm/types/packages/react/src/hooks/useEventListener.d.ts.map +0 -1
  116. package/dist/esm/types/packages/react/src/hooks/useRefCallbackEvent.d.ts.map +0 -1
  117. package/dist/esm/types/packages/react/src/index.d.ts +0 -7
  118. package/dist/esm/types/packages/react/src/index.d.ts.map +0 -1
  119. package/dist/esm/types/packages/react/src/system/pseudo.d.ts.map +0 -1
  120. /package/dist/cjs/types/{packages/react/src/components → components}/AnimationCanvas/canvas.d.ts +0 -0
  121. /package/dist/cjs/types/{packages/react/src/hooks → hooks}/useCallbackRef.d.ts +0 -0
  122. /package/dist/cjs/types/{packages/react/src/hooks → hooks}/useEventListener.d.ts +0 -0
  123. /package/dist/cjs/types/{packages/react/src/hooks → hooks}/useRefCallbackEvent.d.ts +0 -0
  124. /package/dist/cjs/types/{packages/react/src/system → system}/pseudo.d.ts +0 -0
  125. /package/dist/esm/types/{packages/react/src/components → components}/AnimationCanvas/canvas.d.ts +0 -0
  126. /package/dist/esm/types/{packages/react/src/hooks → hooks}/useCallbackRef.d.ts +0 -0
  127. /package/dist/esm/types/{packages/react/src/hooks → hooks}/useEventListener.d.ts +0 -0
  128. /package/dist/esm/types/{packages/react/src/hooks → hooks}/useRefCallbackEvent.d.ts +0 -0
  129. /package/dist/esm/types/{packages/react/src/system → system}/pseudo.d.ts +0 -0
@@ -0,0 +1,169 @@
1
+ import { css, CSSObject } from '@emotion/react'
2
+ import { MutableRefObject, useEffect, useRef } from 'react'
3
+ import clsx from 'clsx'
4
+
5
+ import { DrawingContext, drawingState, DrawingStateValue } from './canvas'
6
+
7
+ function setDprConfig(
8
+ canvas: HTMLCanvasElement,
9
+ context: CanvasRenderingContext2D,
10
+ width: number,
11
+ height: number,
12
+ dpr: number,
13
+ ) {
14
+ canvas.width = width * dpr
15
+ canvas.height = height * dpr
16
+ context.scale(dpr, dpr)
17
+ canvas.style.width = `${width}px`
18
+ canvas.style.height = `${height}px`
19
+ }
20
+
21
+ export type DrawFunction = (
22
+ context: CanvasRenderingContext2D,
23
+ width: MutableRefObject<number>,
24
+ height: MutableRefObject<number>,
25
+ count: number,
26
+ ) => void
27
+
28
+ interface AnimationCanvasProps {
29
+ width?: number
30
+ height?: number
31
+ relativeSize?: boolean
32
+ /** only available relativeSize=true */
33
+ reduceFlickering?: boolean
34
+ options?: CanvasRenderingContext2DSettings
35
+ style?: CSSObject
36
+ init?: DrawFunction
37
+ draw: DrawFunction
38
+ }
39
+
40
+ /**
41
+ * A simple animatable canvas with requestAnimationFrame()
42
+ */
43
+ export function AnimationCanvas({
44
+ width = 100,
45
+ height = 100,
46
+ style,
47
+ relativeSize = false,
48
+ reduceFlickering = true,
49
+ options,
50
+ init,
51
+ draw,
52
+ className,
53
+ ...props
54
+ }: AnimationCanvasProps &
55
+ Omit<
56
+ React.ClassAttributes<HTMLCanvasElement> &
57
+ React.CanvasHTMLAttributes<HTMLCanvasElement>,
58
+ 'ref' | 'width' | 'height' | 'style'
59
+ >) {
60
+ const canvasRef = useRef<HTMLCanvasElement>(null)
61
+ const memoCanvasRef = useRef<HTMLCanvasElement>(null)
62
+ const reqIdRef = useRef<number>()
63
+ const widthRef = useRef(0)
64
+ const heightRef = useRef(0)
65
+
66
+ const loop = (
67
+ context: CanvasRenderingContext2D,
68
+ width: MutableRefObject<number>,
69
+ height: MutableRefObject<number>,
70
+ count: number,
71
+ ) => {
72
+ reqIdRef.current = requestAnimationFrame(() =>
73
+ loop(context, width, height, count + 1),
74
+ )
75
+ draw(context, width, height, count + 1)
76
+ }
77
+
78
+ useEffect(() => {
79
+ if (!canvasRef.current) return
80
+ const canvas = canvasRef.current
81
+ const context = canvas.getContext('2d', options)
82
+ if (!context) throw new Error('Cannot get canvas context.')
83
+ const dpr = window.devicePixelRatio
84
+
85
+ if (relativeSize) {
86
+ const parent = canvas.parentElement
87
+ if (!parent) throw new Error("Canvas doesn't have a parent element.")
88
+ const memoCanvas = memoCanvasRef.current
89
+ const memoContext = memoCanvas?.getContext('2d', options)
90
+
91
+ const resizeObserver = new ResizeObserver(() => {
92
+ // console.log('resize')
93
+ // Prevents loss of some context when the canvas is resized
94
+ const contextMemo = {} as DrawingContext
95
+ for (const prop of drawingState) {
96
+ (contextMemo[prop] as DrawingStateValue) = context[prop]
97
+ }
98
+
99
+ const w = Math.floor(parent.clientWidth)
100
+ const h = Math.floor(parent.clientHeight)
101
+
102
+ if (reduceFlickering && memoCanvas && memoContext) {
103
+ memoCanvas.width = w * dpr
104
+ memoCanvas.height = h * dpr
105
+ memoContext.scale(1 / dpr, 1 / dpr)
106
+ memoContext.drawImage(context.canvas, 0, 0)
107
+ }
108
+
109
+ setDprConfig(canvas, context, w, h, dpr)
110
+ widthRef.current = w
111
+ heightRef.current = h
112
+
113
+ for (const prop of drawingState) {
114
+ (context[prop] as DrawingStateValue) = contextMemo[prop]
115
+ }
116
+
117
+ if (reduceFlickering && memoContext) {
118
+ context.drawImage(memoContext.canvas, 0, 0)
119
+ }
120
+ })
121
+ resizeObserver.observe(parent)
122
+
123
+ const w = Math.floor(parent.clientWidth)
124
+ const h = Math.floor(parent.clientHeight)
125
+ setDprConfig(canvas, context, w, h, dpr)
126
+
127
+ if (init) init(context, widthRef, heightRef, 0)
128
+ loop(context, widthRef, heightRef, 0)
129
+
130
+ return () => {
131
+ if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current)
132
+ resizeObserver.unobserve(parent)
133
+ }
134
+ } else {
135
+ const rect = canvas.getBoundingClientRect()
136
+ setDprConfig(canvas, context, rect.width, rect.height, dpr)
137
+ widthRef.current = rect.width
138
+ heightRef.current = rect.height
139
+
140
+ if (init) init(context, widthRef, heightRef, 0)
141
+ loop(context, widthRef, heightRef, 0)
142
+
143
+ return () => {
144
+ if (reqIdRef.current) cancelAnimationFrame(reqIdRef.current)
145
+ }
146
+ }
147
+ }, [loop])
148
+
149
+ return (
150
+ <>
151
+ <canvas
152
+ className={clsx('tremolo-animation-canvas', className)}
153
+ width={relativeSize ? 0 : width}
154
+ height={relativeSize ? 0 : height}
155
+ css={css(style)}
156
+ ref={canvasRef}
157
+ {...props}
158
+ ></canvas>
159
+ {relativeSize && reduceFlickering && (
160
+ <canvas
161
+ style={{ display: 'none' }}
162
+ width={relativeSize ? 0 : width}
163
+ height={relativeSize ? 0 : height}
164
+ ref={memoCanvasRef}
165
+ ></canvas>
166
+ )}
167
+ </>
168
+ )
169
+ }
@@ -0,0 +1,292 @@
1
+ import { css, CSSObject, Global } from '@emotion/react'
2
+ import { clamp, normalizeValue, radian, rawValue, stepValue } from '@tremolo-ui/functions'
3
+ import { WheelOption } from '@tremolo-ui/functions'
4
+ import React, { useRef } from 'react'
5
+ import clsx from 'clsx'
6
+
7
+ import { useEventListener } from '../../hooks/useEventListener'
8
+ import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
9
+ import { UserActionPseudoProps } from '../../system/pseudo'
10
+ import { AnimationCanvas, DrawFunction } from '../AnimationCanvas'
11
+
12
+ interface KnobProps {
13
+ // required
14
+ value: number
15
+ min: number
16
+ max: number
17
+
18
+ // optional
19
+ /**
20
+ * value set when double-clicking
21
+ * @see enableDoubleClickDefault
22
+ */
23
+ defaultValue?: number
24
+
25
+ /**
26
+ * Value to be used as the starting point of the line when drawing.
27
+ * @default min
28
+ */
29
+ startValue?: number
30
+
31
+ step?: number
32
+ skew?: number // | SkewFunction
33
+
34
+ /** Priority over width or height */
35
+ size?: number | `${number}%`
36
+
37
+ width?: number | `${number}%`
38
+ height?: number | `${number}%`
39
+
40
+ /** drawing options */
41
+ options?: {
42
+ active?: string // active line color
43
+ inactive?: string // inactive line color
44
+ bg?: string // background color
45
+ thumb?: string // thumb color
46
+ lineWeight?: number // line color
47
+ thumbWeight?: number // thumb weight
48
+ }
49
+ // TODO: need?
50
+ draw?: DrawFunction
51
+
52
+ /** Whether to apply {use-select: none} when dragging */
53
+ bodyNoSelect?: boolean
54
+
55
+ enableWheel?: WheelOption
56
+ enableDoubleClickDefault?: boolean
57
+ style?: CSSObject
58
+ onChange?: (value: number) => void
59
+ }
60
+
61
+ const defaultOptions = {
62
+ active: '#7998ec',
63
+ inactive: '#eee',
64
+ bg: '#ccc',
65
+ thumb: '#4e76e6',
66
+ lineWeight: 6,
67
+ thumbWeight: 4,
68
+ } as const
69
+
70
+ /**
71
+ * simple rotary Knob.
72
+ */
73
+ export function Knob({
74
+ value,
75
+ min,
76
+ max,
77
+ defaultValue = min,
78
+ startValue = min,
79
+ step = 1,
80
+ skew = 1,
81
+ size,
82
+ width = 50,
83
+ height = 50,
84
+ options = defaultOptions,
85
+ draw,
86
+ bodyNoSelect = true,
87
+ enableWheel,
88
+ enableDoubleClickDefault = true,
89
+ style,
90
+ className,
91
+ onChange,
92
+ _active,
93
+ _focus,
94
+ _hover,
95
+ ...props
96
+ }: KnobProps &
97
+ UserActionPseudoProps &
98
+ Omit<
99
+ React.ClassAttributes<HTMLDivElement> &
100
+ React.HTMLAttributes<HTMLDivElement>,
101
+ 'onChange'
102
+ >) {
103
+ // -- state and ref ---
104
+ // const [privateValue, setPrivateValue] = useState(value);
105
+ const dragOffsetY = useRef<number | undefined>(undefined)
106
+
107
+ // --- interpret props ---
108
+ const opts = { ...defaultOptions, ...options }
109
+ // const percent = normalizeValue(value, min, max, skew)
110
+
111
+ const isRelativeSize = typeof (size ?? width) == 'string'
112
+ const adaptSize = (size: number | `${number}%`) => {
113
+ if (typeof size == 'string') {
114
+ return Number(size.slice(0, -1))
115
+ } else {
116
+ return size
117
+ }
118
+ }
119
+
120
+ const handleEvent = (
121
+ event:
122
+ | MouseEvent
123
+ | React.MouseEvent<HTMLDivElement, MouseEvent>
124
+ | TouchEvent,
125
+ ) => {
126
+ const isTouch = event instanceof TouchEvent
127
+ if (isTouch && event.cancelable) event.preventDefault()
128
+ if (dragOffsetY.current) {
129
+ if (bodyNoSelect) document.body.classList.add('no-select')
130
+ const screenY = isTouch ? event.touches[0].screenY : event.screenY
131
+ const delta = dragOffsetY.current - screenY
132
+ dragOffsetY.current = screenY
133
+ const n = normalizeValue(value, min, max, skew)
134
+ const v = rawValue(n + delta / 100, min, max, skew)
135
+ const v2 = clamp(stepValue(v, step), min, max)
136
+ if (onChange) onChange(v2)
137
+ }
138
+ }
139
+
140
+ // --- hooks ---
141
+ const wheelRefCallback = useRefCallbackEvent(
142
+ 'wheel',
143
+ (event) => {
144
+ if (!enableWheel) return
145
+ event.preventDefault()
146
+ const x = event.deltaY > 0 ? -enableWheel[1] : enableWheel[1]
147
+ let v
148
+ if (enableWheel[0] == 'normalized') {
149
+ const n = normalizeValue(value, min, max, skew)
150
+ v = rawValue(n + x, min, max, skew)
151
+ } else {
152
+ v = value + x
153
+ }
154
+ if (onChange) onChange(clamp(stepValue(v, step), min, max))
155
+ },
156
+ { passive: false },
157
+ [enableWheel, value, min, max, skew, step, onChange],
158
+ )
159
+
160
+ const touchMoveRefCallback = useRefCallbackEvent(
161
+ 'touchmove',
162
+ handleEvent,
163
+ { passive: false },
164
+ [value, min, max, skew, step, onChange, bodyNoSelect],
165
+ )
166
+
167
+ useEventListener(window, 'mousemove', handleEvent)
168
+
169
+ useEventListener(window, 'pointerup', () => {
170
+ dragOffsetY.current = undefined
171
+ if (bodyNoSelect) document.body.classList.remove('no-select')
172
+ })
173
+
174
+ return (
175
+ <div
176
+ className={clsx('tremolo-knob', className)}
177
+ ref={(div) => {
178
+ wheelRefCallback(div)
179
+ touchMoveRefCallback(div)
180
+ }}
181
+ role="slider"
182
+ tabIndex={0}
183
+ aria-valuenow={value}
184
+ aria-valuemax={max}
185
+ aria-valuemin={min}
186
+ css={css({
187
+ display: 'block',
188
+ width: size ?? width,
189
+ height: size ?? height,
190
+ cursor: 'pointer',
191
+ ...style,
192
+ ':active': {
193
+ ..._active,
194
+ },
195
+ ':focus': {
196
+ ..._focus,
197
+ },
198
+ ':hover': {
199
+ ..._hover,
200
+ },
201
+ })}
202
+ onPointerDown={(event) => {
203
+ dragOffsetY.current = event.screenY
204
+ handleEvent(event)
205
+ }}
206
+ onDoubleClick={() => {
207
+ if (enableDoubleClickDefault && onChange) {
208
+ onChange(defaultValue)
209
+ }
210
+ }}
211
+ {...props}
212
+ >
213
+ <Global
214
+ styles={{
215
+ '.no-select': {
216
+ userSelect: 'none',
217
+ },
218
+ }}
219
+ />
220
+ <AnimationCanvas
221
+ width={adaptSize(size ?? width)}
222
+ height={adaptSize(size ?? height)}
223
+ relativeSize={isRelativeSize}
224
+ // TODO
225
+ // reduceFlickering={false}
226
+ draw={draw ? draw : (ctx, width, height) => {
227
+ const p = normalizeValue(value, min, max, skew)
228
+ const startP = normalizeValue(startValue, min, max, skew)
229
+ const w = width.current,
230
+ cx = w / 2
231
+ const h = height.current,
232
+ cy = h / 2
233
+ const r = Math.min(cx, cy)
234
+ if (r <= 0) return
235
+
236
+ // reset
237
+ ctx.clearRect(0, 0, w, h)
238
+ ctx.lineCap = 'butt'
239
+
240
+ // bg
241
+ ctx.beginPath()
242
+ ctx.fillStyle = opts.bg
243
+ ctx.arc(cx, cy, r - 0.5, 0, 2 * Math.PI)
244
+ ctx.fill()
245
+
246
+ ctx.lineCap = 'round'
247
+ ctx.lineWidth = opts.lineWeight
248
+
249
+ // inactive rotary
250
+ ctx.beginPath()
251
+ ctx.strokeStyle = opts.inactive
252
+ ctx.arc(
253
+ cx,
254
+ cy,
255
+ r - 0.5 - opts.lineWeight / 2,
256
+ radian(135),
257
+ radian(45),
258
+ )
259
+ ctx.stroke()
260
+
261
+ // active rotary
262
+ ctx.beginPath()
263
+ ctx.strokeStyle = opts.active
264
+ ctx.arc(
265
+ cx,
266
+ cy,
267
+ r - 0.5 - opts.lineWeight / 2,
268
+ radian(135 + 270 * startP),
269
+ radian(135 + 270 * p),
270
+ p < startP,
271
+ )
272
+ ctx.stroke()
273
+
274
+ // thumb
275
+ ctx.beginPath()
276
+ ctx.moveTo(cx, cy)
277
+ ctx.lineTo(
278
+ cx +
279
+ Math.cos(radian(135 + 270 * p)) *
280
+ (r - 0.5 - opts.thumbWeight / 2),
281
+ cy +
282
+ Math.sin(radian(135 + 270 * p)) *
283
+ (r - 0.5 - opts.thumbWeight / 2),
284
+ )
285
+ ctx.strokeStyle = opts.thumb
286
+ ctx.lineWidth = opts.thumbWeight
287
+ ctx.stroke()
288
+ }}
289
+ />
290
+ </div>
291
+ )
292
+ }
@@ -0,0 +1,165 @@
1
+ import { css, CSSObject } from '@emotion/react'
2
+ import { clamp } from '@tremolo-ui/functions'
3
+ import { parseValue, Units } from '@tremolo-ui/functions/NumberInput'
4
+ import { ChangeEvent, useState } from 'react'
5
+ import clsx from 'clsx'
6
+
7
+ import { InputPseudoProps, UserActionPseudoProps } from '../../system/pseudo'
8
+
9
+ interface NumberInputProps {
10
+ value: number | string
11
+
12
+ min?: number
13
+ max?: number
14
+ step?: number
15
+ defaultValue?: number
16
+
17
+ // wrapping html input
18
+ disabled?: boolean
19
+ placeholder?: string
20
+ readOnly?: boolean
21
+ tabIndex?: number
22
+
23
+ /**
24
+ * @example
25
+ * units='Hz'
26
+ * units={[['Hz', 1], ['kHz', 1000]]}
27
+ * units={[['ms', 1], ['s', 1000]]}
28
+ */
29
+ units?: string | Units
30
+
31
+ /**
32
+ * NOTE: If units is specified, it is forced to `false`
33
+ */
34
+ strict?: boolean
35
+
36
+ /**
37
+ * @example
38
+ * with units-{[['Hz', 1], ['kHz', 1000]]}
39
+ * 3 ... 100Hz, 1.60Hz
40
+ */
41
+ digit?: number
42
+ selectWithFocus?: 'all' | 'number' | 'none'
43
+ blurWithEnter?: boolean
44
+ className?: string
45
+ style?: CSSObject
46
+ onChange?: (value: number, event: ChangeEvent<HTMLInputElement>) => void
47
+ onFocus?: (
48
+ value: number,
49
+ event: React.FocusEvent<HTMLInputElement, Element>,
50
+ ) => void
51
+ onBlur?: (
52
+ value: number,
53
+ event: React.FocusEvent<HTMLInputElement, Element>,
54
+ ) => void
55
+ }
56
+
57
+ /**
58
+ * Input with some useful functions for entering numerical values.
59
+ */
60
+ export function NumberInput({
61
+ value,
62
+ min,
63
+ max,
64
+ step,
65
+ defaultValue = min,
66
+ units,
67
+ disabled = false,
68
+ placeholder,
69
+ readOnly,
70
+ tabIndex,
71
+ strict = false,
72
+ digit,
73
+ selectWithFocus = 'none',
74
+ blurWithEnter = true,
75
+ className,
76
+ style,
77
+ onChange,
78
+ onFocus,
79
+ onBlur,
80
+ ...pseudo
81
+ }: NumberInputProps & UserActionPseudoProps & InputPseudoProps) {
82
+ const [showValue, setShowValue] = useState(
83
+ parseValue(String(value), units, digit, defaultValue).formatValue,
84
+ )
85
+ const calculatedStrict = units ? false : strict
86
+ const { _active, _focus, _hover } = pseudo
87
+
88
+ return (
89
+ <input
90
+ className={clsx('tremolo-number-input', className)}
91
+ value={showValue}
92
+ min={min}
93
+ max={max}
94
+ step={step}
95
+ type={calculatedStrict ? 'number' : 'text'}
96
+ spellCheck={'false'}
97
+ disabled={disabled}
98
+ placeholder={placeholder}
99
+ readOnly={readOnly}
100
+ tabIndex={tabIndex}
101
+ css={css({
102
+ display: 'block',
103
+ font: 'inherit',
104
+ margin: 0,
105
+ paddingTop: 6,
106
+ paddingBottom: 6,
107
+ paddingLeft: 10,
108
+ paddingRight: 10,
109
+ borderStyle: 'solid',
110
+ borderWidth: 1,
111
+ borderColor: '#bbb',
112
+ borderRadius: 4,
113
+ outline: 0,
114
+ transition: 'all 0.1s linear',
115
+ appearance: 'textfield',
116
+ '&:hover': {
117
+ borderColor: 'var(--tremolo-theme-color)',
118
+ ..._hover,
119
+ },
120
+ '&:focus': {
121
+ borderColor: 'var(--tremolo-theme-color)',
122
+ boxShadow: '0px 0px 0px 2px rgba(var(--tremolo-theme-color-rgb), 0.1)',
123
+ ..._focus,
124
+ },
125
+ ...(_active && { '&:active': { ..._active } }),
126
+ ...style,
127
+ })}
128
+ onChange={(event) => {
129
+ const v = event.currentTarget.value
130
+ setShowValue(v)
131
+ const newValue = clamp(
132
+ parseValue(v, units, digit, defaultValue).rawValue,
133
+ min ?? -Infinity,
134
+ max ?? Infinity
135
+ )
136
+ if (onChange) onChange(newValue, event)
137
+ }}
138
+ onFocus={(event) => {
139
+ const v = event.currentTarget.value
140
+ const parsed = parseValue(v, units, digit, defaultValue)
141
+ if (selectWithFocus == 'all') {
142
+ event.currentTarget.setSelectionRange(0, v.length)
143
+ } else if (selectWithFocus == 'number') {
144
+ event.currentTarget.setSelectionRange(
145
+ 0,
146
+ parsed.formatValue.length - parsed.unit.length,
147
+ )
148
+ }
149
+ if (onFocus) onFocus(parsed.rawValue, event)
150
+ }}
151
+ onBlur={(event) => {
152
+ // update value
153
+ const v = event.currentTarget.value
154
+ const parsed = parseValue(v, units, digit, defaultValue)
155
+ setShowValue(parsed.formatValue)
156
+ if (onBlur) onBlur(parsed.rawValue, event)
157
+ }}
158
+ onKeyDown={(event) => {
159
+ if (blurWithEnter && event.key == 'Enter') {
160
+ event.currentTarget.blur()
161
+ }
162
+ }}
163
+ />
164
+ )
165
+ }
@@ -0,0 +1,54 @@
1
+ import { CSSObject, css } from '@emotion/react'
2
+ import { ReactNode } from 'react'
3
+
4
+ interface SliderThumbProps {
5
+ color?: string
6
+ style?: CSSObject
7
+ __css?: CSSObject
8
+ children?: ReactNode
9
+ }
10
+
11
+ export function SliderThumb({
12
+ color,
13
+ children,
14
+ style,
15
+ __css,
16
+ }: SliderThumbProps) {
17
+ return (
18
+ <div
19
+ className="tremolo-slider-thumb-wrapper"
20
+ css={css({
21
+ width: 'fit-content',
22
+ height: 'fit-content',
23
+ position: 'absolute',
24
+ translate: '-50% -50%',
25
+ zIndex: 100,
26
+ ...__css,
27
+ })}
28
+ >
29
+ {children ? (
30
+ children
31
+ ) : (
32
+ // default slider thumb
33
+ <div
34
+ className="tremolo-slider-thumb"
35
+ // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
36
+ tabIndex={0}
37
+ css={css({
38
+ background: color,
39
+ width: '1.4rem',
40
+ height: '1.4rem',
41
+ borderRadius: '50%',
42
+ '&:hover': {
43
+ background: `color-mix(in srgb, ${color} 95%, black)`
44
+ },
45
+ '&:active': {
46
+ boxShadow: '0px 0px 0px 3px rgba(var(--tremolo-theme-color-rgb), 0.2)',
47
+ },
48
+ ...style,
49
+ })}
50
+ ></div>
51
+ )}
52
+ </div>
53
+ )
54
+ }
@@ -0,0 +1,20 @@
1
+ import { css, CSSObject } from '@emotion/react'
2
+ import { ReactElement } from 'react'
3
+
4
+ interface SliderTrackProps {
5
+ style?: CSSObject
6
+ children?: ReactElement
7
+ __css?: CSSObject
8
+ }
9
+
10
+ export function SliderTrack({
11
+ children,
12
+ style,
13
+ __css,
14
+ }: SliderTrackProps) {
15
+ return (
16
+ <div className="tremolo-slider-track" css={css({ ...__css, ...style })}>
17
+ {children}
18
+ </div>
19
+ )
20
+ }