@tremolo-ui/react 0.7.0 → 0.9.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 (55) hide show
  1. package/dist/index.cjs +666 -1171
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +316 -315
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.ts +316 -315
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +643 -1123
  8. package/dist/index.js.map +1 -1
  9. package/package.json +9 -9
  10. package/src/components/AnimationCanvas/index.tsx +4 -18
  11. package/src/components/DropZone/index.tsx +2 -1
  12. package/src/components/FileInput/index.tsx +11 -13
  13. package/src/components/Knob/ActiveLine.tsx +5 -3
  14. package/src/components/Knob/InactiveLine.tsx +6 -4
  15. package/src/components/Knob/SVGRoot.tsx +3 -3
  16. package/src/components/Knob/Thumb.tsx +21 -20
  17. package/src/components/Knob/context.tsx +3 -74
  18. package/src/components/Knob/index.tsx +99 -52
  19. package/src/components/NumberInput/InputField.tsx +20 -33
  20. package/src/components/NumberInput/Stepper.tsx +76 -100
  21. package/src/components/NumberInput/StepperButton.tsx +15 -8
  22. package/src/components/NumberInput/context.tsx +26 -3
  23. package/src/components/NumberInput/index.tsx +175 -91
  24. package/src/components/Piano/index.tsx +54 -144
  25. package/src/components/PointsEditor/Container.tsx +15 -26
  26. package/src/components/PointsEditor/Point.tsx +94 -110
  27. package/src/components/PointsEditor/context.tsx +8 -60
  28. package/src/components/PointsEditor/index.tsx +41 -324
  29. package/src/components/Slider/Marks.tsx +5 -6
  30. package/src/components/Slider/MarksOption.tsx +9 -19
  31. package/src/components/Slider/Thumb.tsx +20 -11
  32. package/src/components/Slider/Track.tsx +16 -13
  33. package/src/components/Slider/context.tsx +3 -3
  34. package/src/components/Slider/index.tsx +110 -72
  35. package/src/components/XYPad/Area.tsx +27 -49
  36. package/src/components/XYPad/Thumb.tsx +20 -8
  37. package/src/components/XYPad/context.tsx +2 -4
  38. package/src/components/XYPad/index.tsx +102 -83
  39. package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
  40. package/src/hooks/_internal/useChangeGesture.ts +93 -0
  41. package/src/hooks/_internal/useCheckSteps.ts +9 -146
  42. package/src/hooks/useAnimationFrame.ts +37 -15
  43. package/src/hooks/useEventListener.ts +93 -22
  44. package/src/hooks/useLongPress.ts +35 -51
  45. package/src/hooks/useMIDIAccess.ts +0 -8
  46. package/src/hooks/useMIDIInput.ts +0 -3
  47. package/src/hooks/useWheel.ts +4 -4
  48. package/src/index.ts +11 -32
  49. package/src/components/Piano/keyboard-shortcuts.ts +0 -61
  50. package/src/components/Slider/type.ts +0 -46
  51. package/src/components/_util/css-length.ts +0 -13
  52. package/src/components/_util/decimal-digits.ts +0 -23
  53. package/src/components/_util/visually-hidden.ts +0 -25
  54. package/src/hooks/useInterval.ts +0 -27
  55. package/src/input-event.ts +0 -32
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tremolo-ui/react",
3
- "version": "0.7.0",
3
+ "version": "0.9.0",
4
4
  "description": "React component library for audio app",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -22,18 +22,18 @@
22
22
  "clean:sb": "rimraf ./storybook-static"
23
23
  },
24
24
  "devDependencies": {
25
- "@storybook/addon-a11y": "10.6.0",
26
- "@storybook/addon-docs": "10.6.0",
27
- "@storybook/addon-links": "10.6.0",
28
- "@storybook/react-vite": "10.6.0",
25
+ "@storybook/addon-a11y": "10.6.1",
26
+ "@storybook/addon-docs": "10.6.1",
27
+ "@storybook/addon-links": "10.6.1",
28
+ "@storybook/react-vite": "10.6.1",
29
29
  "@testing-library/jest-dom": "7.0.1",
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": "3.0.0",
33
+ "jotai": "3.0.1",
34
34
  "react": "19.3.0",
35
35
  "react-dom": "19.3.0",
36
- "storybook": "10.6.0",
36
+ "storybook": "10.6.1",
37
37
  "tone": "15.1.22"
38
38
  },
