@moises.ai/design-system 4.16.6 → 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.
Files changed (26) hide show
  1. package/dist/{ForwardBarFillIcon-CUEgLgyx.js → ChordDiagramGridIcon-CxpGQmg4.js} +1029 -976
  2. package/dist/colors/custom-styles.css +1 -0
  3. package/dist/icons.js +604 -602
  4. package/dist/{index-f-eJMg6F.js → index-Dwaw5zqT.js} +151 -151
  5. package/dist/index.js +253 -253
  6. package/dist/primitives.js +147 -147
  7. package/package.json +1 -1
  8. package/src/colors/custom-styles.css +1 -0
  9. package/src/components/PlayerBar/PlayerBar.jsx +12 -11
  10. package/src/components/PlayerBar/PlayerBar.stories.jsx +193 -8
  11. package/src/components/PlayerMasterVolume/PlayerMasterVolume.jsx +54 -0
  12. package/src/components/PlayerMasterVolume/PlayerMasterVolume.module.css +31 -0
  13. package/src/components/PlayerMasterVolume/PlayerMasterVolume.stories.jsx +182 -0
  14. package/src/components/PlayerSmartMetronome/PlayerSmartMetronome.jsx +79 -0
  15. package/src/components/PlayerSmartMetronome/PlayerSmartMetronome.module.css +23 -0
  16. package/src/components/PlayerSmartMetronome/PlayerSmartMetronome.stories.jsx +225 -0
  17. package/src/components/PlayerSmartMetronome/SegmentedField.jsx +19 -0
  18. package/src/components/PlayerTempoPitch/PlayerTempoPitch.jsx +123 -0
  19. package/src/components/PlayerTempoPitch/PlayerTempoPitch.module.css +25 -0
  20. package/src/components/PlayerTempoPitch/PlayerTempoPitch.stories.jsx +215 -0
  21. package/src/components/PlayerTempoPitch/StepperField.jsx +57 -0
  22. package/src/components/PlayerTempoPitch/constants.js +40 -0
  23. package/src/icons/ChordDiagramGridIcon.jsx +22 -0
  24. package/src/icons/ChordGrid3Icon.jsx +20 -0
  25. package/src/icons.jsx +2 -0
  26. package/src/index.jsx +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "4.16.6",
3
+ "version": "4.16.7",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -155,6 +155,7 @@
155
155
 
156
156
  /* Error */
157
157
  --error-alpha-5: rgba(255, 32, 71, 0.34);
158
+ --error-alpha-10: #EC5D5E;
158
159
  --error-alpha-11: #FF9592;
159
160
  --error-alpha-12: #FFD1D9;
160
161
 
