@tremolo-ui/react 0.1.6 → 0.2.1

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 (68) hide show
  1. package/dist/index.cjs +1329 -1152
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.css +238 -183
  4. package/dist/index.d.cts +367 -366
  5. package/dist/index.d.cts.map +1 -1
  6. package/dist/index.d.ts +367 -366
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1313 -1125
  9. package/dist/index.js.map +1 -1
  10. package/package.json +17 -19
  11. package/src/components/AnimationCanvas/index.tsx +0 -7
  12. package/src/components/DragObserver/index.tsx +9 -7
  13. package/src/components/Knob/ActiveLine.tsx +29 -0
  14. package/src/components/Knob/InactiveLine.tsx +53 -0
  15. package/src/components/Knob/SVGRoot.tsx +39 -0
  16. package/src/components/Knob/Thumb.tsx +64 -0
  17. package/src/components/Knob/context.tsx +120 -0
  18. package/src/components/Knob/index.css +25 -27
  19. package/src/components/Knob/index.tsx +110 -164
  20. package/src/components/NumberInput/DecrementStepper.tsx +0 -2
  21. package/src/components/NumberInput/IncrementStepper.tsx +0 -2
  22. package/src/components/NumberInput/InternalInput.tsx +3 -1
  23. package/src/components/NumberInput/Stepper.tsx +0 -2
  24. package/src/components/NumberInput/context.tsx +0 -1
  25. package/src/components/NumberInput/index.css +31 -15
  26. package/src/components/NumberInput/index.tsx +21 -14
  27. package/src/components/Piano/KeyLabel.tsx +0 -2
  28. package/src/components/Piano/context.tsx +0 -2
  29. package/src/components/Piano/index.css +7 -14
  30. package/src/components/Piano/index.tsx +14 -15
  31. package/src/components/Piano/key.tsx +0 -10
  32. package/src/components/Piano/keyboardShortcuts.ts +0 -2
  33. package/src/components/PointsEditor/Background.tsx +17 -0
  34. package/src/components/PointsEditor/Container.tsx +26 -0
  35. package/src/components/PointsEditor/Point.tsx +137 -0
  36. package/src/components/PointsEditor/context.tsx +91 -0
  37. package/src/components/PointsEditor/index.css +30 -0
  38. package/src/components/PointsEditor/index.tsx +129 -0
  39. package/src/components/Slider/Scale.tsx +0 -2
  40. package/src/components/Slider/ScaleOption.tsx +0 -2
  41. package/src/components/Slider/Thumb.tsx +1 -5
  42. package/src/components/Slider/Track.tsx +1 -3
  43. package/src/components/Slider/context.tsx +0 -1
  44. package/src/components/Slider/index.css +33 -8
  45. package/src/components/Slider/index.tsx +61 -33
  46. package/src/components/WheelObserver/index.tsx +17 -19
  47. package/src/components/XYPad/Area.tsx +4 -5
  48. package/src/components/XYPad/Thumb.tsx +8 -7
  49. package/src/components/XYPad/index.css +18 -4
  50. package/src/components/XYPad/index.tsx +71 -38
  51. package/src/components/_util/composeRefs.tsx +63 -0
  52. package/src/components/_util/index.ts +23 -6
  53. package/src/components/_util/type.ts +1 -0
  54. package/src/hooks/useAnimationFrame.ts +0 -3
  55. package/src/hooks/useCallbackRef.ts +2 -2
  56. package/src/hooks/useDrag.ts +12 -11
  57. package/src/hooks/useDragWithElement.ts +12 -17
  58. package/src/hooks/useEventListener.ts +6 -9
  59. package/src/hooks/useInterval.ts +0 -3
  60. package/src/hooks/useLongPress.ts +0 -3
  61. package/src/hooks/useMIDIAccess.ts +0 -1
  62. package/src/hooks/useMIDIInput.ts +0 -1
  63. package/src/hooks/useMIDIMessage.ts +0 -1
  64. package/src/hooks/usePianoDrag.ts +75 -0
  65. package/src/index.ts +21 -30
  66. package/src/styles/global.css +16 -4
  67. package/dist/index.css.map +0 -1
  68. package/src/components/AnimationKnob/index.tsx +0 -314