39
39
  "peerDependencies": {
@@ -41,8 +41,8 @@
41
41
  "react-dom": "^18 || ^19"
42
42
  },
43
43
  "dependencies": {
44
- "@tremolo-ui/dom": "^0.7.0",
45
- "@tremolo-ui/functions": "^0.7.0"
44
+ "@tremolo-ui/dom": "0.9.0",
45
+ "@tremolo-ui/functions": "^0.9.0"
46
46
  },
47
47
  "files": [
48
48
  "dist",
@@ -9,26 +9,12 @@ import {
9
9
  import {
10
10
  createAnimationCanvas,
11
11
  type AnimationCanvasInstance,
12
- type AnimationFrame,
12
+ type CanvasDrawFunction,
13
+ type CanvasInitFunction,
13
14
  } from '@tremolo-ui/dom'
14
15
 
15
16
  import { useComposedRefs } from '../../compose-refs'
16
17
 
17
- export type InitFunction = (
18
- context: CanvasRenderingContext2D,
19
- option: {
20
- /** current canvas width */
21
- width: number
22
- /** current canvas height */
23
- height: number
24
- },
25
- ) => void
26
-
27
- export type DrawFunction = (
28
- context: CanvasRenderingContext2D,
29
- option: AnimationFrame,
30
- ) => void
31
-
32
18
  export interface AnimationCanvasCommonProps {
33
19
  /**
34
20
  * Draw one frame. It is given the 2D context and the frame: the size in CSS
@@ -37,12 +23,12 @@ export interface AnimationCanvasCommonProps {
37
23
  * A new function replaces the old one in place, so writing it inline does
38
24
  * not restart anything.
39
25
  */
40
- draw: DrawFunction
26
+ draw: CanvasDrawFunction
41
27
  /**
42
28
  * Called once before the first frame, with the context and the size in CSS
43
29
  * pixels. Set up what every frame shares here.
44
30
  */
45
- init?: InitFunction
31
+ init?: CanvasInitFunction
46
32
  /**
47
33
  * Redraw on every animation frame. Turn it off to draw only when there is a
48
34
  * reason to: when the canvas mounts, when it is resized, and when the
@@ -39,7 +39,8 @@ export interface DropZoneProps {
39
39
  onDrop?: (files: File[], event: DragEvent) => void
40
40
  /**
41
41
  * Called with the dropped files that do not match `accept`, so that the
42
- * reason can be shown.
42
+ * reason can be shown. It comes after `onDrop` for the same drop, so a list
43
+ * of rejected files can be cleared in `onDrop` and filled here.
43
44
  */
44
45
  onReject?: (files: File[], event: DragEvent) => void
45
46
 
@@ -7,9 +7,10 @@ import {
7
7
  useMemo,
8
8
  } from 'react'
9
9
 
10
- import { matchesAccept } from '@tremolo-ui/dom'
11
-
12
- import { visuallyHiddenStyle } from '../_util/visually-hidden'
10
+ import {
11
+ partitionByAccept,
12
+ visuallyHiddenStyle,
13
+ } from '@tremolo-ui/dom/internal'
13
14
 
14
15
  import { FileInputProvider } from './context'
15
16
  import { Trigger } from './Trigger'
@@ -47,7 +48,8 @@ export interface FileInputProps {
47
48
  /**
48
49
  * Called with the files that were picked and do not match `accept`, so that
49
50
  * the reason can be shown. Given alongside `onChange` when a selection held
50
- * both.
51
+ * both, and after it, so a list of rejected files can be cleared in
52
+ * `onChange` and filled here.
51
53
  */
52
54
  onReject?: (files: File[]) => void
53
55
 
@@ -92,22 +94,18 @@ export const Root = /* @__PURE__ */ forwardRef<HTMLDivElement, Props>(
92
94
  const context = useMemo(() => ({ inputId, disabled }), [inputId, disabled])
93
95
 
94
96
  function handleChange(event: ChangeEvent<HTMLInputElement>) {
95
- const picked = Array.from(event.target.files ?? [])
97
+ const { accepted, rejected } = partitionByAccept(
98
+ Array.from(event.target.files ?? []),
99
+ accept,
100
+ )
96
101
 
97
102
  // Picking the same file twice fires no second change event while the
98
103
  // value is still on the input, so it is cleared as soon as it is read.
99
104
  // Nothing is lost: the files are already in hand.
100
105
  event.target.value = ''
101
106
 
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
107
  if (accepted.length > 0) onChange?.(accepted)
108
+ if (rejected.length > 0) onReject?.(rejected)
111
109
  }
112
110
 
113
111
  return (
@@ -1,8 +1,10 @@
1
1
  import { SVGProps } from 'react'
2
2
 
3
+ import { knobArcPath, knobArcRadius } from '@tremolo-ui/dom'
4
+
3
5
  import { useCheckPlacement } from '../_util/Placement'
4
6
 
5
- import { arcPath, arcRadius, useKnobContext } from './context'
7
+ import { useKnobContext } from './context'
6
8
 
7
9
  export function ActiveLine({
8
10
  stroke = 'currentColor',
@@ -15,12 +17,12 @@ export function ActiveLine({
15
17
  const r2 = useKnobContext((s) => s.r2)
16
18
  const r3 = useKnobContext((s) => s.r3)
17
19
 
18
- const radius = arcRadius(strokeWidth)
20
+ const radius = knobArcRadius(strokeWidth)
19
21
 
20
22
  return (
21
23
  <path
22
24
  className={className}
23
- d={arcPath(r2, r3, radius)}
25
+ d={knobArcPath(r2, r3, radius)}
24
26
  fill="none"
25
27
  stroke={stroke}
26
28
  strokeWidth={strokeWidth}
@@ -1,8 +1,10 @@
1
1
  import { SVGProps } from 'react'
2
2
 
3
+ import { knobArcPath, knobArcRadius } from '@tremolo-ui/dom'
4
+
3
5
  import { useCheckPlacement } from '../_util/Placement'
4
6
 
5
- import { arcPath, arcRadius, useKnobContext } from './context'
7
+ import { useKnobContext } from './context'
6
8
 
7
9
  export function InactiveLine({
8
10
  stroke = 'currentColor',
@@ -21,14 +23,14 @@ export function InactiveLine({
21
23
  const r3 = useKnobContext((s) => s.r3)
22
24
  const r4 = useKnobContext((s) => s.r4)
23
25
 
24
- const radius = arcRadius(strokeWidth)
26
+ const radius = knobArcRadius(strokeWidth)
25
27
 
26
28
  return (
27
29
  <>
28
30
  {startValue > min && (
29
31
  <path
30
32
  className={className}
31
- d={arcPath(r1, r2, radius)}
33
+ d={knobArcPath(r1, r2, radius)}
32
34
  fill="none"
33
35
  stroke={stroke}
34
36
  strokeWidth={strokeWidth}
@@ -38,7 +40,7 @@ export function InactiveLine({
38
40
  {startValue < max && (
39
41
  <path
40
42
  className={className}
41
- d={arcPath(r3, r4, radius)}
43
+ d={knobArcPath(r3, r4, radius)}
42
44
  fill="none"
43
45
  stroke={stroke}
44
46
  strokeWidth={strokeWidth}
@@ -1,8 +1,8 @@
1
1
  import { ReactNode, SVGProps } from 'react'
2
2
 
3
- import { Placement } from '../_util/Placement'
3
+ import { KNOB_VIEWBOX_SIZE } from '@tremolo-ui/dom'
4
4
 
5
- import { viewBoxSize } from './context'
5
+ import { Placement } from '../_util/Placement'
6
6
 
7
7
  export interface KnobSVGRootProps {
8
8
  /**
@@ -19,7 +19,7 @@ export function SVGRoot({
19
19
  }: KnobSVGRootProps & Omit<SVGProps<SVGSVGElement>, keyof KnobSVGRootProps>) {
20
20
  return (
21
21
  <svg
22
- viewBox={`0 0 ${viewBoxSize} ${viewBoxSize}`}
22
+ viewBox={`0 0 ${KNOB_VIEWBOX_SIZE} ${KNOB_VIEWBOX_SIZE}`}
23
23
  style={{
24
24
  display: 'block',
25
25
  ...style,
@@ -1,56 +1,57 @@
1
1
  import { SVGProps } from 'react'
2
2
 
3
+ import { KNOB_VIEWBOX_SIZE } from '@tremolo-ui/dom'
3
4
  import { clamp } from '@tremolo-ui/functions'
4
5
 
5
6
  import { useCheckPlacement } from '../_util/Placement'
6
7
 
7
- import { useKnobContext, viewBoxSize } from './context'
8
+ import { useKnobContext } from './context'
8
9
 
9
10
  export interface KnobThumbProps {
10
11
  /**
11
12
  * Fill colour of the circle.
12
13
  * @default 'currentColor'
13
14
  */
14
- thumb?: string
15
+ color?: string
15
16
  /**
16
17
  * Colour of the line that points at the value.
17
18
  * @default 'currentColor'
18
19
  */
19
- thumbLine?: string
20
+ lineColor?: string
20
21
  /**
21
22
  * Diameter of the circle, as a percentage of the knob.
22
23
  * @default 84
23
24
  */
24
- thumbSize?: number
25
+ size?: number
25
26
  /**
26
27
  * Thickness of the line, as a percentage of the knob.
27
28
  * @default 6
28
29
  */
29
- thumbLineWeight?: number
30
+ lineWeight?: number
30
31
  /**
31
32
  * How far down the line reaches, as a percentage of the knob from its top.
32
33
  * The line starts at the edge of the circle.
33
34
  * @default 35
34
35
  */
35
- thumbLineLength?: number
36
+ lineLength?: number
36
37
 
37
38
  /**
38
- * Classes for what the thumb draws inside itself: `thumbLine` is the line
39
- * that points at the value.
39
+ * Classes for what the thumb draws inside itself: `line` is the line that
40
+ * points at the value.
40
41
  */
41
42
  classes?: {
42
- thumbLine?: string
43
+ line?: string
43
44
  }
44
45
  }
45
46
 
46
47
  export function Thumb({
47
48
  className,
48
49
 
49
- thumb = 'currentColor',
50
- thumbLine = 'currentColor',
51
- thumbSize = 84,
52
- thumbLineWeight = 6,
53
- thumbLineLength = 35,
50
+ color = 'currentColor',
51
+ lineColor = 'currentColor',
52
+ size = 84,
53
+ lineWeight = 6,
54
+ lineLength = 35,
54
55
  classes,
55
56
 
56
57
  ...props
@@ -64,15 +65,15 @@ export function Thumb({
64
65
 
65
66
  return (
66
67
  <svg className={className} {...props}>
67
- <circle cx="50%" cy="50%" r={`${thumbSize / 2}%`} fill={thumb} />
68
+ <circle cx="50%" cy="50%" r={`${size / 2}%`} fill={color} />
68
69
  <line
69
- className={classes?.thumbLine}
70
+ className={classes?.line}
70
71
  x1="50%"
71
- y1={`${(viewBoxSize - clamp(thumbSize, 0, 100)) / 2}%`}
72
+ y1={`${(KNOB_VIEWBOX_SIZE - clamp(size, 0, 100)) / 2}%`}
72
73
  x2="50%"
73
- y2={`${thumbLineLength}%`}
74
- stroke={thumbLine}
75
- strokeWidth={`${thumbLineWeight}%`}
74
+ y2={`${lineLength}%`}
75
+ stroke={lineColor}
76
+ strokeWidth={`${lineWeight}%`}
76
77
  // NOTE
77
78
  // https://bugs.webkit.org/show_bug.cgi?id=201854
78
79
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/transform-origin#browser_compatibility
@@ -1,9 +1,7 @@
1
1
  import { createContext, useContext } from 'react'
2
2
 
3
- import { radian, type Scale } from '@tremolo-ui/functions'
4
-
5
- export const viewBoxSize = 100
6
- export const center = viewBoxSize / 2
3
+ import { type KnobAngles } from '@tremolo-ui/dom'
4
+ import { type Scale } from '@tremolo-ui/functions'
7
5
 
8
6
  export type KnobConfig = {
9
7
  value: number
@@ -16,76 +14,7 @@ export type KnobConfig = {
16
14
  angleRange: number
17
15
  }
18
16
 
19
- export type KnobContextValue = KnobConfig & {
20
- /** normalized value */
21
- p: number
22
- /** ロータリー開始位置 */
23
- r1: number
24
- /** activeLine の開始位置 */
25
- r2: number
26
- /** activeLine の終了位置 */
27
- r3: number
28
- /** ロータリー終了位置 */
29
- r4: number
30
- }
31
-
32
- /**
33
- * 角度から円弧上の点を求める。
34
- *
35
- * 半径は線の太さの分だけ内側に取る必要があるが、太さは ActiveLine / InactiveLine が
36
- * それぞれ持つため、座標はストアで先に計算せず各コンポーネント側で求める。
37
- */
38
- export function pointOnArc(angle: number, radius: number) {
39
- return {
40
- x: center + radius * Math.cos(radian(angle - 90)),
41
- y: center + radius * Math.sin(radian(angle - 90)),
42
- }
43
- }
44
-
45
- /** 線の太さが viewBox からはみ出さないようにした半径 */
46
- export function arcRadius(strokeWidth: number | string | undefined) {
47
- const width =
48
- typeof strokeWidth === 'number'
49
- ? strokeWidth
50
- : Number.parseFloat(String(strokeWidth))
51
- return Number.isFinite(width) ? center - width / 2 : center
52
- }
53
-
54
- /** Build an SVG path for an arc, splitting full turns into drawable segments. */
55
- export function arcPath(startAngle: number, endAngle: number, radius: number) {
56
- const start = pointOnArc(startAngle, radius)
57
- const sweep = endAngle - startAngle
58
- const segmentCount = Math.max(1, Math.ceil(Math.abs(sweep) / 180))
59
- const segmentSweep = sweep / segmentCount
60
- let path = `M ${start.x} ${start.y}`
61
-
62
- for (let i = 1; i <= segmentCount; i += 1) {
63
- const end = pointOnArc(startAngle + segmentSweep * i, radius)
64
- path += ` A ${radius} ${radius} 0 0 ${segmentSweep >= 0 ? 1 : 0} ${end.x} ${end.y}`
65
- }
66
-
67
- return path
68
- }
69
-
70
- /** 設定から描画に必要な角度を導出する。レンダー中に呼ぶ。 */
71
- export function calcAngles({
72
- value,
73
- min,
74
- max,
75
- scale,
76
- startValue,
77
- angleRange,
78
- }: KnobConfig) {
79
- const p = scale.normalize(value, min, max)
80
- const s = scale.normalize(startValue, min, max)
81
-
82
- const r1 = -angleRange / 2
83
- const r2 = r1 + Math.min(p, s) * angleRange
84
- const r3 = r1 + Math.max(p, s) * angleRange
85
- const r4 = angleRange / 2
86
-
87
- return { p, r1, r2, r3, r4 }
88
- }
17
+ export type KnobContextValue = KnobConfig & KnobAngles
89
18
 
90
19
  const KnobContext = /* @__PURE__ */ createContext<KnobContextValue | null>(null)
91
20