@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.
- package/dist/index.cjs +666 -1171
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +316 -315
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +316 -315
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +643 -1123
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/components/AnimationCanvas/index.tsx +4 -18
- package/src/components/DropZone/index.tsx +2 -1
- package/src/components/FileInput/index.tsx +11 -13
- package/src/components/Knob/ActiveLine.tsx +5 -3
- package/src/components/Knob/InactiveLine.tsx +6 -4
- package/src/components/Knob/SVGRoot.tsx +3 -3
- package/src/components/Knob/Thumb.tsx +21 -20
- package/src/components/Knob/context.tsx +3 -74
- package/src/components/Knob/index.tsx +99 -52
- package/src/components/NumberInput/InputField.tsx +20 -33
- package/src/components/NumberInput/Stepper.tsx +76 -100
- package/src/components/NumberInput/StepperButton.tsx +15 -8
- package/src/components/NumberInput/context.tsx +26 -3
- package/src/components/NumberInput/index.tsx +175 -91
- package/src/components/Piano/index.tsx +54 -144
- package/src/components/PointsEditor/Container.tsx +15 -26
- package/src/components/PointsEditor/Point.tsx +94 -110
- package/src/components/PointsEditor/context.tsx +8 -60
- package/src/components/PointsEditor/index.tsx +41 -324
- package/src/components/Slider/Marks.tsx +5 -6
- package/src/components/Slider/MarksOption.tsx +9 -19
- package/src/components/Slider/Thumb.tsx +20 -11
- package/src/components/Slider/Track.tsx +16 -13
- package/src/components/Slider/context.tsx +3 -3
- package/src/components/Slider/index.tsx +110 -72
- package/src/components/XYPad/Area.tsx +27 -49
- package/src/components/XYPad/Thumb.tsx +20 -8
- package/src/components/XYPad/context.tsx +2 -4
- package/src/components/XYPad/index.tsx +102 -83
- package/src/components/_util/VisuallyHiddenRangeInput.tsx +1 -1
- package/src/hooks/_internal/useChangeGesture.ts +93 -0
- package/src/hooks/_internal/useCheckSteps.ts +9 -146
- package/src/hooks/useAnimationFrame.ts +37 -15
- package/src/hooks/useEventListener.ts +93 -22
- package/src/hooks/useLongPress.ts +35 -51
- package/src/hooks/useMIDIAccess.ts +0 -8
- package/src/hooks/useMIDIInput.ts +0 -3
- package/src/hooks/useWheel.ts +4 -4
- package/src/index.ts +11 -32
- package/src/components/Piano/keyboard-shortcuts.ts +0 -61
- package/src/components/Slider/type.ts +0 -46
- package/src/components/_util/css-length.ts +0 -13
- package/src/components/_util/decimal-digits.ts +0 -23
- package/src/components/_util/visually-hidden.ts +0 -25
- package/src/hooks/useInterval.ts +0 -27
- 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.
|
|
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.
|
|
26
|
-
"@storybook/addon-docs": "10.6.
|
|
27
|
-
"@storybook/addon-links": "10.6.
|
|
28
|
-
"@storybook/react-vite": "10.6.
|
|
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.
|
|
33
|
+
"jotai": "3.0.1",
|
|
34
34
|
"react": "19.3.0",
|
|
35
35
|
"react-dom": "19.3.0",
|
|
36
|
-
"storybook": "10.6.
|
|
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": "
|
|
45
|
-
"@tremolo-ui/functions": "^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
|
|
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:
|
|
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?:
|
|
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 {
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
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 {
|
|
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 =
|
|
20
|
+
const radius = knobArcRadius(strokeWidth)
|
|
19
21
|
|
|
20
22
|
return (
|
|
21
23
|
<path
|
|
22
24
|
className={className}
|
|
23
|
-
d={
|
|
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 {
|
|
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 =
|
|
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={
|
|
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={
|
|
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 {
|
|
3
|
+
import { KNOB_VIEWBOX_SIZE } from '@tremolo-ui/dom'
|
|
4
4
|
|
|
5
|
-
import {
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
15
|
+
color?: string
|
|
15
16
|
/**
|
|
16
17
|
* Colour of the line that points at the value.
|
|
17
18
|
* @default 'currentColor'
|
|
18
19
|
*/
|
|
19
|
-
|
|
20
|
+
lineColor?: string
|
|
20
21
|
/**
|
|
21
22
|
* Diameter of the circle, as a percentage of the knob.
|
|
22
23
|
* @default 84
|
|
23
24
|
*/
|
|
24
|
-
|
|
25
|
+
size?: number
|
|
25
26
|
/**
|
|
26
27
|
* Thickness of the line, as a percentage of the knob.
|
|
27
28
|
* @default 6
|
|
28
29
|
*/
|
|
29
|
-
|
|
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
|
-
|
|
36
|
+
lineLength?: number
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
|
-
* Classes for what the thumb draws inside itself: `
|
|
39
|
-
*
|
|
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
|
-
|
|
43
|
+
line?: string
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
export function Thumb({
|
|
47
48
|
className,
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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={`${
|
|
68
|
+
<circle cx="50%" cy="50%" r={`${size / 2}%`} fill={color} />
|
|
68
69
|
<line
|
|
69
|
-
className={classes?.
|
|
70
|
+
className={classes?.line}
|
|
70
71
|
x1="50%"
|
|
71
|
-
y1={`${(
|
|
72
|
+
y1={`${(KNOB_VIEWBOX_SIZE - clamp(size, 0, 100)) / 2}%`}
|
|
72
73
|
x2="50%"
|
|
73
|
-
y2={`${
|
|
74
|
-
stroke={
|
|
75
|
-
strokeWidth={`${
|
|
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 {
|
|
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
|
|