@moises.ai/design-system 4.16.5 → 4.16.7
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/{ForwardBarFillIcon-CUEgLgyx.js → ChordDiagramGridIcon-CxpGQmg4.js} +1029 -976
- package/dist/colors/custom-styles.css +1 -0
- package/dist/icons.js +604 -602
- package/dist/{index-f-eJMg6F.js → index-Dwaw5zqT.js} +151 -151
- package/dist/index.js +1252 -1233
- package/dist/primitives.js +147 -147
- package/package.json +1 -1
- package/src/colors/custom-styles.css +1 -0
- package/src/components/DropdownButton/DropdownButton.jsx +29 -3
- package/src/components/DropdownButton/DropdownButton.module.css +50 -5
- package/src/components/PlayerBar/PlayerBar.jsx +12 -11
- package/src/components/PlayerBar/PlayerBar.stories.jsx +193 -8
- package/src/components/PlayerMasterVolume/PlayerMasterVolume.jsx +54 -0
- package/src/components/PlayerMasterVolume/PlayerMasterVolume.module.css +31 -0
- package/src/components/PlayerMasterVolume/PlayerMasterVolume.stories.jsx +182 -0
- package/src/components/PlayerSmartMetronome/PlayerSmartMetronome.jsx +79 -0
- package/src/components/PlayerSmartMetronome/PlayerSmartMetronome.module.css +23 -0
- package/src/components/PlayerSmartMetronome/PlayerSmartMetronome.stories.jsx +225 -0
- package/src/components/PlayerSmartMetronome/SegmentedField.jsx +19 -0
- package/src/components/PlayerTempoPitch/PlayerTempoPitch.jsx +123 -0
- package/src/components/PlayerTempoPitch/PlayerTempoPitch.module.css +25 -0
- package/src/components/PlayerTempoPitch/PlayerTempoPitch.stories.jsx +215 -0
- package/src/components/PlayerTempoPitch/StepperField.jsx +57 -0
- package/src/components/PlayerTempoPitch/constants.js +40 -0
- package/src/icons/ChordDiagramGridIcon.jsx +22 -0
- package/src/icons/ChordGrid3Icon.jsx +20 -0
- package/src/icons.jsx +2 -0
- package/src/index.jsx +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import { PlayerMasterVolume } from './PlayerMasterVolume'
|
|
3
|
+
|
|
4
|
+
const DEFAULT_STEMS = [
|
|
5
|
+
{ name: 'Vocals', volume: 80 },
|
|
6
|
+
{ name: 'Drums', volume: 65 },
|
|
7
|
+
{ name: 'Bass', volume: 70 },
|
|
8
|
+
{ name: 'Others', volume: 55 },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
function StatefulPlayerMasterVolume({
|
|
12
|
+
initialVolume = 1,
|
|
13
|
+
initialStems = DEFAULT_STEMS,
|
|
14
|
+
onVolumeChange,
|
|
15
|
+
onStemVolumeChange,
|
|
16
|
+
}) {
|
|
17
|
+
const [volume, setVolume] = useState(initialVolume)
|
|
18
|
+
const [stems, setStems] = useState(initialStems)
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
setVolume(initialVolume)
|
|
22
|
+
}, [initialVolume])
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
setStems(initialStems)
|
|
26
|
+
}, [initialStems])
|
|
27
|
+
|
|
28
|
+
const handleVolumeChange = useCallback(
|
|
29
|
+
([value]) => {
|
|
30
|
+
setVolume(value)
|
|
31
|
+
onVolumeChange?.([value])
|
|
32
|
+
},
|
|
33
|
+
[onVolumeChange],
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
const handleStemVolumeChange = useCallback(
|
|
37
|
+
(name, value) => {
|
|
38
|
+
setStems((prev) =>
|
|
39
|
+
prev.map((stem) =>
|
|
40
|
+
stem.name === name ? { ...stem, volume: value } : stem,
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
onStemVolumeChange?.(name, value)
|
|
44
|
+
},
|
|
45
|
+
[onStemVolumeChange],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<PlayerMasterVolume
|
|
50
|
+
volume={volume}
|
|
51
|
+
stems={stems}
|
|
52
|
+
onVolumeChange={handleVolumeChange}
|
|
53
|
+
onStemVolumeChange={handleStemVolumeChange}
|
|
54
|
+
/>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default {
|
|
59
|
+
title: 'New Mixer/PlayerMasterVolume',
|
|
60
|
+
component: PlayerMasterVolume,
|
|
61
|
+
parameters: {
|
|
62
|
+
layout: 'centered',
|
|
63
|
+
docs: {
|
|
64
|
+
description: {
|
|
65
|
+
component: `
|
|
66
|
+
Master volume popover for New Mixer. Pass \`volume\` and update it from \`onVolumeChange\` (receives a gain array from \`InputLevelMeter\`). Stem sliders are controlled via \`stems\` and \`onStemVolumeChange\`.
|
|
67
|
+
`,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
decorators: [
|
|
72
|
+
(Story) => (
|
|
73
|
+
<div
|
|
74
|
+
style={{
|
|
75
|
+
width: 286,
|
|
76
|
+
padding: 16,
|
|
77
|
+
background: '#1d1d1d',
|
|
78
|
+
borderRadius: 8,
|
|
79
|
+
boxSizing: 'border-box',
|
|
80
|
+
}}
|
|
81
|
+
>
|
|
82
|
+
<Story />
|
|
83
|
+
</div>
|
|
84
|
+
),
|
|
85
|
+
],
|
|
86
|
+
tags: ['autodocs'],
|
|
87
|
+
argTypes: {
|
|
88
|
+
volume: {
|
|
89
|
+
description: 'Master gain passed to InputLevelMeter (0 = mute, 1 = unity).',
|
|
90
|
+
table: { type: { summary: 'number' }, defaultValue: { summary: 1 } },
|
|
91
|
+
control: { type: 'range', min: 0, max: 2, step: 0.01 },
|
|
92
|
+
},
|
|
93
|
+
stems: {
|
|
94
|
+
description: 'Stem rows rendered when the accordion is open.',
|
|
95
|
+
table: { type: { summary: '{ name: string, volume: number }[]' } },
|
|
96
|
+
control: false,
|
|
97
|
+
},
|
|
98
|
+
onVolumeChange: {
|
|
99
|
+
description: 'Called with `[gain]` when the master fader moves.',
|
|
100
|
+
table: { type: { summary: '([gain: number]) => void' } },
|
|
101
|
+
action: 'volume changed',
|
|
102
|
+
},
|
|
103
|
+
onStemVolumeChange: {
|
|
104
|
+
description: 'Called with stem name and slider value (0–100).',
|
|
105
|
+
table: { type: { summary: '(name: string, value: number) => void' } },
|
|
106
|
+
action: 'stem volume changed',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export const Default = {
|
|
112
|
+
args: {
|
|
113
|
+
volume: 1,
|
|
114
|
+
initialStems: DEFAULT_STEMS,
|
|
115
|
+
},
|
|
116
|
+
render: (args) => (
|
|
117
|
+
<StatefulPlayerMasterVolume
|
|
118
|
+
initialVolume={args.volume}
|
|
119
|
+
initialStems={args.initialStems}
|
|
120
|
+
onVolumeChange={args.onVolumeChange}
|
|
121
|
+
onStemVolumeChange={args.onStemVolumeChange}
|
|
122
|
+
/>
|
|
123
|
+
),
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const VolumeAtZero = {
|
|
127
|
+
args: {
|
|
128
|
+
volume: 0,
|
|
129
|
+
},
|
|
130
|
+
render: (args) => (
|
|
131
|
+
<StatefulPlayerMasterVolume
|
|
132
|
+
initialVolume={args.volume}
|
|
133
|
+
onVolumeChange={args.onVolumeChange}
|
|
134
|
+
onStemVolumeChange={args.onStemVolumeChange}
|
|
135
|
+
/>
|
|
136
|
+
),
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const VolumeBoosted = {
|
|
140
|
+
args: {
|
|
141
|
+
volume: 1.5,
|
|
142
|
+
},
|
|
143
|
+
render: (args) => (
|
|
144
|
+
<StatefulPlayerMasterVolume
|
|
145
|
+
initialVolume={args.volume}
|
|
146
|
+
onVolumeChange={args.onVolumeChange}
|
|
147
|
+
onStemVolumeChange={args.onStemVolumeChange}
|
|
148
|
+
/>
|
|
149
|
+
),
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const CustomStems = {
|
|
153
|
+
args: {
|
|
154
|
+
volume: 1,
|
|
155
|
+
initialStems: [
|
|
156
|
+
{ name: 'Vocals', volume: 100 },
|
|
157
|
+
{ name: 'Guitar', volume: 40 },
|
|
158
|
+
{ name: 'Keys', volume: 60 },
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
render: (args) => (
|
|
162
|
+
<StatefulPlayerMasterVolume
|
|
163
|
+
initialVolume={args.volume}
|
|
164
|
+
initialStems={args.initialStems}
|
|
165
|
+
onVolumeChange={args.onVolumeChange}
|
|
166
|
+
onStemVolumeChange={args.onStemVolumeChange}
|
|
167
|
+
/>
|
|
168
|
+
),
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const Playground = {
|
|
172
|
+
args: {
|
|
173
|
+
volume: 1,
|
|
174
|
+
},
|
|
175
|
+
render: (args) => (
|
|
176
|
+
<StatefulPlayerMasterVolume
|
|
177
|
+
initialVolume={args.volume}
|
|
178
|
+
onVolumeChange={args.onVolumeChange}
|
|
179
|
+
onStemVolumeChange={args.onStemVolumeChange}
|
|
180
|
+
/>
|
|
181
|
+
),
|
|
182
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import styles from './PlayerSmartMetronome.module.css'
|
|
2
|
+
import { Flex, Text, SliderLibrary, Separator, IconButton, PanControl } from '../../index'
|
|
3
|
+
import { RefreshBackIcon } from '../../icons'
|
|
4
|
+
import { SegmentedField } from './SegmentedField'
|
|
5
|
+
|
|
6
|
+
export const PlayerSmartMetronome = ({
|
|
7
|
+
subDivision = '1x',
|
|
8
|
+
volume = 50,
|
|
9
|
+
countIn = 'off',
|
|
10
|
+
panValue = 0,
|
|
11
|
+
onSubDivisionChange,
|
|
12
|
+
onVolumeChange,
|
|
13
|
+
onCountInChange,
|
|
14
|
+
onRefresh,
|
|
15
|
+
onPanChange,
|
|
16
|
+
}) => {
|
|
17
|
+
return (
|
|
18
|
+
<Flex className={styles.PlayerSmartMetronome} direction="column" gap="4">
|
|
19
|
+
<Flex align="center" gap="2" justify="between">
|
|
20
|
+
<Text size="3" weight="medium" className={styles.white}>
|
|
21
|
+
Smart Metronome
|
|
22
|
+
</Text>
|
|
23
|
+
|
|
24
|
+
<IconButton variant="ghost" size="1" onClick={onRefresh}>
|
|
25
|
+
<RefreshBackIcon width={16} height={16} />
|
|
26
|
+
</IconButton>
|
|
27
|
+
</Flex>
|
|
28
|
+
|
|
29
|
+
<SegmentedField
|
|
30
|
+
label="Sub-division"
|
|
31
|
+
items={[
|
|
32
|
+
{ value: '0.5x', label: '0.5x' },
|
|
33
|
+
{ value: '1x', label: '1x' },
|
|
34
|
+
{ value: '2x', label: '2x' },
|
|
35
|
+
]}
|
|
36
|
+
value={subDivision}
|
|
37
|
+
onChange={onSubDivisionChange}
|
|
38
|
+
/>
|
|
39
|
+
<Flex gap="3" align="end" justify="between">
|
|
40
|
+
<Flex className={styles.flex1} direction="column" gap="1">
|
|
41
|
+
<Flex width="100%" justify="between" align="center">
|
|
42
|
+
<Text size="2" className={styles.neutral12}>
|
|
43
|
+
Metronome volume
|
|
44
|
+
</Text>
|
|
45
|
+
|
|
46
|
+
<Text size="2" className={styles.neutralAlpha9}>
|
|
47
|
+
{volume}%
|
|
48
|
+
</Text>
|
|
49
|
+
</Flex>
|
|
50
|
+
<SliderLibrary
|
|
51
|
+
value={volume}
|
|
52
|
+
onValueChange={onVolumeChange}
|
|
53
|
+
formatValue={(v) => `${v}%`}
|
|
54
|
+
aria-label="Volume"
|
|
55
|
+
/>
|
|
56
|
+
</Flex>
|
|
57
|
+
|
|
58
|
+
<PanControl value={panValue} onChange={onPanChange} />
|
|
59
|
+
</Flex>
|
|
60
|
+
|
|
61
|
+
<Flex justify="center" width="100%">
|
|
62
|
+
<Separator size="1" className={styles.neutralAlpha4} />
|
|
63
|
+
</Flex>
|
|
64
|
+
|
|
65
|
+
<SegmentedField
|
|
66
|
+
label="Count-in"
|
|
67
|
+
items={[
|
|
68
|
+
{ value: 'off', label: 'Off' },
|
|
69
|
+
{ value: '1 bar', label: '1 bar' },
|
|
70
|
+
{ value: '2 bars', label: '2 bars' },
|
|
71
|
+
]}
|
|
72
|
+
value={countIn}
|
|
73
|
+
onChange={onCountInChange}
|
|
74
|
+
/>
|
|
75
|
+
</Flex>
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
PlayerSmartMetronome.displayName = 'PlayerSmartMetronome'
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.PlayerSmartMetronome {
|
|
2
|
+
min-width: 258px;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.neutral12 {
|
|
6
|
+
color: var(--neutral-12);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.neutralAlpha4 {
|
|
10
|
+
color: var(--neutral-alpha-4);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.flex1 {
|
|
14
|
+
flex: 1;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.neutralAlpha9 {
|
|
18
|
+
color: var(--neutral-alpha-9);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.white {
|
|
22
|
+
color: var(--white);
|
|
23
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
2
|
+
import { PlayerSmartMetronome } from './PlayerSmartMetronome'
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
subDivision: '1x',
|
|
6
|
+
volume: 50,
|
|
7
|
+
countIn: 'off',
|
|
8
|
+
panValue: 0,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function StatefulPlayerSmartMetronome({
|
|
12
|
+
initialSubDivision = DEFAULTS.subDivision,
|
|
13
|
+
initialVolume = DEFAULTS.volume,
|
|
14
|
+
initialCountIn = DEFAULTS.countIn,
|
|
15
|
+
initialPanValue = DEFAULTS.panValue,
|
|
16
|
+
onSubDivisionChange,
|
|
17
|
+
onVolumeChange,
|
|
18
|
+
onCountInChange,
|
|
19
|
+
onPanChange,
|
|
20
|
+
onRefresh,
|
|
21
|
+
}) {
|
|
22
|
+
const [subDivision, setSubDivision] = useState(initialSubDivision)
|
|
23
|
+
const [volume, setVolume] = useState(initialVolume)
|
|
24
|
+
const [countIn, setCountIn] = useState(initialCountIn)
|
|
25
|
+
const [panValue, setPanValue] = useState(initialPanValue)
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
setSubDivision(initialSubDivision)
|
|
29
|
+
}, [initialSubDivision])
|
|
30
|
+
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
setVolume(initialVolume)
|
|
33
|
+
}, [initialVolume])
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
setCountIn(initialCountIn)
|
|
37
|
+
}, [initialCountIn])
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
setPanValue(initialPanValue)
|
|
41
|
+
}, [initialPanValue])
|
|
42
|
+
|
|
43
|
+
const handleRefresh = useCallback(() => {
|
|
44
|
+
setSubDivision(DEFAULTS.subDivision)
|
|
45
|
+
setVolume(DEFAULTS.volume)
|
|
46
|
+
setCountIn(DEFAULTS.countIn)
|
|
47
|
+
setPanValue(DEFAULTS.panValue)
|
|
48
|
+
onRefresh?.()
|
|
49
|
+
}, [onRefresh])
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<PlayerSmartMetronome
|
|
53
|
+
subDivision={subDivision}
|
|
54
|
+
volume={volume}
|
|
55
|
+
countIn={countIn}
|
|
56
|
+
panValue={panValue}
|
|
57
|
+
onSubDivisionChange={(value) => {
|
|
58
|
+
setSubDivision(value)
|
|
59
|
+
onSubDivisionChange?.(value)
|
|
60
|
+
}}
|
|
61
|
+
onVolumeChange={(value) => {
|
|
62
|
+
setVolume(value)
|
|
63
|
+
onVolumeChange?.(value)
|
|
64
|
+
}}
|
|
65
|
+
onCountInChange={(value) => {
|
|
66
|
+
setCountIn(value)
|
|
67
|
+
onCountInChange?.(value)
|
|
68
|
+
}}
|
|
69
|
+
onPanChange={(value) => {
|
|
70
|
+
setPanValue(value)
|
|
71
|
+
onPanChange?.(value)
|
|
72
|
+
}}
|
|
73
|
+
onRefresh={handleRefresh}
|
|
74
|
+
/>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default {
|
|
79
|
+
title: 'New Mixer/PlayerSmartMetronome',
|
|
80
|
+
component: PlayerSmartMetronome,
|
|
81
|
+
parameters: {
|
|
82
|
+
layout: 'centered',
|
|
83
|
+
docs: {
|
|
84
|
+
description: {
|
|
85
|
+
component: `
|
|
86
|
+
Smart metronome panel for New Mixer. Control sub-division, volume, pan and count-in via props and their change handlers.
|
|
87
|
+
`,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
decorators: [
|
|
92
|
+
(Story) => (
|
|
93
|
+
<div
|
|
94
|
+
style={{
|
|
95
|
+
width: 258,
|
|
96
|
+
padding: 16,
|
|
97
|
+
background: '#1d1d1d',
|
|
98
|
+
borderRadius: 8,
|
|
99
|
+
boxSizing: 'border-box',
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
<Story />
|
|
103
|
+
</div>
|
|
104
|
+
),
|
|
105
|
+
],
|
|
106
|
+
tags: ['autodocs'],
|
|
107
|
+
argTypes: {
|
|
108
|
+
subDivision: {
|
|
109
|
+
description: 'Selected sub-division multiplier.',
|
|
110
|
+
table: { type: { summary: "'0.5x' | '1x' | '2x'" }, defaultValue: { summary: '1x' } },
|
|
111
|
+
control: 'select',
|
|
112
|
+
options: ['0.5x', '1x', '2x'],
|
|
113
|
+
},
|
|
114
|
+
volume: {
|
|
115
|
+
description: 'Metronome volume percentage (0–100).',
|
|
116
|
+
table: { type: { summary: 'number' }, defaultValue: { summary: 50 } },
|
|
117
|
+
control: { type: 'range', min: 0, max: 100, step: 1 },
|
|
118
|
+
},
|
|
119
|
+
countIn: {
|
|
120
|
+
description: 'Count-in bars before playback.',
|
|
121
|
+
table: { type: { summary: "'off' | '1 bar' | '2 bars'" }, defaultValue: { summary: 'off' } },
|
|
122
|
+
control: 'select',
|
|
123
|
+
options: ['off', '1 bar', '2 bars'],
|
|
124
|
+
},
|
|
125
|
+
panValue: {
|
|
126
|
+
description: 'Pan position passed to PanControl.',
|
|
127
|
+
table: { type: { summary: 'number' }, defaultValue: { summary: 0 } },
|
|
128
|
+
control: { type: 'range', min: -1, max: 1, step: 0.01 },
|
|
129
|
+
},
|
|
130
|
+
onSubDivisionChange: {
|
|
131
|
+
table: { type: { summary: '(value: string) => void' } },
|
|
132
|
+
action: 'sub-division changed',
|
|
133
|
+
},
|
|
134
|
+
onVolumeChange: {
|
|
135
|
+
table: { type: { summary: '(value: number) => void' } },
|
|
136
|
+
action: 'volume changed',
|
|
137
|
+
},
|
|
138
|
+
onCountInChange: {
|
|
139
|
+
table: { type: { summary: '(value: string) => void' } },
|
|
140
|
+
action: 'count-in changed',
|
|
141
|
+
},
|
|
142
|
+
onPanChange: {
|
|
143
|
+
table: { type: { summary: '(value: number) => void' } },
|
|
144
|
+
action: 'pan changed',
|
|
145
|
+
},
|
|
146
|
+
onRefresh: {
|
|
147
|
+
table: { type: { summary: '() => void' } },
|
|
148
|
+
action: 'refreshed',
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const Default = {
|
|
154
|
+
render: (args) => (
|
|
155
|
+
<StatefulPlayerSmartMetronome
|
|
156
|
+
initialSubDivision={args.subDivision}
|
|
157
|
+
initialVolume={args.volume}
|
|
158
|
+
initialCountIn={args.countIn}
|
|
159
|
+
initialPanValue={args.panValue}
|
|
160
|
+
onSubDivisionChange={args.onSubDivisionChange}
|
|
161
|
+
onVolumeChange={args.onVolumeChange}
|
|
162
|
+
onCountInChange={args.onCountInChange}
|
|
163
|
+
onPanChange={args.onPanChange}
|
|
164
|
+
onRefresh={args.onRefresh}
|
|
165
|
+
/>
|
|
166
|
+
),
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const HalfSubDivision = {
|
|
170
|
+
args: {
|
|
171
|
+
subDivision: '0.5x',
|
|
172
|
+
volume: 75,
|
|
173
|
+
},
|
|
174
|
+
render: (args) => (
|
|
175
|
+
<StatefulPlayerSmartMetronome
|
|
176
|
+
initialSubDivision={args.subDivision}
|
|
177
|
+
initialVolume={args.volume}
|
|
178
|
+
onSubDivisionChange={args.onSubDivisionChange}
|
|
179
|
+
onVolumeChange={args.onVolumeChange}
|
|
180
|
+
onCountInChange={args.onCountInChange}
|
|
181
|
+
onPanChange={args.onPanChange}
|
|
182
|
+
onRefresh={args.onRefresh}
|
|
183
|
+
/>
|
|
184
|
+
),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const WithCountIn = {
|
|
188
|
+
args: {
|
|
189
|
+
countIn: '2 bars',
|
|
190
|
+
volume: 80,
|
|
191
|
+
},
|
|
192
|
+
render: (args) => (
|
|
193
|
+
<StatefulPlayerSmartMetronome
|
|
194
|
+
initialCountIn={args.countIn}
|
|
195
|
+
initialVolume={args.volume}
|
|
196
|
+
onSubDivisionChange={args.onSubDivisionChange}
|
|
197
|
+
onVolumeChange={args.onVolumeChange}
|
|
198
|
+
onCountInChange={args.onCountInChange}
|
|
199
|
+
onPanChange={args.onPanChange}
|
|
200
|
+
onRefresh={args.onRefresh}
|
|
201
|
+
/>
|
|
202
|
+
),
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const Playground = {
|
|
206
|
+
args: {
|
|
207
|
+
subDivision: '1x',
|
|
208
|
+
volume: 50,
|
|
209
|
+
countIn: 'off',
|
|
210
|
+
panValue: 0,
|
|
211
|
+
},
|
|
212
|
+
render: (args) => (
|
|
213
|
+
<StatefulPlayerSmartMetronome
|
|
214
|
+
initialSubDivision={args.subDivision}
|
|
215
|
+
initialVolume={args.volume}
|
|
216
|
+
initialCountIn={args.countIn}
|
|
217
|
+
initialPanValue={args.panValue}
|
|
218
|
+
onSubDivisionChange={args.onSubDivisionChange}
|
|
219
|
+
onVolumeChange={args.onVolumeChange}
|
|
220
|
+
onCountInChange={args.onCountInChange}
|
|
221
|
+
onPanChange={args.onPanChange}
|
|
222
|
+
onRefresh={args.onRefresh}
|
|
223
|
+
/>
|
|
224
|
+
),
|
|
225
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Flex, Text, SegmentedControl } from '../../index'
|
|
2
|
+
import styles from './PlayerSmartMetronome.module.css'
|
|
3
|
+
|
|
4
|
+
export function SegmentedField({ label, items, value, onChange }) {
|
|
5
|
+
return (
|
|
6
|
+
<Flex direction="column" gap="1">
|
|
7
|
+
<Text size="2" className={styles.neutral12}>
|
|
8
|
+
{label}
|
|
9
|
+
</Text>
|
|
10
|
+
|
|
11
|
+
<SegmentedControl
|
|
12
|
+
items={items}
|
|
13
|
+
selectedType={value}
|
|
14
|
+
handleTypeChange={onChange}
|
|
15
|
+
size="2"
|
|
16
|
+
/>
|
|
17
|
+
</Flex>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { Flex, Separator, Text, IconButton, Select } from '../../index'
|
|
2
|
+
import styles from './PlayerTempoPitch.module.css'
|
|
3
|
+
import { RefreshBackIcon } from '../../icons'
|
|
4
|
+
import { MIN_VALUE_BPM, MAX_VALUE_BPM, TEMPO_RANGES, PITCH_ITEMS, TUNING_ITEMS } from './constants'
|
|
5
|
+
import { StepperField } from './StepperField'
|
|
6
|
+
|
|
7
|
+
export const PlayerTempoPitch = ({
|
|
8
|
+
tempo = 120,
|
|
9
|
+
pitch = 'C#',
|
|
10
|
+
tuning = 440,
|
|
11
|
+
onTempoChange,
|
|
12
|
+
onPitchChange,
|
|
13
|
+
onTuningChange,
|
|
14
|
+
onRefresh,
|
|
15
|
+
}) => {
|
|
16
|
+
const handleTempoChange = (e) => {
|
|
17
|
+
const raw = e.target.value
|
|
18
|
+
|
|
19
|
+
if (raw === '') {
|
|
20
|
+
onTempoChange?.('')
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const parsed = Number(raw)
|
|
25
|
+
if (Number.isNaN(parsed)) return
|
|
26
|
+
|
|
27
|
+
onTempoChange?.(parsed)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handleTempoBlur = () => {
|
|
31
|
+
const clamped = Math.min(
|
|
32
|
+
MAX_VALUE_BPM,
|
|
33
|
+
Math.max(MIN_VALUE_BPM, Number(tempo) || MIN_VALUE_BPM),
|
|
34
|
+
)
|
|
35
|
+
onTempoChange?.(clamped)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const handleTempoDecrease = () => {
|
|
39
|
+
onTempoChange?.(Math.max(MIN_VALUE_BPM, tempo - 1))
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const handleTempoIncrease = () => {
|
|
43
|
+
onTempoChange?.(Math.min(MAX_VALUE_BPM, tempo + 1))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const handlePitchChange = (e) => {
|
|
47
|
+
onPitchChange?.(e.target.value)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const handlePitchBlur = () => {
|
|
51
|
+
const exists = PITCH_ITEMS.find((item) => item.value === pitch)
|
|
52
|
+
if (!exists) onPitchChange?.(PITCH_ITEMS[0].value)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const handlePitchDecrease = () => {
|
|
56
|
+
const currentIndex = PITCH_ITEMS.findIndex((item) => item.value === pitch)
|
|
57
|
+
if (currentIndex > 0) onPitchChange?.(PITCH_ITEMS[currentIndex - 1].value)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const handlePitchIncrease = () => {
|
|
61
|
+
const currentIndex = PITCH_ITEMS.findIndex((item) => item.value === pitch)
|
|
62
|
+
if (currentIndex < PITCH_ITEMS.length - 1) {
|
|
63
|
+
onPitchChange?.(PITCH_ITEMS[currentIndex + 1].value)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<Flex direction="column" gap="4">
|
|
69
|
+
<Flex align="center" gap="2" justify="between">
|
|
70
|
+
<Text size="3" weight="medium" className={styles.white}>
|
|
71
|
+
Tempo & Pitch
|
|
72
|
+
</Text>
|
|
73
|
+
|
|
74
|
+
<IconButton variant="ghost" size="1" onClick={onRefresh}>
|
|
75
|
+
<RefreshBackIcon width={16} height={16} />
|
|
76
|
+
</IconButton>
|
|
77
|
+
</Flex>
|
|
78
|
+
|
|
79
|
+
<StepperField
|
|
80
|
+
label="Tempo change"
|
|
81
|
+
hint={TEMPO_RANGES.find((item) => item.maxBpm >= tempo)?.marking}
|
|
82
|
+
value={tempo}
|
|
83
|
+
onChange={handleTempoChange}
|
|
84
|
+
onBlur={handleTempoBlur}
|
|
85
|
+
onDecrease={handleTempoDecrease}
|
|
86
|
+
onIncrease={handleTempoIncrease}
|
|
87
|
+
disableDecrease={tempo <= MIN_VALUE_BPM}
|
|
88
|
+
disableIncrease={tempo >= MAX_VALUE_BPM}
|
|
89
|
+
/>
|
|
90
|
+
|
|
91
|
+
<Separator size="4" className={styles.neutralAlpha4} />
|
|
92
|
+
|
|
93
|
+
<StepperField
|
|
94
|
+
label="Pitch change"
|
|
95
|
+
value={pitch}
|
|
96
|
+
onChange={handlePitchChange}
|
|
97
|
+
onBlur={handlePitchBlur}
|
|
98
|
+
onDecrease={handlePitchDecrease}
|
|
99
|
+
onIncrease={handlePitchIncrease}
|
|
100
|
+
disableDecrease={pitch === PITCH_ITEMS[0].value}
|
|
101
|
+
disableIncrease={pitch === PITCH_ITEMS[PITCH_ITEMS.length - 1].value}
|
|
102
|
+
/>
|
|
103
|
+
|
|
104
|
+
<Flex direction="column" gap="1">
|
|
105
|
+
<Text size="2" className={styles.neutral12}>
|
|
106
|
+
Tuning (Hz)
|
|
107
|
+
</Text>
|
|
108
|
+
|
|
109
|
+
<Select
|
|
110
|
+
value={tuning}
|
|
111
|
+
onChange={onTuningChange}
|
|
112
|
+
items={TUNING_ITEMS}
|
|
113
|
+
placeholder="Select tuning"
|
|
114
|
+
size="2"
|
|
115
|
+
variant="soft"
|
|
116
|
+
maxHeight="300px"
|
|
117
|
+
/>
|
|
118
|
+
</Flex>
|
|
119
|
+
</Flex>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
PlayerTempoPitch.displayName = 'PlayerTempoPitch'
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
.PlayerTempoPitch {
|
|
2
|
+
background-color: var(--neutral-alpha-2);
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.neutral12 {
|
|
6
|
+
color: var(--neutral-12);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.neutralAlpha9 {
|
|
10
|
+
color: var(--neutral-alpha-9);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.neutralAlpha4 {
|
|
14
|
+
color: var(--neutral-alpha-4);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.white {
|
|
18
|
+
color: var(--white);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.customTextField {
|
|
22
|
+
input {
|
|
23
|
+
text-align: center;
|
|
24
|
+
}
|
|
25
|
+
}
|