@moises.ai/design-system 3.11.8 → 3.11.10

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/{ChevronDownFilledIcon-qgjYUwMz.js → TrackPreviousIcon-QO8TnAez.js} +2457 -2609
  2. package/dist/icons.js +1 -1
  3. package/dist/index.js +3179 -3112
  4. package/package.json +1 -1
  5. package/src/components/InputLevelMeter/InputLevelMeter.jsx +29 -7
  6. package/src/components/InputLevelMeter/InputLevelMeter.module.css +4 -0
  7. package/src/components/InputLevelMeter/InputLevelMeter.stories.jsx +290 -3
  8. package/src/components/PeakLevel/PeakLevel.jsx +71 -0
  9. package/src/components/PeakLevel/PeakLevel.module.css +49 -0
  10. package/src/components/PeakLevel/PeakLevel.stories.jsx +192 -0
  11. package/src/icons/AlertIcon.jsx +2 -7
  12. package/src/icons/BarsIcon.jsx +1 -6
  13. package/src/icons/CameraRollIcon.jsx +1 -6
  14. package/src/icons/CartIcon.jsx +2 -7
  15. package/src/icons/ClipIcon.jsx +2 -7
  16. package/src/icons/CloudDownloadGradientIcon.jsx +2 -5
  17. package/src/icons/CodeIcon.jsx +2 -7
  18. package/src/icons/CountdownIcon.jsx +2 -7
  19. package/src/icons/CropIcon.jsx +2 -7
  20. package/src/icons/DialogueIcon.jsx +2 -7
  21. package/src/icons/DotsVerticalIcon.jsx +2 -7
  22. package/src/icons/FolderIcon.jsx +2 -7
  23. package/src/icons/GoalsIcon.jsx +2 -7
  24. package/src/icons/IsolateDrumsGradientIcon.jsx +2 -5
  25. package/src/icons/IsolateDrumsIcon.jsx +2 -7
  26. package/src/icons/Knob2Icon.jsx +2 -7
  27. package/src/icons/Knob3Icon.jsx +2 -7
  28. package/src/icons/KnobIcon.jsx +2 -7
  29. package/src/icons/LibraryIcon.jsx +2 -7
  30. package/src/icons/LockIcon.jsx +2 -7
  31. package/src/icons/LoopIcon.jsx +2 -7
  32. package/src/icons/MoreIcon.jsx +2 -7
  33. package/src/icons/MusicIcon.jsx +1 -6
  34. package/src/icons/NoInternetSignalIcon.jsx +2 -7
  35. package/src/icons/NoKeysIcon.jsx +2 -7
  36. package/src/icons/NoMusicIcon.jsx +2 -7
  37. package/src/icons/NoPianoIcon.jsx +2 -7
  38. package/src/icons/NoSoundtrackIcon.jsx +2 -7
  39. package/src/icons/PianoIcon.jsx +1 -6
  40. package/src/icons/PlayBackSpeedIcon.jsx +2 -7
  41. package/src/icons/PlayCircleIcon.jsx +2 -7
  42. package/src/icons/RadioIcon.jsx +2 -7
  43. package/src/icons/RedoIcon.jsx +2 -7
  44. package/src/icons/RefreshBackIcon.jsx +2 -7
  45. package/src/icons/RocketIcon.jsx +2 -7
  46. package/src/icons/SearchIcon.jsx +2 -7
  47. package/src/icons/Share2Icon.jsx +30 -0
  48. package/src/icons/SoundtrackGradientIcon.jsx +2 -5
  49. package/src/icons/SparkleIcon.jsx +2 -7
  50. package/src/icons/SparklesGradientIcon.jsx +2 -5
  51. package/src/icons/SpatialAudioIcon.jsx +2 -7
  52. package/src/icons/SpeakerLoudIcon.jsx +2 -7
  53. package/src/icons/SpeedChangerIcon.jsx +2 -7
  54. package/src/icons/SpliterGradientIcon.jsx +2 -5
  55. package/src/icons/StringsIcon.jsx +1 -11
  56. package/src/icons/TargetIcon.jsx +2 -7
  57. package/src/icons/TrackPreviousIcon.jsx +18 -0
  58. package/src/icons/TrimIcon.jsx +2 -7
  59. package/src/icons/UndoIcon.jsx +2 -7
  60. package/src/icons/UnlockIcon.jsx +2 -7
  61. package/src/icons/UploadIcon.jsx +2 -7
  62. package/src/icons/UserIcon.jsx +2 -7
  63. package/src/icons/VocalsBackgroundIcon.jsx +2 -7
  64. package/src/icons/VocalsIcon.jsx +1 -6
  65. package/src/icons/ZoomCloseIcon.jsx +2 -7
  66. package/src/icons/ZoomInIcon.jsx +2 -7
  67. package/src/icons.jsx +2 -0
  68. package/src/index.jsx +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "3.11.8",
