@moises.ai/design-system 3.11.9 → 3.11.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "3.11.9",
3
+ "version": "3.11.11",
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,4 @@
1
- import { memo, useCallback, useRef, useState } from 'react'
1
+ import { memo, useCallback, useEffect, useRef, useState } from 'react'
2
2
  import { Tooltip } from '../Tooltip/Tooltip'
3
3
  import styles from './InputLevelMeter.module.css'
4
4
 
@@ -86,6 +86,8 @@ export const InputLevelMeter = memo(function InputLevelMeter({
86
86
  onVolumeChange,
87
87
  showHandler = true,
88
88
  showMeter = true,
89
+ hover = false,
90
+ resetPeakHold,
89
91
  }) {
90
92
  const [linearL, linearR] = linear
91
93
  const leftLevel = clampLevel(linearToMeterLevel(linearL ?? 0))
@@ -95,6 +97,18 @@ export const InputLevelMeter = memo(function InputLevelMeter({
95
97
  const isPressedRef = useRef(false)
96
98
  const meterRef = useRef(null)
97
99
  const peakHoldRef = useRef(0)
100
+ const peakTimerRef = useRef(null)
101
+ const [peakHoldPct, setPeakHoldPct] = useState(0)
102
+ const [peakRising, setPeakRising] = useState(false)
103
+
104
+ useEffect(() => {
105
+ peakHoldRef.current = 0
106
+ setPeakHoldPct(0)
107
+ }, [resetPeakHold])
108
+
109
+ useEffect(() => {
110
+ return () => clearTimeout(peakTimerRef.current)
111
+ }, [])
98
112
 
99
113
  const faderPercent = Math.max(
100
114
  0,
@@ -105,14 +119,28 @@ export const InputLevelMeter = memo(function InputLevelMeter({
105
119
  const rightNormalized = getNormalizedLevel(rightLevel)
106
120
  const currentPeakPct = Math.max(leftNormalized, rightNormalized)
107
121
 
108
- if (currentPeakPct > peakHoldRef.current) {
109
- peakHoldRef.current = currentPeakPct
110
- }
111
- if (currentPeakPct <= 0) {
112
- peakHoldRef.current = 0
113
- }
122
+ useEffect(() => {
123
+ if (currentPeakPct > peakHoldRef.current) {
124
+ peakHoldRef.current = currentPeakPct
125
+ setPeakHoldPct(currentPeakPct)
126
+ setPeakRising(true)
127
+ clearTimeout(peakTimerRef.current)
128
+ peakTimerRef.current = setTimeout(() => {
129
+ setPeakRising(false)
130
+ requestAnimationFrame(() => {
131
+ requestAnimationFrame(() => {
132
+ peakHoldRef.current = 0
133
+ setPeakHoldPct(0)
134
+ })
135
+ })
136
+ }, 400)
137
+ }
138
+ if (currentPeakPct <= 0) {
139
+ peakHoldRef.current = 0
140
+ setPeakHoldPct(0)
141
+ }
142
+ }, [currentPeakPct])
114
143
 
115
- const peakHoldPct = peakHoldRef.current
116
144
  const peakHoldColor = getPeakHoldColor(peakHoldPct)
117
145
  const showPeakHold = peakHoldPct > 0
118
146
 
@@ -189,30 +217,28 @@ export const InputLevelMeter = memo(function InputLevelMeter({
189
217
  delayDuration={0}
190
218
  >
191
219
  <div
192
- className={`${styles.thumb} ${isPressed ? styles.pressed : ''}`}
220
+ className={`${styles.thumb} ${isPressed ? styles.pressed : hover ? styles.hovered : ''}`}
193
221
  style={{ left: `${faderPercent}%` }}
194
222
  />
195
223
  </Tooltip>
196
224
  )}
197
225
 
198
- {showPeakHold && (
199
- <>
200
- <div
201
- className={styles.peakDotTop}
202
- style={{
203
- left: `min(calc(${peakHoldPct}% + 3px), calc(100% - 3px))`,
204
- background: peakHoldColor,
205
- }}
206
- />
207
- <div
208
- className={styles.peakDotBottom}
209
- style={{
210
- left: `min(calc(${peakHoldPct}% + 3px), calc(100% - 3px))`,
211
- background: peakHoldColor,
212
- }}
213
- />
214
- </>
215
- )}
226
+ <div
227
+ className={`${styles.peakDotTop} ${peakRising ? styles.peakRising : ''}`}
228
+ style={{
229
+ left: `min(calc(${peakHoldPct}% + 3px), calc(100% - 3px))`,
230
+ background: peakHoldColor,
231
+ opacity: showPeakHold ? 1 : 0,
232
+ }}
233
+ />
234
+ <div
235
+ className={`${styles.peakDotBottom} ${peakRising ? styles.peakRising : ''}`}
236
+ style={{
237
+ left: `min(calc(${peakHoldPct}% + 3px), calc(100% - 3px))`,
238
+ background: peakHoldColor,
239
+ opacity: showPeakHold ? 1 : 0,
240
+ }}
241
+ />
216
242
  </div>
217
243
 
218
244
  {showMeter && (
@@ -15,7 +15,7 @@
15
15
  padding: 2px;
16
16
  border-radius: 3px;
17
17
  width: 100%;
18
- background: rgba(216, 244, 246, 0.04);
18
+ background: rgba(211, 237, 248, 0.11);
19
19
  transition: background-color 0.1s ease;
20
20
  }
21
21
 
@@ -23,10 +23,6 @@
23
23
  cursor: ew-resize;
24
24
  }
25
25
 
26
- .bars.pressed {
27
- background: rgba(221, 234, 248, 0.08);
28
- }
29
-
30
26
  .barRow {
31
27
  display: inline-grid;
32
28
  grid-template-rows: max-content;
@@ -66,12 +62,18 @@
66
62
  width: 8px;
67
63
  height: 19px;
68
64
  border-radius: 9999px;
69
- background: #edeef0;
65
+ background: #777b84;
66
+ border: 1px solid rgba(46, 49, 53, 0.3);
70
67
  cursor: ew-resize;
71
68
  z-index: 2;
72
69
  touch-action: none;
73
70
  }
74
71
 
72
+ .thumb.hovered,
73
+ .container:hover .thumb {
74
+ background: white;
75
+ }
76
+
75
77
  .thumb.pressed {
76
78
  background: white;
77
79
  }
@@ -83,6 +85,11 @@
83
85
  border-radius: 9999px;
84
86
  z-index: 1;
85
87
  pointer-events: none;
88
+ transition: left 0.5s ease-out, opacity 0.5s ease-out;
89
+ }
90
+
91
+ .peakDot.peakRising {
92
+ transition: opacity 0.3s ease-out;
86
93
  }
87
94
 
88
95
  .peakDotTop {
@@ -23,6 +23,7 @@ export default {
23
23
  },
24
24
  showHandler: { control: 'boolean' },
25
25
  showMeter: { control: 'boolean' },
26
+ hover: { control: 'boolean' },
26
27
  onVolumeChange: { action: 'volumeChanged' },
27
28
  },
28
29
  }
@@ -147,6 +148,25 @@ export const AsymmetricLevels = {
147
148
  },
148
149
  }
149
150
 
151
+ export const HoverState = {
152
+ render: () => (
153
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
154
+ <div>
155
+ <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '10px', fontFamily: 'monospace', display: 'block', marginBottom: '4px' }}>
156
+ Default
157
+ </span>
158
+ <AnimatedMeter />
159
+ </div>
160
+ <div>
161
+ <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '10px', fontFamily: 'monospace', display: 'block', marginBottom: '4px' }}>
162
+ hover=true (parent hovered)
163
+ </span>
164
+ <AnimatedMeter hover />
165
+ </div>
166
+ </div>
167
+ ),
168
+ }
169
+
150
170
  export const WithoutHandler = {
151
171
  render: () => <AnimatedMeter showHandler={false} />,
152
172
  }
@@ -289,6 +309,7 @@ export const Playground = {
289
309
  volume: 1,
290
310
  showHandler: true,
291
311
  showMeter: true,
312
+ hover: false,
292
313
  },
293
314
  render: (args) => {
294
315
  function PlaygroundMeter() {
@@ -306,6 +327,7 @@ export const Playground = {
306
327
  onVolumeChange={([v]) => setVolume(v)}
307
328
  showHandler={args.showHandler}
308
329
  showMeter={args.showMeter}
330
+ hover={args.hover}
309
331
  />
310
332
  )
311
333
  }
@@ -0,0 +1,73 @@
1
+ import { memo, useRef } from 'react'
2
+ import styles from './PeakLevel.module.css'
3
+
4
+ const THRESHOLD_CLIP = 0
5
+ const THRESHOLD_RED = -6
6
+ const THRESHOLD_YELLOW = -12
7
+
8
+ function linearToPeakDb(linear) {
9
+ if (!Array.isArray(linear)) return null
10
+ const peak = Math.max(linear[0] ?? 0, linear[1] ?? 0)
11
+ if (peak <= 0) return null
12
+ const db = 20 * Math.log10(peak)
13
+ if (db <= -96) return null
14
+ return db
15
+ }
16
+
17
+ function getLevel(db) {
18
+ if (db == null) return 'reset'
19
+ if (db >= THRESHOLD_CLIP) return 'clip'
20
+ if (db >= THRESHOLD_RED) return 'high'
21
+ if (db >= THRESHOLD_YELLOW) return 'medium'
22
+ return 'regular'
23
+ }
24
+
25
+ function formatPeakDb(db) {
26
+ if (db == null) return '--'
27
+ return db.toFixed(1)
28
+ }
29
+
30
+ export const PeakLevel = memo(function PeakLevel({
31
+ linear,
32
+ value,
33
+ onClick,
34
+ }) {
35
+ const peakHoldRef = useRef(null)
36
+
37
+ let displayDb = value ?? null
38
+
39
+ if (linear !== undefined) {
40
+ const currentDb = linearToPeakDb(linear)
41
+
42
+ if (currentDb != null) {
43
+ if (peakHoldRef.current == null || currentDb > peakHoldRef.current) {
44
+ peakHoldRef.current = currentDb
45
+ }
46
+ }
47
+
48
+ if (linear[0] <= 0 && linear[1] <= 0) {
49
+ peakHoldRef.current = null
50
+ }
51
+
52
+ displayDb = peakHoldRef.current
53
+ }
54
+
55
+ const level = getLevel(displayDb)
56
+ const text = formatPeakDb(displayDb)
57
+
58
+ return (
59
+ <div
60
+ className={styles.container}
61
+ onClick={onClick}
62
+ role={onClick ? 'button' : undefined}
63
+ tabIndex={onClick ? 0 : undefined}
64
+ style={onClick ? { cursor: 'pointer' } : undefined}
65
+ >
66
+ <span className={`${styles.value} ${styles[level]}`}>
67
+ {text}
68
+ </span>
69
+ </div>
70
+ )
71
+ })
72
+
73
+ PeakLevel.displayName = 'PeakLevel'
@@ -0,0 +1,63 @@
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
+ .clip {
36
+ color: #ec5d5e;
37
+ animation: blink 0.6s steps(1) infinite;
38
+ }
39
+
40
+ @keyframes blink {
41
+ 0%, 100% { opacity: 1; }
42
+ 50% { opacity: 0.4; }
43
+ }
44
+
45
+ .container:hover .reset {
46
+ color: rgba(214, 235, 253, 0.19);
47
+ }
48
+
49
+ .container:hover .regular {
50
+ color: #75ebf5;
51
+ }
52
+
53
+ .container:hover .medium {
54
+ color: #ffe7b3;
55
+ }
56
+
57
+ .container:hover .high {
58
+ color: #ff9592;
59
+ }
60
+
61
+ .container:hover .clip {
62
+ color: #ff9592;
63
+ }
@@ -0,0 +1,202 @@
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { PeakLevel } from './PeakLevel'
3
+ import { InputLevelMeter } from '../InputLevelMeter/InputLevelMeter'
4
+ import { useSimulatedInputLevel } from '../InputLevelMeter/useSimulatedInputLevel'
5
+
6
+ export default {
7
+ title: 'Components/PeakLevel',
8
+ component: PeakLevel,
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ layout: 'centered',
12
+ },
13
+ decorators: [
14
+ (Story) => (
15
+ <div style={{ padding: '16px', background: '#1d1d1d' }}>
16
+ <Story />
17
+ </div>
18
+ ),
19
+ ],
20
+ }
21
+
22
+ export const Reset = {
23
+ render: () => <PeakLevel value={null} />,
24
+ }
25
+
26
+ export const GreenLevel = {
27
+ render: () => <PeakLevel value={-18} />,
28
+ }
29
+
30
+ export const YellowLevel = {
31
+ render: () => <PeakLevel value={-9} />,
32
+ }
33
+
34
+ export const RedLevel = {
35
+ render: () => <PeakLevel value={-3} />,
36
+ }
37
+
38
+ export const ClipLevel = {
39
+ render: () => <PeakLevel value={1.3} />,
40
+ }
41
+
42
+ export const AllLevels = {
43
+ render: () => (
44
+ <div style={{ display: 'flex', gap: '16px', alignItems: 'center' }}>
45
+ <div style={{ textAlign: 'center' }}>
46
+ <PeakLevel value={null} />
47
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '9px', display: 'block', marginTop: '4px' }}>
48
+ Reset
49
+ </span>
50
+ </div>
51
+ <div style={{ textAlign: 'center' }}>
52
+ <PeakLevel value={-18} />
53
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '9px', display: 'block', marginTop: '4px' }}>
54
+ Green (&lt;-12)
55
+ </span>
56
+ </div>
57
+ <div style={{ textAlign: 'center' }}>
58
+ <PeakLevel value={-9} />
59
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '9px', display: 'block', marginTop: '4px' }}>
60
+ Yellow (-12 to -6)
61
+ </span>
62
+ </div>
63
+ <div style={{ textAlign: 'center' }}>
64
+ <PeakLevel value={-3} />
65
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '9px', display: 'block', marginTop: '4px' }}>
66
+ Red (&gt;-6)
67
+ </span>
68
+ </div>
69
+ <div style={{ textAlign: 'center' }}>
70
+ <PeakLevel value={1.3} />
71
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '9px', display: 'block', marginTop: '4px' }}>
72
+ Clip (0+)
73
+ </span>
74
+ </div>
75
+ </div>
76
+ ),
77
+ }
78
+
79
+ export const WithLinearInput = {
80
+ render: () => {
81
+ function LivePeak() {
82
+ const linear = useSimulatedInputLevel()
83
+ return (
84
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
85
+ <div style={{ width: '200px' }}>
86
+ <InputLevelMeter linear={linear} />
87
+ </div>
88
+ <PeakLevel linear={linear} />
89
+ </div>
90
+ )
91
+ }
92
+ return <LivePeak />
93
+ },
94
+ }
95
+
96
+ export const WithClickToReset = {
97
+ render: () => {
98
+ function ResettablePeak() {
99
+ const linear = useSimulatedInputLevel()
100
+ const [peakDb, setPeakDb] = useState(null)
101
+ const peakRef = useRef(null)
102
+
103
+ useEffect(() => {
104
+ const peak = Math.max(linear[0] ?? 0, linear[1] ?? 0)
105
+ if (peak > 0) {
106
+ const db = 20 * Math.log10(peak)
107
+ if (db > -96) {
108
+ if (peakRef.current == null || db > peakRef.current) {
109
+ peakRef.current = db
110
+ setPeakDb(db)
111
+ }
112
+ }
113
+ }
114
+ }, [linear])
115
+
116
+ const handleReset = () => {
117
+ peakRef.current = null
118
+ setPeakDb(null)
119
+ }
120
+
121
+ return (
122
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
123
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
124
+ <div style={{ width: '200px' }}>
125
+ <InputLevelMeter linear={linear} />
126
+ </div>
127
+ <PeakLevel value={peakDb} onClick={handleReset} />
128
+ </div>
129
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '10px', fontFamily: 'monospace' }}>
130
+ Click the peak value to reset
131
+ </span>
132
+ </div>
133
+ )
134
+ }
135
+ return <ResettablePeak />
136
+ },
137
+ }
138
+
139
+ export const WithVolumeFader = {
140
+ render: () => {
141
+ function FaderWithPeak() {
142
+ const linear = useSimulatedInputLevel()
143
+ const [volume, setVolume] = useState(1)
144
+
145
+ return (
146
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
147
+ <div style={{ width: '200px' }}>
148
+ <InputLevelMeter
149
+ linear={linear}
150
+ volume={volume}
151
+ onVolumeChange={([v]) => setVolume(v)}
152
+ />
153
+ </div>
154
+ <PeakLevel linear={linear} />
155
+ </div>
156
+ )
157
+ }
158
+ return <FaderWithPeak />
159
+ },
160
+ }
161
+
162
+ export const BoundaryValues = {
163
+ render: () => (
164
+ <div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
165
+ {[-96, -60, -24, -6, -1, -0.1, 0, 0.1, 1, 3, 6].map((db) => (
166
+ <div key={db} style={{ textAlign: 'center' }}>
167
+ <PeakLevel value={db} />
168
+ <span style={{ color: 'rgba(255,255,255,0.3)', fontSize: '9px', fontFamily: 'monospace', display: 'block', marginTop: '4px' }}>
169
+ {db} dB
170
+ </span>
171
+ </div>
172
+ ))}
173
+ </div>
174
+ ),
175
+ }
176
+
177
+ export const MultipleChannels = {
178
+ render: () => {
179
+ function MultiChannel() {
180
+ const linear = useSimulatedInputLevel()
181
+ const labels = ['Master', 'Vocals', 'Guitar', 'Drums']
182
+ const multipliers = [1, 0.7, 0.4, 0.15]
183
+
184
+ return (
185
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
186
+ {labels.map((label, i) => (
187
+ <div key={label} style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
188
+ <span style={{ color: 'rgba(255,255,255,0.4)', fontSize: '10px', fontFamily: 'monospace', width: '48px' }}>
189
+ {label}
190
+ </span>
191
+ <div style={{ width: '160px' }}>
192
+ <InputLevelMeter linear={linear.map((l) => l * multipliers[i])} />
193
+ </div>
194
+ <PeakLevel linear={linear.map((l) => l * multipliers[i])} />
195
+ </div>
196
+ ))}
197
+ </div>
198
+ )
199
+ }
200
+ return <MultiChannel />
201
+ },
202
+ }
@@ -0,0 +1,18 @@
1
+ export function TrackPreviousIcon({ width = 32, height = 32 }) {
2
+ return (
3
+ <svg
4
+ width={width}
5
+ height={height}
6
+ viewBox="0 0 32 32"
7
+ fill="none"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ >
10
+ <path
11
+ d="M21.1797 9.6884C21.4546 9.86496 21.625 10.1751 21.625 10.5188L21.625 21.4812C21.625 21.8248 21.4546 22.135 21.1797 22.3116C20.9021 22.4898 20.5259 22.5208 20.2018 22.3202L11.8333 17.4237L11.8333 20.7917C11.8333 21.367 11.367 21.8333 10.7917 21.8333C10.2164 21.8333 9.75 21.367 9.75 20.7917V11.2083C9.75 10.633 10.2164 10.1667 10.7917 10.1667C11.367 10.1667 11.8333 10.633 11.8333 11.2083L11.8333 14.5762L20.2018 9.67975C20.5259 9.47916 20.9021 9.51013 21.1797 9.6884Z"
12
+ fill="currentColor"
13
+ />
14
+ </svg>
15
+ )
16
+ }
17
+
18
+ TrackPreviousIcon.displayName = 'TrackPreviousIcon'
package/src/icons.jsx CHANGED
@@ -350,3 +350,4 @@ export { Play1Icon } from './icons/Play1Icon'
350
350
  export { ChevronRightFilledIcon } from './icons/ChevronRightFilledIcon'
351
351
  export { ChevronDownFilledIcon } from './icons/ChevronDownFilledIcon'
352
352
  export { Share2Icon } from './icons/Share2Icon'
353
+ export { TrackPreviousIcon } from './icons/TrackPreviousIcon'
package/src/index.jsx CHANGED
@@ -111,6 +111,7 @@ export { MoreButton } from './components/MoreButton/MoreButton'
111
111
  export { MultiSelect } from './components/MultiSelect/MultiSelect'
112
112
  export { MultiSelectCards } from './components/MultiSelectCards/MultiSelectCards'
113
113
  export { PanControl } from './components/PanControl/PanControl'
114
+ export { PeakLevel } from './components/PeakLevel/PeakLevel'
114
115
  export { ProductsBrandPattern } from './components/ProductsBrandPattern/ProductsBrandPattern'
115
116
  export { ProductsList } from './components/ProductsList/ProductsList'
116
117
  export { ProfileMenu } from './components/ProfileMenu/ProfileMenu'