@@ -16,14 +16,14 @@
16
16
  }
17
17
 
18
18
  .tremolo-slider-thumb {
19
- --color: var(--tremolo-theme-color);
19
+ --color: oklch(59.5% 0.17548 266.373);
20
20
 
21
21
  background: var(--color);
22
22
  border-radius: 50%;
23
23
  outline: none;
24
24
 
25
25
  &[aria-disabled='true'] {
26
- background: #5d6478;
26
+ background: oklch(50.462% 0.03307 270.021);
27
27
  }
28
28
 
29
29
  &[aria-readonly='false']:focus {
@@ -33,8 +33,8 @@
33
33
  }
34
34
 
35
35
  .tremolo-slider-track {
36
- --active: #7998ec;
37
- --inactive: #eee;
36
+ --active: oklch(69.212% 0.12903 267.633);
37
+ --inactive: oklch(95% 0 0);
38
38
 
39
39
  position: relative;
40
40
 
@@ -47,13 +47,13 @@
47
47
  }
48
48
 
49
49
  &:hover {
50
- --active: #6387e9;
50
+ --active: oklch(64.276% 0.15244 266.932);
51
51
  --inactive: #e0e0e0;
52
52
  }
53
53
 
54
54
  &[aria-disabled='true'] {
55
- --active: #858890;
56
- --inactive: #ddd;
55
+ --active: oklch(75.327% 0.01237 264.547);
56
+ --inactive: oklch(89.755% 0.0001 271.152);
57
57
  }
58
58
  }
59
59
 
@@ -64,7 +64,7 @@
64
64
 
