@moises.ai/design-system 4.15.2 → 4.15.4

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.2",
3
+ "version": "4.15.4",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -1,27 +1,41 @@
1
- import { useState, useCallback, useEffect, useRef } from 'react'
1
+ import {
2
+ useState,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ forwardRef,
7
+ useImperativeHandle,
8
+ } from 'react'
2
9
  import WavesurferPlayer from '@wavesurfer/react'
3
10
  import classNames from 'classnames'
4
11
  import { PlayIcon, PauseIcon } from '../../icons'
5
12
  import styles from './PreviewCard.module.css'
6
13
 
7
- export function PreviewCard({
8
- audio,
9
- selected = false,
10
- loading = false,
11
- wavegroup,
12
- onSelect,
13
- onPlayStateChange,
14
- actions,
15
- autoRepeat = false,
16
- waveColor = 'rgba(90, 97, 105, 1)',
17
- progressColor = 'rgba(255, 255, 255, 1)',
18
- className,
19
- }) {
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
+ ) {
20
32
  const [isPlaying, setIsPlaying] = useState(false)
21
33
  const [isReady, setIsReady] = useState(false)
22
34
  const [isSafari, setIsSafari] = useState(false)
23
35
 
24
36
  const waveSurferRef = useRef(null)
37
+ const skipTimeSyncRef = useRef(false)
38
+ const pendingSeekTimeRef = useRef(null)
25
39
 
26
40
  const showSkeleton = loading || !isReady
27
41
 
@@ -51,6 +65,44 @@ export function PreviewCard({
51
65
  waveSurferRef.current.play()
52
66
  }, [isSafari, progressColor, wavegroup])
53
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
+
54
106
  useEffect(() => {
55
107
  if (waveSurferRef.current && audio) {
56
108
  waveSurferRef.current.load(audio)
@@ -66,16 +118,31 @@ export function PreviewCard({
66
118
 
67
119
  const onPlay = useCallback(
68
120
  (ws) => {
121
+ const isSeekHandoff = skipTimeSyncRef.current
122
+
69
123
  setIsPlaying(true)
70
124
  onPlayStateChange?.(true)
125
+ if (!isSeekHandoff) {
126
+ onSelect?.()
127
+ }
71
128
 
72
129
  if (wavegroup) {
73
- if (wavegroup.state.active && wavegroup.state.active !== ws) {
74
- const currentTime = wavegroup.state.active.getCurrentTime()
75
- wavegroup.state.active.pause()
76
- ws.setTime(currentTime)
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
+ }
77
142
  }
78
143
 
144
+ skipTimeSyncRef.current = false
145
+
79
146
  wavegroup.setter((prev) => ({
80
147
  ...prev,
81
148
  isPlaying: true,
@@ -84,7 +151,7 @@ export function PreviewCard({
84
151
  }))
85
152
  }
86
153
  },
87
- [onPlayStateChange, wavegroup],
154
+ [onPlayStateChange, onSelect, wavegroup],
88
155
  )
89
156
 
90
157
  const onPause = useCallback(() => {
@@ -92,6 +159,10 @@ export function PreviewCard({
92
159
  onPlayStateChange?.(false)
93
160
 
94
161
  if (wavegroup) {
162
+ if (wavegroup.state.active !== waveSurferRef.current) {
163
+ return
164
+ }
165
+
95
166
  wavegroup.setter((prev) => ({
96
167
  ...prev,
97
168
  isPlaying: false,
@@ -111,6 +182,55 @@ export function PreviewCard({
111
182
  [wavegroup],
112
183
  )
113
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
+
114
234
  const onFinish = useCallback(() => {
115
235
  if (autoRepeat) {
116
236
  waveSurferRef.current?.play()
@@ -136,29 +256,8 @@ export function PreviewCard({
136
256
  return
137
257
  }
138
258
 
139
- if (
140
- wavegroup?.state?.active &&
141
- wavegroup.state.active !== waveSurferRef.current
142
- ) {
143
- wavegroup.state.active.pause()
144
-
145
- wavegroup.setter((prev) => ({
146
- ...prev,
147
- isPlaying: false,
148
- active: null,
149
- }))
150
- }
151
-
152
- if (
153
- wavegroup?.state?.active &&
154
- wavegroup.state.active !== waveSurferRef.current &&
155
- wavegroup.state.active.getCurrentTime
156
- ) {
157
- waveSurferRef.current.setTime(wavegroup.state.active.getCurrentTime())
158
- }
159
-
160
- play()
161
- }, [showSkeleton, pause, play, wavegroup])
259
+ startPlay()
260
+ }, [showSkeleton, pause, startPlay])
162
261
 
163
262
  const handleCardClick = useCallback(() => {
164
263
  if (showSkeleton) return
@@ -167,6 +266,7 @@ export function PreviewCard({
167
266
 
168
267
  return (
169
268
  <div
269
+ id={id}
170
270
  className={classNames(
171
271
  styles.previewCard,
172
272
  selected && styles.selected,
@@ -175,8 +275,9 @@ export function PreviewCard({
175
275
  className,
176
276
  )}
177
277
  onClick={handleCardClick}
178
- role="button"
179
- tabIndex={showSkeleton ? -1 : 0}
278
+ role="option"
279
+ aria-selected={selected}
280
+ tabIndex={showSkeleton ? -1 : (tabIndexProp ?? 0)}
180
281
  onKeyDown={(e) => {
181
282
  if (e.key === 'Enter' || e.key === ' ') {
182
283
  e.preventDefault()
@@ -230,6 +331,7 @@ export function PreviewCard({
230
331
  cursorWidth={0}
231
332
  height={40}
232
333
  onReady={onReady}
334
+ onInteraction={handleInteraction}
233
335
  onSeeking={onSeeking}
234
336
  onPlay={onPlay}
235
337
  onPause={onPause}
@@ -237,11 +339,6 @@ export function PreviewCard({
237
339
  backend={isSafari ? 'WebAudio' : 'MediaElement'}
238
340
  />
239
341
  )}
240
- {showSkeleton && (
241
- <span className={styles.skeletonWaveform} aria-hidden="true">
242
- <span className={styles.skeletonMask} />
243
- </span>
244
- )}
245
342
  </div>
246
343
 
247
344
  {actions && (
@@ -251,6 +348,6 @@ export function PreviewCard({
251
348
  )}
252
349
  </div>
253
350
  )
254
- }
351
+ })
255
352
 
256
353
  PreviewCard.displayName = 'PreviewCard'
@@ -1,9 +1,12 @@
1
- @keyframes skeleton_animate_mask {
1
+ @keyframes skeletonShimmer {
2
2
  0% {
3
- mask-position: 200% 50%;
3
+ background-position: 200% 50%;
4
+ }
5
+ 99.999% {
6
+ background-position: -200% 50%;
4
7
  }
5
8
  100% {
6
- mask-position: -100% 50%;
9
+ background-position: 200% 50%;
7
10
  }
8
11
  }
9
12
 
@@ -62,7 +65,6 @@
62
65
  align-items: center;
63
66
  gap: 12px;
64
67
  width: 100%;
65
- max-width: 344px;
66
68
  padding: 4px;
67
69
  border-radius: 8px;
68
70
  border: 1px solid var(--neutral-alpha-4);
@@ -70,6 +72,12 @@
70
72
  cursor: pointer;
71
73
  box-sizing: border-box;
72
74
  transition: background-color 0.2s ease, border-color 0.2s ease;
75
+ outline: none;
76
+ }
77
+
78
+ .previewCard:focus,
79
+ .previewCard:focus-visible {
80
+ outline: none;
73
81
  }
74
82
 
75
83
  .previewCard:hover:not(.selected):not(.skeleton):not(.playing),
@@ -87,8 +95,12 @@
87
95
  background: linear-gradient(
88
96
  90deg,
89
97
  var(--neutral-alpha-3) 0%,
90
- rgba(221, 235, 236, 0.02) 61.19%
98
+ rgba(221, 235, 236, 0.14) 35%,
99
+ rgba(221, 235, 236, 0.02) 70%,
100
+ var(--neutral-alpha-3) 100%
91
101
  );
102
+ background-size: 200% 100%;
103
+ animation: skeletonShimmer 2.5s linear infinite;
92
104
  pointer-events: none;
93
105
  border-color: transparent;
94
106
  }
@@ -106,6 +118,12 @@
106
118
  background: var(--neutral-alpha-3);
107
119
  color: var(--neutral-alpha-11);
108
120
  cursor: pointer;
121
+ outline: none;
122
+ }
123
+
124
+ .leftButton:focus,
125
+ .leftButton:focus-visible {
126
+ outline: none;
109
127
  }
110
128
 
111
129
  .leftButton:disabled {
@@ -114,7 +132,8 @@
114
132
  }
115
133
 
116
134
  .skeleton .leftButton,
117
- .skeleton .actions {
135
+ .skeleton .actions,
136
+ .skeleton .waveformContainer {
118
137
  opacity: 0;
119
138
  }
120
139
 
@@ -201,26 +220,6 @@
201
220
  visibility: hidden;
202
221
  }
203
222
 
204
- .skeletonWaveform {
205
- position: absolute;
206
- inset: 0;
207
- overflow: hidden;
208
- border-radius: 4px;
209
- }
210
-
211
- .skeletonMask {
212
- width: 100%;
213
- height: 100%;
214
- display: block;
215
- background-repeat: repeat-x;
216
- background-position: 30px center;
217
- mask-image: radial-gradient(circle, blue 50%, rgba(255, 255, 0, 0.5) 50%);
218
- mask-size: 400% 400%;
219
- animation: skeleton_animate_mask 3s linear infinite;
220
- -webkit-animation: skeleton_animate_mask 3s linear infinite;
221
- background-image: url('/waveform.svg');
222
- }
223
-
224
223
  .actions {
225
224
  display: flex;
226
225
  flex-shrink: 0;
@@ -243,6 +242,12 @@
243
242
  min-height: 24px !important;
244
243
  padding: 0 !important;
245
244
  border-radius: 4px;
245
+ outline: none;
246
+ }
247
+
248
+ .actions :global(button:focus),
249
+ .actions :global(button:focus-visible) {
250
+ outline: none;
246
251
  }
247
252
 
248
253
  .actions :global(svg) {