@moises.ai/design-system 4.15.1 → 4.15.3

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": "4.15.1",
3
+ "version": "4.15.3",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -0,0 +1,353 @@
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ forwardRef,
7
+ useImperativeHandle,
8
+ } from 'react'
9
+ import WavesurferPlayer from '@wavesurfer/react'
10
+ import classNames from 'classnames'
11
+ import { PlayIcon, PauseIcon } from '../../icons'
12
+ import styles from './PreviewCard.module.css'
13
+
14
+ export const PreviewCard = forwardRef(function PreviewCard(
15
+ {
16
+ audio,
17
+ selected = false,
18
+ loading = false,
19
+ wavegroup,
20
+ onSelect,
21
+ onPlayStateChange,
22
+ actions,
23
+ autoRepeat = false,
24
+ waveColor = 'rgba(90, 97, 105, 1)',
25
+ progressColor = 'rgba(255, 255, 255, 1)',
26
+ className,
27
+ id,
28
+ tabIndex: tabIndexProp,
29
+ },
30
+ ref,
31
+ ) {
32
+ const [isPlaying, setIsPlaying] = useState(false)
33
+ const [isReady, setIsReady] = useState(false)
34
+ const [isSafari, setIsSafari] = useState(false)
35
+
36
+ const waveSurferRef = useRef(null)
37
+ const skipTimeSyncRef = useRef(false)
38
+ const pendingSeekTimeRef = useRef(null)
39
+
40
+ const showSkeleton = loading || !isReady
41
+
42
+ useEffect(() => {
43
+ if (typeof navigator !== 'undefined') {
44
+ setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent))
45
+ }
46
+ }, [])
47
+
48
+ const pause = useCallback(() => {
49
+ waveSurferRef.current?.pause()
50
+ }, [])
51
+
52
+ const play = useCallback(() => {
53
+ if (!waveSurferRef.current) return
54
+
55
+ if (
56
+ wavegroup?.state?.active &&
57
+ wavegroup.state.active !== waveSurferRef.current
58
+ ) {
59
+ wavegroup.state.active.pause()
60
+ }
61
+
62
+ if (!isSafari) {
63
+ waveSurferRef.current.setOptions({ progressColor })
64
+ }
65
+ waveSurferRef.current.play()
66
+ }, [isSafari, progressColor, wavegroup])
67
+
68
+ const startPlay = useCallback(() => {
69
+ if (showSkeleton || !waveSurferRef.current) return
70
+
71
+ if (
72
+ wavegroup?.state?.active &&
73
+ wavegroup.state.active !== waveSurferRef.current
74
+ ) {
75
+ wavegroup.state.active.pause()
76
+
77
+ wavegroup.setter((prev) => ({
78
+ ...prev,
79
+ isPlaying: false,
80
+ active: null,
81
+ }))
82
+ }
83
+
84
+ if (
85
+ wavegroup?.state?.active &&
86
+ wavegroup.state.active !== waveSurferRef.current &&
87
+ wavegroup.state.active.getCurrentTime
88
+ ) {
89
+ waveSurferRef.current.setTime(wavegroup.state.active.getCurrentTime())
90
+ }
91
+
92
+ play()
93
+ }, [showSkeleton, play, wavegroup])
94
+
95
+ useImperativeHandle(
96
+ ref,
97
+ () => ({
98
+ play: startPlay,
99
+ pause,
100
+ isReady: () => isReady,
101
+ isPlaying: () => Boolean(waveSurferRef.current?.isPlaying()),
102
+ }),
103
+ [startPlay, pause, isReady],
104
+ )
105
+
106
+ useEffect(() => {
107
+ if (waveSurferRef.current && audio) {
108
+ waveSurferRef.current.load(audio)
109
+ setIsReady(false)
110
+ }
111
+ }, [audio])
112
+
113
+ const onReady = useCallback((ws) => {
114
+ waveSurferRef.current = ws
115
+ setIsReady(true)
116
+ setIsPlaying(false)
117
+ }, [])
118
+
119
+ const onPlay = useCallback(
120
+ (ws) => {
121
+ const isSeekHandoff = skipTimeSyncRef.current
122
+
123
+ setIsPlaying(true)
124
+ onPlayStateChange?.(true)
125
+ if (!isSeekHandoff) {
126
+ onSelect?.()
127
+ }
128
+
129
+ if (wavegroup) {
130
+ const previousActive = wavegroup.state.active
131
+
132
+ if (previousActive && previousActive !== ws) {
133
+ const currentTime = previousActive.getCurrentTime()
134
+ previousActive.pause()
135
+
136
+ if (!skipTimeSyncRef.current) {
137
+ ws.setTime(currentTime)
138
+ } else if (pendingSeekTimeRef.current != null) {
139
+ ws.setTime(pendingSeekTimeRef.current)
140
+ pendingSeekTimeRef.current = null
141
+ }
142
+ }
143
+
144
+ skipTimeSyncRef.current = false
145
+
146
+ wavegroup.setter((prev) => ({
147
+ ...prev,
148
+ isPlaying: true,
149
+ lastPlayed: ws,
150
+ active: ws,
151
+ }))
152
+ }
153
+ },
154
+ [onPlayStateChange, onSelect, wavegroup],
155
+ )
156
+
157
+ const onPause = useCallback(() => {
158
+ setIsPlaying(false)
159
+ onPlayStateChange?.(false)
160
+
161
+ if (wavegroup) {
162
+ if (wavegroup.state.active !== waveSurferRef.current) {
163
+ return
164
+ }
165
+
166
+ wavegroup.setter((prev) => ({
167
+ ...prev,
168
+ isPlaying: false,
169
+ }))
170
+ }
171
+ }, [onPlayStateChange, wavegroup])
172
+
173
+ const onSeeking = useCallback(
174
+ (ws) => {
175
+ if (!wavegroup) return
176
+
177
+ wavegroup.setter((prev) => ({
178
+ ...prev,
179
+ active: ws,
180
+ }))
181
+ },
182
+ [wavegroup],
183
+ )
184
+
185
+ const handleInteraction = useCallback(
186
+ (ws, newTime) => {
187
+ if (!wavegroup) return
188
+
189
+ const previousActive = wavegroup.state.active
190
+ const isHandoff = previousActive && previousActive !== ws
191
+
192
+ if (!isHandoff) {
193
+ wavegroup.setter((prev) => ({
194
+ ...prev,
195
+ active: ws,
196
+ lastPlayed: ws,
197
+ }))
198
+ return
199
+ }
200
+
201
+ const seekTime =
202
+ typeof newTime === 'number' ? newTime : (ws.getCurrentTime?.() ?? 0)
203
+ const wasPlaying =
204
+ Boolean(previousActive?.isPlaying?.()) || wavegroup.state.isPlaying
205
+
206
+ pendingSeekTimeRef.current = seekTime
207
+ skipTimeSyncRef.current = true
208
+
209
+ wavegroup.state.active = ws
210
+ wavegroup.state.lastPlayed = ws
211
+ wavegroup.state.isPlaying = wasPlaying
212
+
213
+ ws.setTime(seekTime)
214
+ previousActive.pause()
215
+
216
+ if (!isSafari) {
217
+ ws.setOptions({ progressColor })
218
+ }
219
+
220
+ if (wasPlaying) {
221
+ ws.play()
222
+ }
223
+
224
+ wavegroup.setter((prev) => ({
225
+ ...prev,
226
+ active: ws,
227
+ lastPlayed: ws,
228
+ isPlaying: wasPlaying,
229
+ }))
230
+ },
231
+ [wavegroup, isSafari, progressColor],
232
+ )
233
+
234
+ const onFinish = useCallback(() => {
235
+ if (autoRepeat) {
236
+ waveSurferRef.current?.play()
237
+ } else {
238
+ setIsPlaying(false)
239
+ onPlayStateChange?.(false)
240
+
241
+ if (wavegroup) {
242
+ wavegroup.setter((prev) => ({
243
+ ...prev,
244
+ isPlaying: false,
245
+ active: null,
246
+ }))
247
+ }
248
+ }
249
+ }, [autoRepeat, onPlayStateChange, wavegroup])
250
+
251
+ const handleTogglePlay = useCallback(() => {
252
+ if (showSkeleton || !waveSurferRef.current) return
253
+
254
+ if (waveSurferRef.current.isPlaying()) {
255
+ pause()
256
+ return
257
+ }
258
+
259
+ startPlay()
260
+ }, [showSkeleton, pause, startPlay])
261
+
262
+ const handleCardClick = useCallback(() => {
263
+ if (showSkeleton) return
264
+ onSelect?.()
265
+ }, [showSkeleton, onSelect])
266
+
267
+ return (
268
+ <div
269
+ id={id}
270
+ className={classNames(
271
+ styles.previewCard,
272
+ selected && styles.selected,
273
+ isPlaying && styles.playing,
274
+ showSkeleton && styles.skeleton,
275
+ className,
276
+ )}
277
+ onClick={handleCardClick}
278
+ role="option"
279
+ aria-selected={selected}
280
+ tabIndex={showSkeleton ? -1 : (tabIndexProp ?? 0)}
281
+ onKeyDown={(e) => {
282
+ if (e.key === 'Enter' || e.key === ' ') {
283
+ e.preventDefault()
284
+ handleCardClick()
285
+ }
286
+ }}
287
+ >
288
+ <button
289
+ type="button"
290
+ className={classNames(styles.leftButton, isPlaying && styles.isPlaying)}
291
+ onClick={(e) => {
292
+ e.stopPropagation()
293
+ handleTogglePlay()
294
+ }}
295
+ disabled={showSkeleton}
296
+ aria-label={isPlaying ? 'Pause' : 'Play'}
297
+ >
298
+ <span className={styles.equalizer} aria-hidden="true">
299
+ <span className={styles.equalizerBar} />
300
+ <span className={styles.equalizerBar} />
301
+ <span className={styles.equalizerBar} />
302
+ <span className={styles.equalizerBar} />
303
+ <span className={styles.equalizerBar} />
304
+ </span>
305
+ <PauseIcon
306
+ width={18}
307
+ height={18}
308
+ className={styles.pauseOnHover}
309
+ aria-hidden="true"
310
+ />
311
+ <PlayIcon
312
+ width={18}
313
+ height={18}
314
+ className={styles.playIcon}
315
+ aria-hidden="true"
316
+ />
317
+ </button>
318
+
319
+ <div
320
+ className={styles.waveformContainer}
321
+ onClick={(e) => e.stopPropagation()}
322
+ >
323
+ {audio && (
324
+ <WavesurferPlayer
325
+ url={audio}
326
+ waveColor={waveColor}
327
+ progressColor={progressColor}
328
+ normalize
329
+ barWidth={2}
330
+ barHeight={6}
331
+ cursorWidth={0}
332
+ height={40}
333
+ onReady={onReady}
334
+ onInteraction={handleInteraction}
335
+ onSeeking={onSeeking}
336
+ onPlay={onPlay}
337
+ onPause={onPause}
338
+ onFinish={onFinish}
339
+ backend={isSafari ? 'WebAudio' : 'MediaElement'}
340
+ />
341
+ )}
342
+ </div>
343
+
344
+ {actions && (
345
+ <div className={styles.actions} onClick={(e) => e.stopPropagation()}>
346
+ {actions}
347
+ </div>
348
+ )}
349
+ </div>
350
+ )
351
+ })
352
+
353
+ PreviewCard.displayName = 'PreviewCard'
@@ -0,0 +1,263 @@
1
+ @keyframes skeletonShimmer {
2
+ 0% {
3
+ background-position: 200% 50%;
4
+ }
5
+ 99.999% {
6
+ background-position: -200% 50%;
7
+ }
8
+ 100% {
9
+ background-position: 200% 50%;
10
+ }
11
+ }
12
+
13
+ @keyframes equalizerBar1 {
14
+ 0%,
15
+ 100% {
16
+ height: 17px;
17
+ }
18
+ 50% {
19
+ height: 7px;
20
+ }
21
+ }
22
+
23
+ @keyframes equalizerBar2 {
24
+ 0%,
25
+ 100% {
26
+ height: 5px;
27
+ }
28
+ 50% {
29
+ height: 17px;
30
+ }
31
+ }
32
+
33
+ @keyframes equalizerBar3 {
34
+ 0%,
35
+ 100% {
36
+ height: 21px;
37
+ }
38
+ 50% {
39
+ height: 9px;
40
+ }
41
+ }
42
+
43
+ @keyframes equalizerBar4 {
44
+ 0%,
45
+ 100% {
46
+ height: 17px;
47
+ }
48
+ 50% {
49
+ height: 5px;
50
+ }
51
+ }
52
+
53
+ @keyframes equalizerBar5 {
54
+ 0%,
55
+ 100% {
56
+ height: 7px;
57
+ }
58
+ 50% {
59
+ height: 17px;
60
+ }
61
+ }
62
+
63
+ .previewCard {
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 12px;
67
+ width: 100%;
68
+ max-width: 344px;
69
+ padding: 4px;
70
+ border-radius: 8px;
71
+ border: 1px solid var(--neutral-alpha-4);
72
+ background: transparent;
73
+ cursor: pointer;
74
+ box-sizing: border-box;
75
+ transition: background-color 0.2s ease, border-color 0.2s ease;
76
+ outline: none;
77
+ }
78
+
79
+ .previewCard:focus,
80
+ .previewCard:focus-visible {
81
+ outline: none;
82
+ }
83
+
84
+ .previewCard:hover:not(.selected):not(.skeleton):not(.playing),
85
+ .previewCard:has(.leftButton:hover):not(.selected):not(.skeleton) {
86
+ background-color: var(--neutral-alpha-2);
87
+ }
88
+
89
+ .selected {
90
+ background-color: var(--neutral-alpha-2);
91
+ border: 2px solid var(--colors-accent-accent-10);
92
+ padding: 3px;
93
+ }
94
+
95
+ .skeleton {
96
+ background: linear-gradient(
97
+ 90deg,
98
+ var(--neutral-alpha-3) 0%,
99
+ rgba(221, 235, 236, 0.14) 35%,
100
+ rgba(221, 235, 236, 0.02) 70%,
101
+ var(--neutral-alpha-3) 100%
102
+ );
103
+ background-size: 200% 100%;
104
+ animation: skeletonShimmer 2.5s linear infinite;
105
+ pointer-events: none;
106
+ border-color: transparent;
107
+ }
108
+
109
+ .leftButton {
110
+ display: flex;
111
+ align-items: center;
112
+ justify-content: center;
113
+ flex-shrink: 0;
114
+ width: 40px;
115
+ height: 40px;
116
+ padding: 0;
117
+ border: none;
118
+ border-radius: 6px;
119
+ background: var(--neutral-alpha-3);
120
+ color: var(--neutral-alpha-11);
121
+ cursor: pointer;
122
+ outline: none;
123
+ }
124
+
125
+ .leftButton:focus,
126
+ .leftButton:focus-visible {
127
+ outline: none;
128
+ }
129
+
130
+ .leftButton:disabled {
131
+ cursor: default;
132
+ pointer-events: none;
133
+ }
134
+
135
+ .skeleton .leftButton,
136
+ .skeleton .actions,
137
+ .skeleton .waveformContainer {
138
+ opacity: 0;
139
+ }
140
+
141
+ .equalizer {
142
+ display: none;
143
+ align-items: center;
144
+ justify-content: center;
145
+ gap: 1.8px;
146
+ width: 19px;
147
+ height: 21px;
148
+ }
149
+
150
+ .equalizerBar {
151
+ display: block;
152
+ width: 2px;
153
+ border-radius: 4.8px;
154
+ background: var(--neutral-alpha-11);
155
+ flex-shrink: 0;
156
+ }
157
+
158
+ .equalizerBar:nth-child(1) {
159
+ height: 17px;
160
+ animation: equalizerBar1 0.8s ease-in-out infinite;
161
+ }
162
+
163
+ .equalizerBar:nth-child(2) {
164
+ height: 5px;
165
+ animation: equalizerBar2 0.8s ease-in-out infinite;
166
+ }
167
+
168
+ .equalizerBar:nth-child(3) {
169
+ height: 21px;
170
+ animation: equalizerBar3 0.8s ease-in-out infinite;
171
+ }
172
+
173
+ .equalizerBar:nth-child(4) {
174
+ height: 17px;
175
+ animation: equalizerBar4 0.8s ease-in-out infinite;
176
+ }
177
+
178
+ .equalizerBar:nth-child(5) {
179
+ height: 7px;
180
+ animation: equalizerBar5 0.8s ease-in-out infinite;
181
+ }
182
+
183
+ .pauseOnHover {
184
+ display: none;
185
+ }
186
+
187
+ .playIcon {
188
+ display: flex;
189
+ }
190
+
191
+ .leftButton.isPlaying .equalizer {
192
+ display: flex;
193
+ }
194
+
195
+ .leftButton.isPlaying .playIcon {
196
+ display: none !important;
197
+ }
198
+
199
+ .previewCard.playing:has(.leftButton:hover) .equalizer {
200
+ display: none;
201
+ }
202
+
203
+ .previewCard.playing:has(.leftButton:hover) .pauseOnHover {
204
+ display: flex;
205
+ }
206
+
207
+ .waveformContainer {
208
+ position: relative;
209
+ flex: 1 1 0;
210
+ min-width: 0;
211
+ height: 40px;
212
+ overflow: hidden;
213
+ cursor: pointer;
214
+ }
215
+
216
+ .waveformContainer :global(wave) {
217
+ display: block;
218
+ }
219
+
220
+ .skeleton .waveformContainer :global(wave) {
221
+ visibility: hidden;
222
+ }
223
+
224
+ .actions {
225
+ display: flex;
226
+ flex-shrink: 0;
227
+ align-items: center;
228
+ justify-content: center;
229
+ width: 24px;
230
+ height: 24px;
231
+ border-radius: 4px;
232
+ opacity: 0.4;
233
+ transition: opacity 0.2s ease;
234
+ }
235
+
236
+ .actions :global(button) {
237
+ display: inline-flex;
238
+ align-items: center;
239
+ justify-content: center;
240
+ width: 24px !important;
241
+ height: 24px !important;
242
+ min-width: 24px !important;
243
+ min-height: 24px !important;
244
+ padding: 0 !important;
245
+ border-radius: 4px;
246
+ outline: none;
247
+ }
248
+
249
+ .actions :global(button:focus),
250
+ .actions :global(button:focus-visible) {
251
+ outline: none;
252
+ }
253
+
254
+ .actions :global(svg) {
255
+ width: 16px;
256
+ height: 16px;
257
+ }
258
+
259
+ .previewCard:hover .actions,
260
+ .previewCard:has(.leftButton:hover) .actions,
261
+ .selected .actions {
262
+ opacity: 1;
263
+ }