@@ -20,7 +20,6 @@ import { Text, Flex, IconButton, DropdownMenu, DropdownButton, Separator, Popove
20
20
  import styles from './PlayerBar.module.css'
21
21
  import { PlayerSlider } from '../PlayerSlider/PlayerSlider'
22
22
 
23
-
24
23
  const formatTime = (time) => {
25
24
  const totalSeconds = Math.floor(time)
26
25
  const minutes = Math.floor(totalSeconds / 60)
@@ -140,16 +139,18 @@ export const PlayerBar = ({
140
139
 
141
140
  <Flex className={styles.flex1} direction="column" gap="0" align="center">
142
141
  <Flex align="center" gap="3">
143
- <Popover.Root>
144
- <Popover.Trigger>
145
- <IconButton variant="ghost" size="2" onClick={onVolumeClick}>
146
- <Volume2Icon width={16} height={16} />
147
- </IconButton>
148
- </Popover.Trigger>
149
- <Popover.Content>
150
- {volumeContent}
151
- </Popover.Content>
152
- </Popover.Root>
142
+ {volumeContent && (
143
+ <Popover.Root>
144
+ <Popover.Trigger>
145
+ <IconButton variant="ghost" size="2" onClick={onVolumeClick}>
146
+ <Volume2Icon width={16} height={16} />
147
+ </IconButton>
148
+ </Popover.Trigger>
149
+ <Popover.Content>
150
+ {volumeContent}
151
+ </Popover.Content>
152
+ </Popover.Root>
153
+ )}
153
154
  <Flex gap='0' >
154
155
  <IconButton variant="ghost" size="4" onClick={onSeekBackward} className={styles.controlsIconButton}>
155
156
  <RewindBarFillIcon width={30} height={30} />
@@ -2,6 +2,9 @@ import { useState } from 'react'
2
2
  import { Flex } from '@radix-ui/themes'
3
3
  import { Text } from '../../index'
4
4
  import { PlayerBar } from './PlayerBar'
5
+ import { PlayerMasterVolume } from '../PlayerMasterVolume/PlayerMasterVolume'
6
+ import { PlayerTempoPitch } from '../PlayerTempoPitch/PlayerTempoPitch'
7
+ import { PlayerSmartMetronome } from '../PlayerSmartMetronome/PlayerSmartMetronome'
5
8
 
6
9
  const SAMPLE_AUDIO =
7
10
  'https://storage.googleapis.com/moises-api-assets/voice/demo_zuri.m4a'
@@ -17,6 +20,22 @@ function StatefulPlayerBar(args) {
17
20
  const [lyrics, setLyrics] = useState(args.lyricsEnabled ?? false)
18
21
  const [metronome, setMetronome] = useState(args.metronomeEnabled ?? false)
19
22
  const [expanded, setExpanded] = useState(args.expanded ?? false)
23
+ const [volume, setVolume] = useState(args.volume ?? 1)
24
+ const [tempo, setTempo] = useState(args.tempo ?? 120)
25
+ const [pitch, setPitch] = useState(args.pitch ?? 'C#')
26
+ const [tuning, setTuning] = useState(args.tuning ?? 440)
27
+ const [subDivision, setSubDivision] = useState(args.subDivision ?? '1x')
28
+ const [metronomeVolume, setMetronomeVolume] = useState(args.metronomeVolume ?? 50)
29
+ const [countIn, setCountIn] = useState(args.countIn ?? 'off')
30
+ const [panValue, setPanValue] = useState(args.panValue ?? 0)
31
+ const [stems, setStems] = useState(
32
+ args.stems ?? [
33
+ { name: 'Vocals', volume: 80 },
34
+ { name: 'Drums', volume: 65 },
35
+ { name: 'Bass', volume: 70 },
36
+ { name: 'Others', volume: 55 },
37
+ ],
38
+ )
20
39
 
21
40
  const seekBy = (delta) => {
22
41
  setCurrentTime((time) =>
@@ -36,6 +55,82 @@ function StatefulPlayerBar(args) {
36
55
  lyricsEnabled={lyrics}
37
56
  metronomeEnabled={metronome}
38
57
  expanded={expanded}
58
+ tempo={tempo}
59
+ pitch={pitch}
60
+ volumeContent={
61
+ <PlayerMasterVolume
62
+ volume={volume}
63
+ stems={stems}
64
+ onVolumeChange={([value]) => {
65
+ setVolume(value)
66
+ args.onVolumeChange?.([value])
67
+ }}
68
+ onStemVolumeChange={(name, value) => {
69
+ setStems((prev) =>
70
+ prev.map((stem) =>
71
+ stem.name === name ? { ...stem, volume: value } : stem,
72
+ ),
73
+ )
74
+ args.onStemVolumeChange?.(name, value)
75
+ }}
76
+ />
77
+ }
78
+ tempoPitchContent={
79
+ <PlayerTempoPitch
80
+ tempo={tempo}
81
+ pitch={pitch}
82
+ tuning={tuning}
83
+ onTempoChange={(value) => {
84
+ setTempo(value)
85
+ args.onTempoChange?.(value)
86
+ }}
87
+ onPitchChange={(value) => {
88
+ setPitch(value)
89
+ args.onPitchChange?.(value)
90
+ }}
91
+ onTuningChange={(value) => {
92
+ setTuning(value)
93
+ args.onTuningChange?.(value)
94
+ }}
95
+ onRefresh={() => {
96
+ setTempo(120)
97
+ setPitch('C#')
98
+ setTuning(440)
99
+ args.onTempoPitchRefresh?.()
100
+ }}
101
+ />
102
+ }
103
+ metronomeContent={
104
+ <PlayerSmartMetronome
105
+ subDivision={subDivision}
106
+ volume={metronomeVolume}
107
+ countIn={countIn}
108
+ panValue={panValue}
109
+ onSubDivisionChange={(value) => {
110
+ setSubDivision(value)
111
+ args.onSubDivisionChange?.(value)
112
+ }}
113
+ onVolumeChange={(value) => {
114
+ setMetronomeVolume(value)
115
+ args.onMetronomeVolumeChange?.(value)
116
+ }}
117
+ onCountInChange={(value) => {
118
+ setCountIn(value)
119
+ args.onCountInChange?.(value)
120
+ }}
121
+ onPanChange={(value) => {
122
+ setPanValue(value)
123
+ args.onPanChange?.(value)
124
+ }}
125
+ onRefresh={() => {
126
+ setSubDivision('1x')
127
+ setMetronomeVolume(50)
128
+ setCountIn('off')
129
+ setPanValue(0)
130
+ args.onMetronomeRefresh?.()
131
+ }}
132
+ />
133
+ }
39
134
  onPlayPause={() => setIsPlaying((value) => !value)}
40
135
  onSeek={setCurrentTime}
41
136
  onSeekBackward={() => seekBy(-10)}
@@ -145,13 +240,51 @@ Use it as a controlled component: pass playback state through props and update i
145
240
  tempo: {
146
241
  description: 'Tempo value shown in the tempo/pitch dropdown trigger.',
147
242
  table: { type: { summary: 'number | string' } },
148
- control: 'text',
243
+ control: 'number',
149
244
  },
150
245
  pitch: {
151
246
  description: 'Pitch value shown in the tempo/pitch dropdown trigger.',
152
247
  table: { type: { summary: 'string' } },
153
248
  control: 'text',
154
249
  },
250
+ tuning: {
251
+ description: 'Reference tuning in Hz for PlayerTempoPitch.',
252
+ table: { type: { summary: 'number' }, defaultValue: { summary: 440 } },
253
+ control: 'select',
254
+ options: [440, 442, 444, 446],
255
+ },
256
+ volume: {
257
+ description: 'Master gain passed to PlayerMasterVolume.',
258
+ table: { type: { summary: 'number' }, defaultValue: { summary: 1 } },
259
+ control: { type: 'range', min: 0, max: 2, step: 0.01 },
260
+ },
261
+ subDivision: {
262
+ description: 'Metronome sub-division for PlayerSmartMetronome.',
263
+ table: { type: { summary: "'0.5x' | '1x' | '2x'" }, defaultValue: { summary: '1x' } },
264
+ control: 'select',
265
+ options: ['0.5x', '1x', '2x'],
266
+ },
267
+ metronomeVolume: {
268
+ description: 'Metronome volume percentage (0–100).',
269
+ table: { type: { summary: 'number' }, defaultValue: { summary: 50 } },
270
+ control: { type: 'range', min: 0, max: 100, step: 1 },
271
+ },
272
+ countIn: {
273
+ description: 'Metronome count-in setting.',
274
+ table: { type: { summary: "'off' | '1 bar' | '2 bars'" }, defaultValue: { summary: 'off' } },
275
+ control: 'select',
276
+ options: ['off', '1 bar', '2 bars'],
277
+ },
278
+ panValue: {
279
+ description: 'Metronome pan position.',
280
+ table: { type: { summary: 'number' }, defaultValue: { summary: 0 } },
281
+ control: { type: 'range', min: -1, max: 1, step: 0.01 },
282
+ },
283
+ stems: {
284
+ description: 'Stem rows for PlayerMasterVolume.',
285
+ table: { type: { summary: '{ name: string, volume: number }[]' } },
286
+ control: false,
287
+ },
155
288
  loopEnabled: {
156
289
  description: 'Whether the loop control is active.',
157
290
  table: { type: { summary: 'boolean' } },
@@ -195,6 +328,11 @@ Use it as a controlled component: pass playback state through props and update i
195
328
  table: { type: { summary: 'ReactNode' } },
196
329
  control: false,
197
330
  },
331
+ volumeContent: {
332
+ description: 'Content rendered inside the volume popover.',
333
+ table: { type: { summary: 'ReactNode' } },
334
+ control: false,
335
+ },
198
336
  tempoPitchContent: {
199
337
  description: 'Content rendered inside the tempo/pitch dropdown.',
200
338
  table: { type: { summary: 'ReactNode' } },
@@ -205,10 +343,60 @@ Use it as a controlled component: pass playback state through props and update i
205
343
  table: { type: { summary: 'ReactNode' } },
206
344
  control: false,
207
345
  },
208
- volumeContent: {
209
- description: 'Content rendered inside the volume popover.',
210
- table: { type: { summary: 'ReactNode' } },
211
- control: false,
346
+ onVolumeChange: {
347
+ description: 'Called with `[gain]` when the master fader moves.',
348
+ table: { type: { summary: '([gain: number]) => void' } },
349
+ action: 'volume changed',
350
+ },
351
+ onStemVolumeChange: {
352
+ description: 'Called when a stem slider changes.',
353
+ table: { type: { summary: '(name: string, value: number) => void' } },
354
+ action: 'stem volume changed',
355
+ },
356
+ onTempoChange: {
357
+ description: 'Called when tempo changes in PlayerTempoPitch.',
358
+ table: { type: { summary: '(tempo: number | "") => void' } },
359
+ action: 'tempo changed',
360
+ },
361
+ onPitchChange: {
362
+ description: 'Called when pitch changes in PlayerTempoPitch.',
363
+ table: { type: { summary: '(pitch: string) => void' } },
364
+ action: 'pitch changed',
365
+ },
366
+ onTuningChange: {
367
+ description: 'Called when tuning changes in PlayerTempoPitch.',
368
+ table: { type: { summary: '(tuning: number) => void' } },
369
+ action: 'tuning changed',
370
+ },
371
+ onTempoPitchRefresh: {
372
+ description: 'Called when PlayerTempoPitch refresh is clicked.',
373
+ table: { type: { summary: '() => void' } },
374
+ action: 'tempo/pitch refreshed',
375
+ },
376
+ onSubDivisionChange: {
377
+ description: 'Called when metronome sub-division changes.',
378
+ table: { type: { summary: '(value: string) => void' } },
379
+ action: 'sub-division changed',
380
+ },
381
+ onMetronomeVolumeChange: {
382
+ description: 'Called when metronome volume changes.',
383
+ table: { type: { summary: '(value: number) => void' } },
384
+ action: 'metronome volume changed',
385
+ },
386
+ onCountInChange: {
387
+ description: 'Called when metronome count-in changes.',
388
+ table: { type: { summary: '(value: string) => void' } },
389
+ action: 'count-in changed',
390
+ },
391
+ onPanChange: {
392
+ description: 'Called when metronome pan changes.',
393
+ table: { type: { summary: '(value: number) => void' } },
394
+ action: 'pan changed',
395
+ },
396
+ onMetronomeRefresh: {
397
+ description: 'Called when PlayerSmartMetronome refresh is clicked.',
398
+ table: { type: { summary: '() => void' } },
399
+ action: 'metronome refreshed',
212
400
  },
213
401
  onPlayPause: {
214
402
  description: 'Called when the main play/pause button is clicked.',
@@ -295,9 +483,6 @@ const defaultArgs = {
295
483
  ],
296
484
  versionTrigger: <Text>v6</Text>,
297
485
  versionContent: <Text>Version 6</Text>,
298
- tempoPitchContent: <Text>120 bpm / 4/4</Text>,
299
- metronomeContent: <Text>Metronome</Text>,
300
- volumeContent: <Text>Volume</Text>,
301
486
  onVolumeClick: () => { },
302
487
  onVideoClick: () => { },
303
488
  onShareClick: () => { },
@@ -0,0 +1,54 @@
1
+ import styles from './PlayerMasterVolume.module.css'
2
+ import classNames from 'classnames'
3
+ import { Text, Flex, InputLevelMeter, Button, Separator, SliderLibrary } from '../../index'
4
+ import { ChevronUpIcon, ChevronDownIcon } from '@radix-ui/react-icons'
5
+ import { useState } from 'react'
6
+
7
+ export const PlayerMasterVolume = ({
8
+ onVolumeChange,
9
+ onStemVolumeChange,
10
+ volume = 1,
11
+ stems,
12
+
13
+ }) => {
14
+ const [stemsOpen, setStemsOpen] = useState(false)
15
+
16
+ return (
17
+ <Flex className={styles.PlayerMasterVolume} direction="column">
18
+ <Flex direction="column" gap="4">
19
+ <Text size="3" weight="medium" className={styles.white}>
20
+ Master Volume
21
+ </Text>
22
+ <Flex gap="4" align="center">
23
+ <InputLevelMeter
24
+ linear={[0, 0]}
25
+ volume={volume}
26
+ onVolumeChange={onVolumeChange}
27
+ />
28
+ <Button variant="soft" size="1">−∞dB</Button>
29
+ </Flex>
30
+ {stems?.length > 0 && <Separator size='4' />}
31
+ </Flex>
32
+ {stems?.length > 0 && (
33
+ <>
34
+ <Flex width='100%' justify='between' mt="2" as="button" align="center" onClick={() => setStemsOpen(!stemsOpen)} className={styles.accordionButton}>
35
+ <Text size="2" className={styles.neutralAlpha11}>Stems</Text>
36
+ {stemsOpen ? <ChevronUpIcon className={styles.neutralAlpha9} width={16} height={16} /> : <ChevronDownIcon className={styles.neutralAlpha9} width={16} height={16} />}
37
+ </Flex>
38
+ {stemsOpen && (
39
+ <Flex direction="column" gap="3" className={styles.accordionContent}>
40
+ {stems.map((stem) => (
41
+ <Flex width='100%' gap="3" key={stem.name}>
42
+ <Text size="2" className={classNames(styles.neutralAlpha11, styles.flex1)}>{stem.name}</Text>
43
+ <SliderLibrary value={stem.volume} onValueChange={(value) => onStemVolumeChange?.(stem.name, value)} className={styles.flex2} />
44
+ </Flex>
45
+ ))}
46
+ </Flex>
47
+ )}
48
+ </>
49
+ )}
50
+ </Flex>
51
+ )
52
+ }
53
+
54
+ PlayerMasterVolume.displayName = 'PlayerMasterVolume'
@@ -0,0 +1,31 @@
1
+ .PlayerMasterVolume {
2
+ min-width: 286px;
3
+ }
4
+
5
+ .neutralAlpha11 {
6
+ color: var(--neutral-alpha-11);
7
+ }
8
+
9
+ .accordionButton {
10
+ padding: 6px 0;
11
+
12
+ &:hover {
13
+ cursor: pointer;
14
+ }
15
+ }
16
+
17
+ .neutralAlpha9 {
18
+ color: var(--neutral-alpha-9);
19
+ }
20
+
21
+ .accordionContent {
22
+ padding: 12px 0 0 0;
23
+ }
24
+
25
+ .flex1 {
26
+ flex: 1;
27
+ }
28
+
29
+ .flex2 {
30
+ flex: 2;
31
+ }
@@ -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
+ }