65
65
  .tremolo-slider-scale-option {
66
66
  display: flex;
67
- color: #222;
67
+ color: oklch(25.197% 0.00003 271.152);
68
68
  flex-direction: column;
69
69
  justify-content: center;
70
70
  align-items: center;
@@ -85,3 +85,28 @@
85
85
  .tremolo-slider-scale-option-label {
86
86
  text-align: right;
87
87
  }
88
+
89
+ :where(.dark, [data-theme='dark']) {
90
+ .tremolo-slider-thumb {
91
+ --color: oklch(64.5% 0.17548 266.373);
92
+
93
+ &:hover {
94
+ --color: oklch(67.5% 0.17548 266.373);
95
+ }
96
+ }
97
+
98
+ .tremolo-slider-track {
99
+ --active: oklch(59.5% 0.17548 266.373);
100
+ --inactive: oklch(41.7% 0 0);
101
+
102
+ &:hover {
103
+ --active: oklch(62.5% 0.17548 266.373);
104
+ --inactive: oklch(44.7% 0 0);
105
+ }
106
+
107
+ &[aria-disabled='true'] {
108
+ --active: oklch(75.327% 0.01237 264.547);
109
+ --inactive: oklch(89.755% 0.0001 271.152);
110
+ }
111
+ }
112
+ }
@@ -23,14 +23,25 @@ import {
23
23
 
24
24
  import { useDragWithElement } from '../../hooks/useDragWithElement'
25
25
  import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
26
- import { addNoSelect, removeNoSelect } from '../_util'
26
+ import {
27
+ addUserSelectNone,
28
+ Cursor,
29
+ removeUserSelectNone,
30
+ resetCursorStyle,
31
+ setCursorStyle,
32
+ } from '../_util'
27
33
 
28
34
  import { SliderProvider } from './context'
29
35
  import { Scale } from './Scale'
30
- import { SliderThumb, SliderThumbMethods, SliderThumbProps } from './Thumb'
31
- import { SliderTrack, SliderTrackProps } from './Track'
36
+ import { ScaleOption } from './ScaleOption'
37
+ import { Thumb, SliderThumbMethods, SliderThumbProps } from './Thumb'
38
+ import { Track, SliderTrackProps } from './Track'
39
+
40
+ const defaultExternalStyles: SliderProps['externalStyles'] = {
41
+ userSelectNone: true,
42
+ cursor: 'pointer',
43
+ }
32
44
 
33
- /** @category Slider */
34
45
  export interface SliderProps {
35
46
  // required
36
47
  value: number
@@ -40,24 +51,38 @@ export interface SliderProps {
40
51
  // optional
41
52
  step?: number
42
53
  skew?: number // TODO | SkewFunction
54
+ /**
55
+ * slider orientation
56
+ * aria-orientation property is also applied.
57
+ */
43
58
  vertical?: boolean
44
59
  reverse?: boolean
45
- bodyNoSelect?: boolean
60
+
61
+ /** Global style to apply when dragged */
62
+ externalStyles?: {
63
+ userSelectNone?: boolean
64
+ cursor?: Cursor
65
+ }
46
66
  /**
47
67
  * wheel control option
68
+ * If null, no event will be triggered
48
69
  */
49
70
  wheel?: InputEventOption | null
50
71
  /**
51
72
  * keyboard control option
73
+ * If null, no event will be triggered
52
74
  */
53
75
  keyboard?: InputEventOption | null
76
+
54
77
  /**
55
78
  * Only the appearance will change.
56
79
  * Please consider using with readonly.
80
+ * aria-disabled property is also applied.
57
81
  */
58
82
  disabled?: boolean
59
83
  /**
60
84
  * Make the value unchangeable.
85
+ * aria-readonly property is also applied.
61
86
  */
62
87
  readonly?: boolean
63
88
  className?: string
@@ -70,9 +95,6 @@ export interface SliderProps {
70
95
  // ReactElement<typeof SliderThumb, typeof SliderTrack>[]
71
96
  }
72
97
 
73
- /**
74
- * @category Slider
75
- */
76
98
  export interface SliderMethods {
77
99
  focus: () => void
78
100
  blur: () => void
@@ -81,11 +103,7 @@ export interface SliderMethods {
81
103
  type Props = SliderProps &
82
104
  Omit<ComponentPropsWithoutRef<'div'>, keyof SliderProps>
83
105
 
84
- /**
85
- * Customizable slider
86
- * @category Slider
87
- */
88
- export const Slider = forwardRef<SliderMethods, Props>(
106
+ const Root = forwardRef<SliderMethods, Props>(
89
107
  (
90
108
  {
91
109
  value,
@@ -95,7 +113,7 @@ export const Slider = forwardRef<SliderMethods, Props>(
95
113
  skew = 1,
96
114
  vertical = false,
97
115
  reverse = false,
98
- bodyNoSelect = true,
116
+ externalStyles: _externalStyles,
99
117
  wheel = ['raw', 1],
100
118
  keyboard = ['raw', 1],
101
119
  disabled = false,
@@ -119,6 +137,8 @@ export const Slider = forwardRef<SliderMethods, Props>(
119
137
  const thumbRef = useRef<SliderThumbMethods>(null)
120
138
 
121
139
  // --- interpret props ---
140
+ const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
141
+
122
142
  const p = toFixed(normalizeValue(value, min, max, skew) * 100)
123
143
  const rev = toFixed(100 - p)
124
144
  // NOTE
@@ -138,9 +158,9 @@ export const Slider = forwardRef<SliderMethods, Props>(
138
158
  if (children != undefined) {
139
159
  React.Children.forEach(children, (child) => {
140
160
  if (React.isValidElement(child)) {
141
- if (child.type == SliderThumb) {
161
+ if (child.type == Thumb) {
142
162
  thumbProps = child.props as SliderThumbProps
143
- } else if (child.type == SliderTrack) {
163
+ } else if (child.type == Track) {
144
164
  trackProps = child.props as SliderTrackProps
145
165
  } else if (child.type == Scale) {
146
166
  scaleComponent = child
@@ -195,13 +215,15 @@ export const Slider = forwardRef<SliderMethods, Props>(
195
215
  )
196
216
 
197
217
  // --- hooks ---
198
- const [touchMoveRefCallback, pointerDownHandler] =
218
+ const { refHandler: touchMoveRefCallback, pointerDownHandler } =
199
219
  useDragWithElement<HTMLDivElement>({
200
220
  baseElementRef: trackElementRef,
201
221
  onDrag: onDrag,
202
222
  onDragStart: (nx, ny) => {
203
223
  if (readonly) return
204
- if (bodyNoSelect) addNoSelect()
224
+ if (externalStyles.userSelectNone) addUserSelectNone()
225
+ if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
226
+
205
227
  thumbRef.current?.focus()
206
228
  onDragStart?.(
207
229
  clamp(
@@ -213,7 +235,10 @@ export const Slider = forwardRef<SliderMethods, Props>(
213
235
  },
214
236
  onDragEnd: (nx, ny) => {
215
237
  if (readonly) return
216
- if (bodyNoSelect) removeNoSelect()
238
+
239
+ if (externalStyles.userSelectNone) removeUserSelectNone()
240
+ if (externalStyles.cursor) resetCursorStyle()
241
+
217
242
  onDragEnd?.(
218
243
  clamp(
219
244
  stepValue(rawValue(vertical ? ny : nx, min, max, skew), step),
@@ -312,14 +337,10 @@ export const Slider = forwardRef<SliderMethods, Props>(
312
337
  }}
313
338
  >
314
339
  <div className="tremolo-slider-track-wrapper" ref={trackElementRef}>
315
- <SliderTrack
340
+ <Track
316
341
  __percent={percent}
317
342
  __thumb={
318
- <SliderThumb
319
- ref={thumbRef}
320
- __percent={percent}
321
- {...thumbProps}
322
- />
343
+ <Thumb ref={thumbRef} __percent={percent} {...thumbProps} />
323
344
  }
324
345
  {...trackProps}
325
346
  />
@@ -332,13 +353,20 @@ export const Slider = forwardRef<SliderMethods, Props>(
332
353
  },
333
354
  )
334
355
 
356
+ /**
357
+ * Customizable slider
358
+ */
359
+ export const Slider = {
360
+ Root,
361
+ Thumb,
362
+ Track,
363
+ Scale,
364
+ ScaleOption,
365
+ }
366
+
335
367
  export { useSliderContext } from './context'
336
- export {
337
- SliderThumb,
338
- type SliderThumbMethods,
339
- type SliderThumbProps,
340
- } from './Thumb'
341
- export { SliderTrack, type SliderTrackProps } from './Track'
342
- export { Scale, type ScaleProps } from './Scale'
343
- export { ScaleOption, type ScaleOptionProps } from './ScaleOption'
368
+ export { type SliderThumbMethods, type SliderThumbProps } from './Thumb'
369
+ export { type SliderTrackProps } from './Track'
370
+ export { type ScaleProps } from './Scale'
371
+ export { type ScaleOptionProps } from './ScaleOption'
344
372
  export { type ScaleOptions, type ScaleType } from './type'
@@ -1,20 +1,9 @@
1
- import { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
1
+ import { ComponentProps, ElementType, ReactNode } from 'react'
2
2
 
3
3
  import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
4
+ import { composeRefs } from '../_util/composeRefs'
5
+ import { Override } from '../_util/type'
4
6
 
5
- /**
6
- * @example
7
- * <WheelObserver
8
- * onWheel={(event) => {
9
- * event.preventDefault()
10
- * console.log(event.deltaY)
11
- * }
12
- * >
13
- * <div>Wheel here</div>
14
- * </WheelObserver>
15
- */
16
-
17
- /** @category WheelObserver */
18
7
  export interface WheelObserverProps<T extends ElementType> {
19
8
  /**
20
9
  * React.ElementType
@@ -26,12 +15,21 @@ export interface WheelObserverProps<T extends ElementType> {
26
15
  onWheel?: (event: WheelEvent) => void
27
16
  }
28
17
 
29
- /** @category WheelObserver */
18
+ /**
19
+ * @example
20
+ * <WheelObserver
21
+ * onWheel={(event) => {
22
+ * event.preventDefault()
23
+ * console.log(event.deltaY)
24
+ * }
25
+ * >
26
+ * <div>Wheel here</div>
27
+ * </WheelObserver>
28
+ */
30
29
  export function WheelObserver<T extends ElementType = 'div'>(
31
- props: WheelObserverProps<T> &
32
- Omit<ComponentPropsWithoutRef<T>, keyof WheelObserverProps<T>>,
30
+ props: Override<WheelObserverProps<T>, ComponentProps<T>>,
33
31
  ) {
34
- const { children, onWheel, as: Component = 'div', ...attributes } = props
32
+ const { as: Component = 'div', children, onWheel, ref, ...attributes } = props
35
33
 
36
34
  const wheelRefCallback = useRefCallbackEvent(
37
35
  'wheel',
@@ -43,7 +41,7 @@ export function WheelObserver<T extends ElementType = 'div'>(
43
41
  )
44
42
 
45
43
  return (
46
- <Component ref={wheelRefCallback} {...attributes}>
44
+ <Component ref={composeRefs(ref, wheelRefCallback)} {...attributes}>
47
45
  {children}
48
46
  </Component>
49
47
  )
@@ -1,8 +1,7 @@
1
1
  import clsx from 'clsx'
2
2
  import { ComponentPropsWithoutRef, CSSProperties, ReactElement } from 'react'
3
3
 
4
- /** @category XYPad */
5
- export interface AreaProps {
4
+ export interface XYPadAreaProps {
6
5
  width?: number | string
7
6
  height?: number | string
8
7
  color?: string
@@ -14,8 +13,7 @@ export interface AreaProps {
14
13
  __thumb?: ReactElement
15
14
  }
16
15
 
17
- /** @category XYPad */
18
- export function XYPadArea({
16
+ export function Area({
19
17
  width = 120,
20
18
  height = 120,
21
19
  color,
@@ -24,7 +22,8 @@ export function XYPadArea({
24
22
  style,
25
23
  __thumb,
26
24
  ...props
27
- }: AreaProps & Omit<ComponentPropsWithoutRef<'div'>, keyof AreaProps>) {
25
+ }: XYPadAreaProps &
26
+ Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadAreaProps>) {
28
27
  return (
29
28
  <div
30
29
  className={clsx('tremolo-xy-pad-area', className)}
@@ -8,8 +8,7 @@ import {
8
8
  useRef,
9
9
  } from 'react'
10
10
 
11
- /** @category XYPad */
12
- export interface ThumbProps {
11
+ export interface XYPadThumbProps {
13
12
  size?: number | string
14
13
  width?: number | string
15
14
  height?: number | string
@@ -24,20 +23,20 @@ export interface ThumbProps {
24
23
  /** @internal */
25
24
  __disabled?: boolean
26
25
  /** @internal */
26
+ __readonly?: boolean
27
+ /** @internal */
27
28
  __css?: CSSProperties
28
29
  }
29
30
 
30
- /** @category XYPad */
31
31
  export interface XYPadThumbMethods {
32
32
  focus: () => void
33
33
  blur: () => void
34
34
  }
35
35
 
36
- type Props = ThumbProps &
37
- Omit<ComponentPropsWithoutRef<'div'>, keyof ThumbProps>
36
+ type Props = XYPadThumbProps &
37
+ Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadThumbProps>
38
38
 
39
- /** @category XYPad */
40
- export const XYPadThumb = forwardRef<XYPadThumbMethods, Props>(
39
+ export const Thumb = forwardRef<XYPadThumbMethods, Props>(
41
40
  (
42
41
  {
43
42
  size,
@@ -50,6 +49,7 @@ export const XYPadThumb = forwardRef<XYPadThumbMethods, Props>(
50
49
  className,
51
50
  style,
52
51
  __disabled,
52
+ __readonly,
53
53
  __css,
54
54
  ...props
55
55
  }: Props,
@@ -87,6 +87,7 @@ export const XYPadThumb = forwardRef<XYPadThumbMethods, Props>(
87
87
  // eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
88
88
  tabIndex={0}
89
89
  aria-disabled={__disabled}
90
+ aria-readonly={__readonly}
90
91
  style={{
91
92
  ...{ '--color': color },
92
93
  width: size ?? width,
@@ -4,7 +4,7 @@
4
4
  }
5
5
 
6
6
  .tremolo-xy-pad-area {
7
- --color: #eee;
7
+ --color: oklch(95% 0 0);
8
8
 
9
9
  position: relative;
10
10
  background-color: var(--color);
@@ -17,18 +17,32 @@
17
17
  }
18
18
 
19
19
  .tremolo-xy-pad-thumb {
20
- --color: var(--tremolo-theme-color);
20
+ --color: oklch(59.5% 0.17548 266.373);
21
21
 
22
22
  border-radius: 50%;
23
23
  outline: none;
24
24
  background-color: var(--color);
25
25
 
26
26
  &[aria-disabled='true'] {
27
- background-color: #5d6478;
27
+ background-color: oklch(50.462% 0.03307 270.021);
28
28
  }
29
29
 
30
- &:focus {
30
+ &[aria-readonly='false']:focus {
31
31
  box-shadow: 0px 0px 0px 3px
32
32
  color-mix(in srgb, var(--color) 20%, transparent);
33
33
  }
34
34
  }
35
+
36
+ :where(.dark, [data-theme='dark']) {
37
+ .tremolo-xy-pad-area {
38
+ --color: oklch(41.7% 0 0);
39
+ }
40
+
41
+ .tremolo-xy-pad-thumb {
42
+ --color: oklch(64.5% 0.17548 266.373);
43
+
44
+ &:hover {
45
+ --color: oklch(67.5% 0.17548 266.373);
46
+ }
47
+ }
48
+ }
@@ -3,6 +3,7 @@ import React, {
3
3
  ComponentPropsWithoutRef,
4
4
  forwardRef,
5
5
  ReactElement,
6
+ Ref,
6
7
  useCallback,
7
8
  useImperativeHandle,
8
9
  useMemo,
@@ -21,14 +22,33 @@ import {
21
22
 
22
23
  import { useDragWithElement } from '../../hooks/useDragWithElement'
23
24
  import { useRefCallbackEvent } from '../../hooks/useRefCallbackEvent'
24
- import { addNoSelect, removeNoSelect } from '../_util'
25
+ import {
26
+ addUserSelectNone,
27
+ Cursor,
28
+ removeUserSelectNone,
29
+ resetCursorStyle,
30
+ setCursorStyle,
31
+ } from '../_util'
32
+ import { composeRefs } from '../_util/composeRefs'
33
+
34
+ import { Area, XYPadAreaProps } from './Area'
35
+ import { Thumb, XYPadThumbMethods, XYPadThumbProps } from './Thumb'
25
36
 
26
- import { XYPadArea, AreaProps } from './Area'
27
- import { XYPadThumb, XYPadThumbMethods, ThumbProps } from './Thumb'
37
+ const defaultValueOptions = {
38
+ skew: 1,
39
+ step: 1,
40
+ reverse: false,
41
+ wheel: ['raw', 1] as InputEventOption,
42
+ keyboard: ['raw', 1] as InputEventOption,
43
+ }
44
+
45
+ const defaultExternalStyles: XYPadProps['externalStyles'] = {
46
+ userSelectNone: true,
47
+ cursor: 'pointer',
48
+ }
28
49
 
29
50
  /**
30
51
  * Two-dimensional slider component.
31
- * @category XYPad
32
52
  */
33
53
  export interface ValueOptions {
34
54
  // required
@@ -50,14 +70,18 @@ export interface ValueOptions {
50
70
  keyboard?: InputEventOption | null
51
71
  }
52
72
 
53
- /** @category XYPad */
54
73
  export interface XYPadProps {
55
74
  x: ValueOptions
56
75
  y: ValueOptions
57
76
 
58
- bodyNoSelect?: boolean
77
+ externalStyles?: {
78
+ userSelectNone?: boolean
79
+ cursor?: Cursor
80
+ }
81
+
59
82
  disabled?: boolean
60
83
  readonly?: boolean
84
+
61
85
  onChange?: (valueX: number, valueY: number) => void
62
86
  onDragStart?: (valueX: number, valueY: number) => void
63
87
  onDragEnd?: (valueX: number, valueY: number) => void
@@ -65,35 +89,23 @@ export interface XYPadProps {
65
89
  children?: ReactElement | ReactElement[]
66
90
  }
67
91
 
68
- /** @category XYPad */
69
92
  export interface XYPadMethods {
70
93
  focus: () => void
71
94
  blur: () => void
72
- }
73
-
74
- const defaultValueOptions = {
75
- skew: 1,
76
- step: 1,
77
- reverse: false,
78
- wheel: ['raw', 1] as InputEventOption,
79
- keyboard: ['raw', 1] as InputEventOption,
95
+ original: Ref<HTMLDivElement>
80
96
  }
81
97
 
82
98
  type Props = XYPadProps &
83
99
  Omit<ComponentPropsWithoutRef<'div'>, keyof XYPadProps>
84
100
 
85
- /**
86
- * Simple XYPad
87
- * @category XYPad
88
- */
89
- export const XYPad = forwardRef<XYPadMethods, Props>(
101
+ export const Root = forwardRef<XYPadMethods, Props>(
90
102
  (
91
103
  {
92
104
  x: _x,
93
105
  y: _y,
94
106
  className,
95
107
  style,
96
- bodyNoSelect = true,
108
+ externalStyles: _externalStyles,
97
109
  disabled = false,
98
110
  readonly = false,
99
111
  onChange,
@@ -116,24 +128,27 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
116
128
  }, [_y])
117
129
 
118
130
  // -- state and ref ---
131
+ const rootRef = useRef<HTMLDivElement>(null)
119
132
  const areaElementRef = useRef<HTMLDivElement>(null)
120
133
  const thumbRef = useRef<XYPadThumbMethods>(null)
121
134
 
122
135
  // --- interpret props ---
136
+ const externalStyles = { ...defaultExternalStyles, ..._externalStyles }
137
+
123
138
  const nx = normalizeValue(x.value, x.min, x.max, x.skew)
124
139
  const ny = normalizeValue(y.value, y.min, y.max, y.skew)
125
140
  const percentX = toFixed((x.reverse ? 1 - nx : nx) * 100)
126
141
  const percentY = toFixed((y.reverse ? 1 - ny : ny) * 100)
127
142
 
128
- let areaProps: AreaProps = {}
129
- let thumbProps: ThumbProps = {}
143
+ let areaProps: XYPadAreaProps = {}
144
+ let thumbProps: XYPadThumbProps = {}
130
145
  if (children != undefined) {
131
146
  React.Children.forEach(children, (child) => {
132
147
  if (React.isValidElement(child)) {
133
- if (child.type == XYPadThumb) {
134
- thumbProps = child.props as ThumbProps
135
- } else if (child.type == XYPadArea) {
136
- areaProps = child.props as AreaProps
148
+ if (child.type == Thumb) {
149
+ thumbProps = child.props as XYPadThumbProps
150
+ } else if (child.type == Area) {
151
+ areaProps = child.props as XYPadAreaProps
137
152
  } else {
138
153
  throw new Error('only <XYPadThumb> or <XYPadArea>')
139
154
  }
@@ -216,14 +231,18 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
216
231
  )
217
232
 
218
233
  // --- hooks ---
219
- const [touchMoveRefCallback, pointerDownHandler] =
234
+ const { refHandler: touchMoveRefCallback, pointerDownHandler } =
220
235
  useDragWithElement<HTMLDivElement>({
221
236
  baseElementRef: areaElementRef,
222
237
  onDrag: onDrag,
223
238
  onDragStart: (nx, ny) => {
224
239
  if (readonly) return
225
- if (bodyNoSelect) addNoSelect()
240
+
241
+ if (externalStyles.userSelectNone) addUserSelectNone()
242
+ if (externalStyles.cursor) setCursorStyle(externalStyles.cursor)
243
+
226
244
  thumbRef.current?.focus()
245
+
227
246
  const valueX = clamp(
228
247
  stepValue(rawValue(nx, x.min, x.max, x.skew), x.step),
229
248
  x.min,
@@ -234,11 +253,17 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
234
253
  y.min,
235
254
  y.max,
236
255
  )
256
+
257
+ // TODO: ドラッグせず、pointer downだけの場合でも、onChange を発火させるべき
258
+ // onChange()
237
259
  onDragStart?.(valueX, valueY)
238
260
  },
239
261
  onDragEnd: (nx, ny) => {
240
262
  if (readonly) return
241
- if (bodyNoSelect) removeNoSelect()
263
+
264
+ if (externalStyles.userSelectNone) removeUserSelectNone()
265
+ if (externalStyles.cursor) resetCursorStyle()
266
+
242
267
  const valueX = clamp(
243
268
  stepValue(rawValue(nx, x.min, x.max, x.skew), x.step),
244
269
  x.min,
@@ -284,6 +309,7 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
284
309
  blur() {
285
310
  thumbRef.current?.blur()
286
311
  },
312
+ original: rootRef,
287
313
  }
288
314
  }, [])
289
315
 
@@ -291,10 +317,7 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
291
317
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
292
318
  <div
293
319
  className={clsx('tremolo-xy-pad', className)}
294
- ref={(div) => {
295
- wheelRefCallback(div)
296
- touchMoveRefCallback(div)
297
- }}
320
+ ref={composeRefs(rootRef, wheelRefCallback, touchMoveRefCallback)}
298
321
  tabIndex={-1}
299
322
  aria-disabled={disabled}
300
323
  aria-readonly={readonly}
@@ -323,11 +346,12 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
323
346
  {...props}
324
347
  >
325
348
  <div className="tremolo-xy-pad-area-wrapper" ref={areaElementRef}>
326
- <XYPadArea
349
+ <Area
327
350
  __thumb={
328
- <XYPadThumb
351
+ <Thumb
329
352
  ref={thumbRef}
330
353
  __disabled={disabled}
354
+ __readonly={readonly}
331
355
  __css={{
332
356
  top: `${percentY}%`,
333
357
  left: `${percentX}%`,
@@ -343,5 +367,14 @@ export const XYPad = forwardRef<XYPadMethods, Props>(
343
367
  },
344
368
  )
345
369
 
346
- export * from './Thumb'
347
- export * from './Area'
370
+ /**
371
+ * Simple XYPad
372
+ */
373
+ export const XYPad = {
374
+ Root,
375
+ Thumb,
376
+ Area,
377
+ }
378
+
379
+ export { type XYPadThumbProps, type XYPadThumbMethods } from './Thumb'
380
+ export { type XYPadAreaProps } from './Area'