3
+ "version": "3.11.10",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,4 +1,5 @@
1
1
  import { memo, useCallback, useRef, useState } from 'react'
2
+ import { Tooltip } from '../Tooltip/Tooltip'
2
3
  import styles from './InputLevelMeter.module.css'
3
4
 
4
5
  const SEGMENT_MIN = 5
@@ -56,6 +57,13 @@ function faderPositionToGain(pos) {
56
57
  return 10 ** (db / 20)
57
58
  }
58
59
 
60
+ function formatGainDb(gain) {
61
+ if (gain <= 0) return '-∞ dB'
62
+ const db = 20 * Math.log10(gain)
63
+ if (db <= -96) return '-∞ dB'
64
+ return `${db >= 0 ? '+' : ''}${db.toFixed(1)} dB`
65
+ }
66
+
59
67
  function MeterRow({ level }) {
60
68
  const pct = getNormalizedLevel(level)
61
69
  return (
@@ -114,6 +122,7 @@ export const InputLevelMeter = memo(function InputLevelMeter({
114
122
  e.currentTarget.setPointerCapture(e.pointerId)
115
123
  isPressedRef.current = true
116
124
  setIsPressed(true)
125
+ document.body.style.cursor = 'ew-resize'
117
126
  const rect = meterRef.current?.getBoundingClientRect()
118
127
  if (rect) {
119
128
  const pos = Math.max(
@@ -144,14 +153,19 @@ export const InputLevelMeter = memo(function InputLevelMeter({
144
153
  const handlePointerUp = useCallback(() => {
145
154
  isPressedRef.current = false
146
155
  setIsPressed(false)
156
+ document.body.style.cursor = ''
147
157
  }, [])
148
158
 
149
- const meterClassName = [styles.bars, isPressed ? styles.pressed : '']
159
+ const hasHandler = onVolumeChange && showHandler
160
+
161
+ const meterClassName = [
162
+ styles.bars,
163
+ hasHandler ? styles.interactive : '',
164
+ isPressed ? styles.pressed : '',
165
+ ]
150
166
  .filter(Boolean)
151
167
  .join(' ')
152
168
 
153
- const hasHandler = onVolumeChange && showHandler
154
-
155
169
  return (
156
170
  <div
157
171
  className={styles.container}
@@ -167,10 +181,18 @@ export const InputLevelMeter = memo(function InputLevelMeter({
167
181
  <MeterRow level={rightLevel} />
168
182
 
169
183
  {hasHandler && (
170
- <div
171
- className={`${styles.thumb} ${isPressed ? styles.pressed : ''}`}
172
- style={{ left: `${faderPercent}%` }}
173
- />
184
+ <Tooltip
185
+ content={formatGainDb(volume)}
186
+ open={isPressed}
187
+ side="top"
188
+ sideOffset={8}
189
+ delayDuration={0}
190
+ >
191
+ <div
192
+ className={`${styles.thumb} ${isPressed ? styles.pressed : ''}`}
193
+ style={{ left: `${faderPercent}%` }}
194
+ />
195
+ </Tooltip>
174
196
  )}
175
197
 
176
198
  {showPeakHold && (
@@ -19,6 +19,10 @@
19
19
  transition: background-color 0.1s ease;
20
20
  }
21
21
 
22
+ .bars.interactive {
23
+ cursor: ew-resize;
24
+ }
25
+
22
26
  .bars.pressed {
23
27
  background: rgba(221, 234, 248, 0.08);
24
28
  }
@@ -1,3 +1,4 @@
1
+ import { useState, useEffect, useRef, useCallback } from 'react'
1
2
  import { InputLevelMeter } from './InputLevelMeter'
2
3
  import { useSimulatedInputLevel } from './useSimulatedInputLevel'
3
4
 
@@ -10,18 +11,304 @@ export default {
10
11
  },
11
12
  decorators: [
12
13
  (Story) => (
13
- <div style={{ padding: '16px', background: '#1d1d1d', width: '200px' }}>
14
+ <div style={{ padding: '16px', background: '#1d1d1d', width: '240px' }}>
14
15
  <Story />
15
16
  </div>
16
17
  ),
17
18
  ],
19
+ argTypes: {
20
+ linear: { control: false },
21
+ volume: {
22
+ control: { type: 'range', min: 0, max: 2, step: 0.01 },
23
+ },
24
+ showHandler: { control: 'boolean' },
25
+ showMeter: { control: 'boolean' },
26
+ onVolumeChange: { action: 'volumeChanged' },
27
+ },
18
28
  }
19
29
 
20
- function AnimatedMeter() {
30
+ function AnimatedMeter(props) {
21
31
  const linear = useSimulatedInputLevel()
22
- return <InputLevelMeter linear={linear} />
32
+ return <InputLevelMeter linear={linear} {...props} />
33
+ }
34
+
35
+ function InteractiveMeter({ initialVolume = 1, ...props }) {
36
+ const [volume, setVolume] = useState(initialVolume)
37
+ const linear = useSimulatedInputLevel()
38
+
39
+ const handleVolumeChange = useCallback(([v]) => {
40
+ setVolume(v)
41
+ }, [])
42
+
43
+ return (
44
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
45
+ <InputLevelMeter
46
+ linear={linear}
47
+ volume={volume}
48
+ onVolumeChange={handleVolumeChange}
49
+ {...props}
50
+ />
51
+ <span
52
+ style={{ color: 'rgba(255,255,255,0.5)', fontSize: '11px', fontFamily: 'monospace' }}
53
+ >
54
+ gain: {volume.toFixed(3)} | dB: {volume > 0 ? (20 * Math.log10(volume)).toFixed(1) : '-∞'}
55
+ </span>
56
+ </div>
57
+ )
58
+ }
59
+
60
+ function useFixedLevel(left, right) {
61
+ const [linear, setLinear] = useState([left, right])
62
+ useEffect(() => {
63
+ setLinear([left, right])
64
+ }, [left, right])
65
+ return linear
23
66
  }
24
67
 
25
68
  export const Default = {
26
69
  render: () => <AnimatedMeter />,
27
70
  }
71
+
72
+ export const WithVolumeControl = {
73
+ render: () => <InteractiveMeter />,
74
+ }
75
+
76
+ export const Silent = {
77
+ render: () => <InputLevelMeter linear={[0, 0]} />,
78
+ }
79
+
80
+ export const LowLevel = {
81
+ render: () => {
82
+ function LowLevelMeter() {
83
+ const linear = useFixedLevel(0.01, 0.008)
84
+ return <InputLevelMeter linear={linear} />
85
+ }
86
+ return <LowLevelMeter />
87
+ },
88
+ }
89
+
90
+ export const MediumLevel = {
91
+ render: () => {
92
+ function MediumLevelMeter() {
93
+ const linear = useFixedLevel(0.15, 0.12)
94
+ return <InputLevelMeter linear={linear} />
95
+ }
96
+ return <MediumLevelMeter />
97
+ },
98
+ }
99
+
100
+ export const HotLevel = {
101
+ render: () => {
102
+ function HotLevelMeter() {
103
+ const linear = useFixedLevel(0.7, 0.65)
104
+ return <InputLevelMeter linear={linear} />
105
+ }
106
+ return <HotLevelMeter />
107
+ },
108
+ }
109
+
110
+ export const Clipping = {
111
+ render: () => {
112
+ function ClippingMeter() {
113
+ const linear = useFixedLevel(1.0, 0.95)
114
+ return <InputLevelMeter linear={linear} />
115
+ }
116
+ return <ClippingMeter />
117
+ },
118
+ }
119
+
120
+ export const MonoLeft = {
121
+ render: () => {
122
+ function MonoLeftMeter() {
123
+ const linear = useFixedLevel(0.5, 0)
124
+ return <InputLevelMeter linear={linear} />
125
+ }
126
+ return <MonoLeftMeter />
127
+ },
128
+ }
129
+
130
+ export const MonoRight = {
131
+ render: () => {
132
+ function MonoRightMeter() {
133
+ const linear = useFixedLevel(0, 0.5)
134
+ return <InputLevelMeter linear={linear} />
135
+ }
136
+ return <MonoRightMeter />
137
+ },
138
+ }
139
+
140
+ export const AsymmetricLevels = {
141
+ render: () => {
142
+ function AsymmetricMeter() {
143
+ const linear = useFixedLevel(0.8, 0.2)
144
+ return <InputLevelMeter linear={linear} />
145
+ }
146
+ return <AsymmetricMeter />
147
+ },
148
+ }
149
+
150
+ export const WithoutHandler = {
151
+ render: () => <AnimatedMeter showHandler={false} />,
152
+ }
153
+
154
+ export const WithoutMeterDot = {
155
+ render: () => <AnimatedMeter showMeter={false} />,
156
+ }
157
+
158
+ export const MeterOnly = {
159
+ render: () => <AnimatedMeter showHandler={false} showMeter={false} />,
160
+ }
161
+
162
+ export const VolumeAtZero = {
163
+ render: () => <InteractiveMeter initialVolume={0} />,
164
+ }
165
+
166
+ export const VolumeAtUnity = {
167
+ render: () => <InteractiveMeter initialVolume={1} />,
168
+ }
169
+
170
+ export const VolumeBoosted = {
171
+ render: () => <InteractiveMeter initialVolume={1.5} />,
172
+ }
173
+
174
+ export const MaxBoost = {
175
+ render: () => <InteractiveMeter initialVolume={2} />,
176
+ }
177
+
178
+ export const PulsingSignal = {
179
+ render: () => {
180
+ function PulsingMeter() {
181
+ const [linear, setLinear] = useState([0, 0])
182
+ const frameRef = useRef(null)
183
+
184
+ useEffect(() => {
185
+ let t = 0
186
+ function animate() {
187
+ t += 0.04
188
+ const val = Math.abs(Math.sin(t)) * 0.9
189
+ setLinear([val, val * 0.85])
190
+ frameRef.current = requestAnimationFrame(animate)
191
+ }
192
+ frameRef.current = requestAnimationFrame(animate)
193
+ return () => cancelAnimationFrame(frameRef.current)
194
+ }, [])
195
+
196
+ return <InputLevelMeter linear={linear} />
197
+ }
198
+ return <PulsingMeter />
199
+ },
200
+ }
201
+
202
+ export const SpikeAndDecay = {
203
+ render: () => {
204
+ function SpikeDecayMeter() {
205
+ const [linear, setLinear] = useState([0, 0])
206
+ const frameRef = useRef(null)
207
+ const currentRef = useRef([0, 0])
208
+ const tickRef = useRef(0)
209
+
210
+ useEffect(() => {
211
+ function animate() {
212
+ tickRef.current++
213
+
214
+ if (tickRef.current % 90 === 0) {
215
+ currentRef.current = [0.95, 0.9]
216
+ } else {
217
+ currentRef.current = currentRef.current.map((v) =>
218
+ Math.max(0, v * 0.97),
219
+ )
220
+ }
221
+
222
+ setLinear([...currentRef.current])
223
+ frameRef.current = requestAnimationFrame(animate)
224
+ }
225
+ frameRef.current = requestAnimationFrame(animate)
226
+ return () => cancelAnimationFrame(frameRef.current)
227
+ }, [])
228
+
229
+ return <InputLevelMeter linear={linear} />
230
+ }
231
+ return <SpikeDecayMeter />
232
+ },
233
+ }
234
+
235
+ export const MultipleMeters = {
236
+ render: () => {
237
+ function MultiMeter() {
238
+ const linear = useSimulatedInputLevel()
239
+ const [volumes, setVolumes] = useState([1, 0.7, 0.4, 0.1])
240
+
241
+ const makeHandler = (idx) => ([v]) => {
242
+ setVolumes((prev) => {
243
+ const next = [...prev]
244
+ next[idx] = v
245
+ return next
246
+ })
247
+ }
248
+
249
+ const labels = ['Master', 'Vocals', 'Guitar', 'Drums']
250
+
251
+ return (
252
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
253
+ {volumes.map((vol, i) => (
254
+ <div key={i}>
255
+ <span
256
+ style={{
257
+ color: 'rgba(255,255,255,0.5)',
258
+ fontSize: '10px',
259
+ fontFamily: 'monospace',
260
+ marginBottom: '4px',
261
+ display: 'block',
262
+ }}
263
+ >
264
+ {labels[i]}
265
+ </span>
266
+ <InputLevelMeter
267
+ linear={linear.map((l) => l * vol)}
268
+ volume={vol}
269
+ onVolumeChange={makeHandler(i)}
270
+ />
271
+ </div>
272
+ ))}
273
+ </div>
274
+ )
275
+ }
276
+ return <MultiMeter />
277
+ },
278
+ decorators: [
279
+ (Story) => (
280
+ <div style={{ padding: '16px', background: '#1d1d1d', width: '240px' }}>
281
+ <Story />
282
+ </div>
283
+ ),
284
+ ],
285
+ }
286
+
287
+ export const Playground = {
288
+ args: {
289
+ volume: 1,
290
+ showHandler: true,
291
+ showMeter: true,
292
+ },
293
+ render: (args) => {
294
+ function PlaygroundMeter() {
295
+ const linear = useSimulatedInputLevel()
296
+ const [volume, setVolume] = useState(args.volume)
297
+
298
+ useEffect(() => {
299
+ setVolume(args.volume)
300
+ }, [args.volume])
301
+
302
+ return (
303
+ <InputLevelMeter
304
+ linear={linear}
305
+ volume={volume}
306
+ onVolumeChange={([v]) => setVolume(v)}
307
+ showHandler={args.showHandler}
308
+ showMeter={args.showMeter}
309
+ />
310
+ )
311
+ }
312
+ return <PlaygroundMeter />
313
+ },
314
+ }
@@ -0,0 +1,71 @@
1
+ import { memo, useRef } from 'react'
2
+ import styles from './PeakLevel.module.css'
3
+
4
+ const PEAK_HOLD_THRESHOLD_HIGH = 0
5
+ const PEAK_HOLD_THRESHOLD_MEDIUM = -1
6
+
7
+ function linearToPeakDb(linear) {
8
+ if (!Array.isArray(linear)) return null
9
+ const peak = Math.max(linear[0] ?? 0, linear[1] ?? 0)
10
+ if (peak <= 0) return null
11
+ const db = 20 * Math.log10(peak)
12
+ if (db <= -96) return null
13
+ return db
14
+ }
15
+
16
+ function getLevel(db) {
17
+ if (db == null) return 'reset'
18
+ if (db >= PEAK_HOLD_THRESHOLD_HIGH) return 'high'
19
+ if (db >= PEAK_HOLD_THRESHOLD_MEDIUM) return 'medium'
20
+ return 'regular'
21
+ }
22
+
23
+ function formatPeakDb(db) {
24
+ if (db == null) return '--'
25
+ return db.toFixed(1)
26
+ }
27
+
28
+ export const PeakLevel = memo(function PeakLevel({
29
+ linear,
30
+ value,
31
+ onClick,
32
+ }) {
33
+ const peakHoldRef = useRef(null)
34
+
35
+ let displayDb = value ?? null
36
+
37
+ if (linear !== undefined) {
38
+ const currentDb = linearToPeakDb(linear)
39
+
40
+ if (currentDb != null) {
41
+ if (peakHoldRef.current == null || currentDb > peakHoldRef.current) {
42
+ peakHoldRef.current = currentDb
43
+ }
44
+ }
45
+
46
+ if (linear[0] <= 0 && linear[1] <= 0) {
47
+ peakHoldRef.current = null
48
+ }
49
+
50
+ displayDb = peakHoldRef.current
51
+ }
52
+
53
+ const level = getLevel(displayDb)
54
+ const text = formatPeakDb(displayDb)
55
+
56
+ return (
57
+ <div
58
+ className={styles.container}
59
+ onClick={onClick}
60
+ role={onClick ? 'button' : undefined}
61
+ tabIndex={onClick ? 0 : undefined}
62
+ style={onClick ? { cursor: 'pointer' } : undefined}
63
+ >
64
+ <span className={`${styles.value} ${styles[level]}`}>
65
+ {text}
66
+ </span>
67
+ </div>
68
+ )
69
+ })
70
+
71
+ PeakLevel.displayName = 'PeakLevel'
@@ -0,0 +1,49 @@
1
+ .container {
2
+ display: flex;
3
+ align-items: center;
4
+ justify-content: center;
5
+ width: 24px;
6
+ padding: 2px 0;
7
+ border-radius: 4px;
8
+ user-select: none;
9
+ }
10
+
11
+ .value {
12
+ font-size: 12px;
13
+ font-weight: 500;
14
+ line-height: 16px;
15
+ letter-spacing: 0.04px;
16
+ white-space: nowrap;
17
+ }
18
+
19
+ .reset {
20
+ color: rgba(214, 235, 253, 0.19);
21
+ }
22
+
23
+ .regular {
24
+ color: #00dae8;
25
+ }
26
+
27
+ .medium {
28
+ color: #ffc53d;
29
+ }
30
+
31
+ .high {
32
+ color: #ec5d5e;
33
+ }
34
+
35
+ .container:hover .reset {
36
+ color: rgba(214, 235, 253, 0.19);
37
+ }
38
+
39
+ .container:hover .regular {
40
+ color: #75ebf5;
41
+ }
42
+
43
+ .container:hover .medium {
44
+ color: #ffe7b3;
45
+ }
46
+
47
+ .container:hover .high {
48
+ color: #ff9592;
49
+ }