@moises.ai/design-system 4.15.9 → 4.15.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/dist/index.js +3899 -3855
- package/package.json +1 -1
- package/src/components/ListCards/CardDetails.stories.jsx +13 -0
- package/src/components/ListCards/ListCards.jsx +30 -13
- package/src/components/ListCards/ListCards.module.css +7 -1
- package/src/components/ListCards/ListCards.stories.jsx +4 -0
- package/src/components/PreviewCard/PreviewCard.jsx +120 -66
- package/src/components/PreviewCard/PreviewCard.stories.jsx +2 -1
package/package.json
CHANGED
|
@@ -21,6 +21,7 @@ The \`CardDetails\` component is the enhanced card variant for the ListCards com
|
|
|
21
21
|
- **See details button** - Navigate to detailed views or trigger modals
|
|
22
22
|
- **Clickable avatar** - Add interactive elements like play/pause to the avatar area
|
|
23
23
|
- **Custom avatar content** - Display custom React nodes in the avatar area
|
|
24
|
+
- **Avatar tooltip** - Show a tooltip on hover over the avatar (e.g. for status icons)
|
|
24
25
|
- **Auto description** - Renders \`item.description\` (or the \`description\` prop) by default
|
|
25
26
|
- **Rich formatting** - Override the description by passing \`children\` for separators, ellipsis, etc.
|
|
26
27
|
|
|
@@ -62,6 +63,17 @@ import { ListCards } from '@moises.ai/design-system'
|
|
|
62
63
|
</ListCards>
|
|
63
64
|
\`\`\`
|
|
64
65
|
|
|
66
|
+
#### Avatar status icon with tooltip
|
|
67
|
+
|
|
68
|
+
\`\`\`jsx
|
|
69
|
+
<ListCards.CardDetails
|
|
70
|
+
item={item}
|
|
71
|
+
disabled={item.isFailed}
|
|
72
|
+
avatarContent={<AlertIcon />}
|
|
73
|
+
avatarTooltip="Voice training failed"
|
|
74
|
+
/>
|
|
75
|
+
\`\`\`
|
|
76
|
+
|
|
65
77
|
#### Full-featured CardDetails
|
|
66
78
|
|
|
67
79
|
\`\`\`jsx
|
|
@@ -95,6 +107,7 @@ import { ListCards } from '@moises.ai/design-system'
|
|
|
95
107
|
|
|
96
108
|
onAvatarClick?: (event: React.MouseEvent) => void; // Called when avatar area is clicked
|
|
97
109
|
avatarContent?: React.ReactNode; // Custom content to render in avatar area
|
|
110
|
+
avatarTooltip?: string; // Tooltip shown on hover over the avatar area (e.g. status icons)
|
|
98
111
|
|
|
99
112
|
// Content
|
|
100
113
|
children?: React.ReactNode; // Custom content for the description area (overrides description)
|
|
@@ -11,8 +11,18 @@ import classNames from 'classnames'
|
|
|
11
11
|
import React from 'react'
|
|
12
12
|
import { SparklesIcon } from '../../icons/SparklesIcon'
|
|
13
13
|
import { ThumbnailPicker } from '../ThumbnailPicker/ThumbnailPicker'
|
|
14
|
+
import { Tooltip } from '../Tooltip/Tooltip'
|
|
14
15
|
import styles from './ListCards.module.css'
|
|
15
16
|
|
|
17
|
+
const withAvatarTooltip = (content, tooltip) => {
|
|
18
|
+
if (!tooltip) return content
|
|
19
|
+
return (
|
|
20
|
+
<Tooltip content={tooltip}>
|
|
21
|
+
<div className={styles.avatarTooltipTrigger}>{content}</div>
|
|
22
|
+
</Tooltip>
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
16
26
|
const Card = ({
|
|
17
27
|
className,
|
|
18
28
|
image,
|
|
@@ -139,6 +149,7 @@ const CardDetails = ({
|
|
|
139
149
|
onSeeDetails,
|
|
140
150
|
onAvatarClick,
|
|
141
151
|
avatarContent,
|
|
152
|
+
avatarTooltip,
|
|
142
153
|
seeDetailsText = 'Details',
|
|
143
154
|
actions,
|
|
144
155
|
loading,
|
|
@@ -194,15 +205,18 @@ const CardDetails = ({
|
|
|
194
205
|
}}
|
|
195
206
|
>
|
|
196
207
|
{!onAvatarClick &&
|
|
197
|
-
(
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
208
|
+
withAvatarTooltip(
|
|
209
|
+
loading ? (
|
|
210
|
+
<div className={styles.listCardsAvatarIcon}>
|
|
211
|
+
<SparklesIcon className={styles.loadingIcon} />
|
|
212
|
+
</div>
|
|
213
|
+
) : avatarContent ? (
|
|
214
|
+
<div className={styles.listCardsAvatarIcon}>{avatarContent}</div>
|
|
215
|
+
) : (
|
|
216
|
+
<Avatar src={avatarUrl} className={styles.listCardsAvatar} />
|
|
217
|
+
),
|
|
218
|
+
avatarTooltip,
|
|
219
|
+
)}
|
|
206
220
|
<Flex
|
|
207
221
|
direction="column"
|
|
208
222
|
justify="center"
|
|
@@ -254,10 +268,13 @@ const CardDetails = ({
|
|
|
254
268
|
: undefined
|
|
255
269
|
}
|
|
256
270
|
>
|
|
257
|
-
{
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
271
|
+
{withAvatarTooltip(
|
|
272
|
+
avatarContent ? (
|
|
273
|
+
<div className={styles.avatarContent}>{avatarContent}</div>
|
|
274
|
+
) : (
|
|
275
|
+
<Avatar src={avatarUrl} className={styles.listCardsAvatar} />
|
|
276
|
+
),
|
|
277
|
+
avatarTooltip,
|
|
261
278
|
)}
|
|
262
279
|
</div>
|
|
263
280
|
</div>
|
|
@@ -58,12 +58,18 @@
|
|
|
58
58
|
height: 80px;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
.avatarTooltipTrigger {
|
|
62
|
+
pointer-events: auto;
|
|
63
|
+
display: flex;
|
|
64
|
+
flex-shrink: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
.listCardsAvatarIcon {
|
|
62
68
|
width: 60px;
|
|
63
69
|
height: 60px;
|
|
64
70
|
min-width: 60px;
|
|
65
71
|
border-radius: 6px;
|
|
66
|
-
|
|
72
|
+
|
|
67
73
|
background: var(--neutral-alpha-2);
|
|
68
74
|
display: flex;
|
|
69
75
|
align-items: center;
|
|
@@ -141,6 +141,7 @@ import { ListCards } from '@moises.ai/design-system'
|
|
|
141
141
|
seeDetailsText?: string; // Custom text for the details button (default: "Details")
|
|
142
142
|
onAvatarClick?: (event: React.MouseEvent) => void; // Avatar click handler
|
|
143
143
|
avatarContent?: React.ReactNode; // Custom avatar content (e.g., play/pause button)
|
|
144
|
+
avatarTooltip?: string; // Tooltip shown on hover over the avatar area
|
|
144
145
|
}
|
|
145
146
|
\`\`\`
|
|
146
147
|
`,
|
|
@@ -396,6 +397,9 @@ export const WithCustomActions = {
|
|
|
396
397
|
/>
|
|
397
398
|
) : undefined
|
|
398
399
|
}
|
|
400
|
+
avatarTooltip={
|
|
401
|
+
item.isFailed ? 'Voice training failed' : undefined
|
|
402
|
+
}
|
|
399
403
|
actions={
|
|
400
404
|
item.isFailed ? (
|
|
401
405
|
<IconButton
|
|
@@ -37,6 +37,7 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
37
37
|
const waveSurferRef = useRef(null)
|
|
38
38
|
const skipTimeSyncRef = useRef(false)
|
|
39
39
|
const pendingSeekTimeRef = useRef(null)
|
|
40
|
+
const pendingPlayHandoffRef = useRef(null)
|
|
40
41
|
|
|
41
42
|
const showSkeleton = loading || !isReady
|
|
42
43
|
|
|
@@ -50,48 +51,92 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
50
51
|
waveSurferRef.current?.pause()
|
|
51
52
|
}, [])
|
|
52
53
|
|
|
54
|
+
const resetHiddenProgress = useCallback(
|
|
55
|
+
(ws) => {
|
|
56
|
+
if (!hiddenProgress) return
|
|
57
|
+
|
|
58
|
+
ws?.setOptions({ progressColor: waveColor })
|
|
59
|
+
},
|
|
60
|
+
[hiddenProgress, waveColor],
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
const handoffFromPreviousActive = useCallback(
|
|
64
|
+
(
|
|
65
|
+
currentWave,
|
|
66
|
+
{ seekTime, syncTime = false, skipPauseFor = null } = {},
|
|
67
|
+
) => {
|
|
68
|
+
const previousActive = wavegroup?.state?.active
|
|
69
|
+
const isHandoff = previousActive && previousActive !== currentWave
|
|
70
|
+
|
|
71
|
+
if (!isHandoff) {
|
|
72
|
+
if (typeof seekTime === 'number') {
|
|
73
|
+
currentWave?.setTime(seekTime)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
isHandoff: false,
|
|
78
|
+
previousActive: null,
|
|
79
|
+
previousWasPlaying: false,
|
|
80
|
+
wasPlaying: Boolean(wavegroup?.state?.isPlaying),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const previousTime = previousActive.getCurrentTime?.()
|
|
85
|
+
const previousWasPlaying = Boolean(previousActive?.isPlaying?.())
|
|
86
|
+
const wasPlaying =
|
|
87
|
+
previousWasPlaying || Boolean(wavegroup?.state?.isPlaying)
|
|
88
|
+
|
|
89
|
+
if (previousActive !== skipPauseFor) {
|
|
90
|
+
previousActive.pause()
|
|
91
|
+
resetHiddenProgress(previousActive)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (typeof seekTime === 'number') {
|
|
95
|
+
currentWave.setTime(seekTime)
|
|
96
|
+
} else if (syncTime && typeof previousTime === 'number') {
|
|
97
|
+
currentWave.setTime(previousTime)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
isHandoff: true,
|
|
102
|
+
previousActive,
|
|
103
|
+
previousWasPlaying,
|
|
104
|
+
wasPlaying,
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
[resetHiddenProgress, wavegroup],
|
|
108
|
+
)
|
|
109
|
+
|
|
53
110
|
const play = useCallback(() => {
|
|
54
|
-
|
|
111
|
+
const currentWave = waveSurferRef.current
|
|
55
112
|
|
|
56
|
-
if (
|
|
57
|
-
wavegroup?.state?.active &&
|
|
58
|
-
wavegroup.state.active !== waveSurferRef.current
|
|
59
|
-
) {
|
|
60
|
-
wavegroup.state.active.pause()
|
|
61
|
-
}
|
|
113
|
+
if (!currentWave) return
|
|
62
114
|
|
|
63
115
|
if (!isSafari) {
|
|
64
|
-
|
|
116
|
+
currentWave.setOptions({ progressColor })
|
|
65
117
|
}
|
|
66
|
-
|
|
67
|
-
}, [isSafari, progressColor
|
|
118
|
+
currentWave.play()
|
|
119
|
+
}, [isSafari, progressColor])
|
|
68
120
|
|
|
69
121
|
const startPlay = useCallback(() => {
|
|
70
|
-
|
|
122
|
+
const currentWave = waveSurferRef.current
|
|
71
123
|
|
|
72
|
-
if (
|
|
73
|
-
wavegroup?.state?.active &&
|
|
74
|
-
wavegroup.state.active !== waveSurferRef.current
|
|
75
|
-
) {
|
|
76
|
-
wavegroup.state.active.pause()
|
|
124
|
+
if (showSkeleton || !currentWave) return
|
|
77
125
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
isPlaying: false,
|
|
81
|
-
active: null,
|
|
82
|
-
}))
|
|
83
|
-
}
|
|
126
|
+
const previousActive = wavegroup?.state?.active
|
|
127
|
+
const isHandoff = previousActive && previousActive !== currentWave
|
|
84
128
|
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
if (isHandoff) {
|
|
130
|
+
const handoff = handoffFromPreviousActive(currentWave, {
|
|
131
|
+
syncTime: true,
|
|
132
|
+
})
|
|
133
|
+
pendingPlayHandoffRef.current = handoff.previousActive
|
|
134
|
+
} else {
|
|
135
|
+
pendingPlayHandoffRef.current = null
|
|
91
136
|
}
|
|
92
137
|
|
|
93
138
|
play()
|
|
94
|
-
}, [showSkeleton, play, wavegroup])
|
|
139
|
+
}, [showSkeleton, play, handoffFromPreviousActive, wavegroup])
|
|
95
140
|
|
|
96
141
|
useImperativeHandle(
|
|
97
142
|
ref,
|
|
@@ -120,6 +165,8 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
120
165
|
const onPlay = useCallback(
|
|
121
166
|
(ws) => {
|
|
122
167
|
const isSeekHandoff = skipTimeSyncRef.current
|
|
168
|
+
const pendingPlayHandoff = pendingPlayHandoffRef.current
|
|
169
|
+
const pendingSeekTime = pendingSeekTimeRef.current
|
|
123
170
|
|
|
124
171
|
setIsPlaying(true)
|
|
125
172
|
onPlayStateChange?.(true)
|
|
@@ -128,21 +175,15 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
128
175
|
}
|
|
129
176
|
|
|
130
177
|
if (wavegroup) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
if (!skipTimeSyncRef.current) {
|
|
138
|
-
ws.setTime(currentTime)
|
|
139
|
-
} else if (pendingSeekTimeRef.current != null) {
|
|
140
|
-
ws.setTime(pendingSeekTimeRef.current)
|
|
141
|
-
pendingSeekTimeRef.current = null
|
|
142
|
-
}
|
|
143
|
-
}
|
|
178
|
+
handoffFromPreviousActive(ws, {
|
|
179
|
+
seekTime: isSeekHandoff ? pendingSeekTime : undefined,
|
|
180
|
+
syncTime: !isSeekHandoff && !pendingPlayHandoff,
|
|
181
|
+
skipPauseFor: pendingPlayHandoff,
|
|
182
|
+
})
|
|
144
183
|
|
|
145
184
|
skipTimeSyncRef.current = false
|
|
185
|
+
pendingSeekTimeRef.current = null
|
|
186
|
+
pendingPlayHandoffRef.current = null
|
|
146
187
|
|
|
147
188
|
wavegroup.setter((prev) => ({
|
|
148
189
|
...prev,
|
|
@@ -152,7 +193,7 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
152
193
|
}))
|
|
153
194
|
}
|
|
154
195
|
},
|
|
155
|
-
[onPlayStateChange, onSelect, wavegroup],
|
|
196
|
+
[handoffFromPreviousActive, onPlayStateChange, onSelect, wavegroup],
|
|
156
197
|
)
|
|
157
198
|
|
|
158
199
|
const onPause = useCallback(() => {
|
|
@@ -161,29 +202,40 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
161
202
|
|
|
162
203
|
if (wavegroup) {
|
|
163
204
|
if (wavegroup.state.active !== waveSurferRef.current) {
|
|
164
|
-
|
|
165
|
-
waveSurferRef.current?.setOptions({ progressColor: waveColor })
|
|
166
|
-
}
|
|
167
|
-
return
|
|
205
|
+
resetHiddenProgress(waveSurferRef.current)
|
|
168
206
|
}
|
|
169
207
|
|
|
170
|
-
wavegroup.setter((prev) =>
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
208
|
+
wavegroup.setter((prev) => {
|
|
209
|
+
if (prev.active !== waveSurferRef.current) {
|
|
210
|
+
return prev
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
...prev,
|
|
215
|
+
isPlaying: false,
|
|
216
|
+
}
|
|
217
|
+
})
|
|
174
218
|
}
|
|
175
|
-
}, [onPlayStateChange, wavegroup,
|
|
219
|
+
}, [onPlayStateChange, wavegroup, resetHiddenProgress])
|
|
176
220
|
|
|
177
221
|
const onSeeking = useCallback(
|
|
178
222
|
(ws) => {
|
|
179
223
|
if (!wavegroup) return
|
|
180
224
|
|
|
225
|
+
const previousActive = wavegroup.state.active
|
|
226
|
+
const isHandoff = previousActive && previousActive !== ws
|
|
227
|
+
|
|
228
|
+
if (isHandoff) {
|
|
229
|
+
resetHiddenProgress(previousActive)
|
|
230
|
+
return
|
|
231
|
+
}
|
|
232
|
+
|
|
181
233
|
wavegroup.setter((prev) => ({
|
|
182
234
|
...prev,
|
|
183
235
|
active: ws,
|
|
184
236
|
}))
|
|
185
237
|
},
|
|
186
|
-
[wavegroup],
|
|
238
|
+
[resetHiddenProgress, wavegroup],
|
|
187
239
|
)
|
|
188
240
|
|
|
189
241
|
const handleInteraction = useCallback(
|
|
@@ -208,20 +260,15 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
208
260
|
|
|
209
261
|
const seekTime =
|
|
210
262
|
typeof newTime === 'number' ? newTime : (ws.getCurrentTime?.() ?? 0)
|
|
211
|
-
|
|
212
|
-
const
|
|
263
|
+
|
|
264
|
+
const handoff = handoffFromPreviousActive(ws, {
|
|
265
|
+
seekTime,
|
|
266
|
+
})
|
|
213
267
|
|
|
214
268
|
pendingSeekTimeRef.current = seekTime
|
|
215
269
|
skipTimeSyncRef.current = true
|
|
216
270
|
|
|
217
|
-
|
|
218
|
-
wavegroup.state.lastPlayed = ws
|
|
219
|
-
wavegroup.state.isPlaying = wasPlaying
|
|
220
|
-
|
|
221
|
-
ws.setTime(seekTime)
|
|
222
|
-
previousActive.pause()
|
|
223
|
-
|
|
224
|
-
if (!previousWasPlaying) {
|
|
271
|
+
if (!handoff.previousWasPlaying) {
|
|
225
272
|
previousActive.emit?.('pause')
|
|
226
273
|
}
|
|
227
274
|
|
|
@@ -229,7 +276,7 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
229
276
|
ws.setOptions({ progressColor })
|
|
230
277
|
}
|
|
231
278
|
|
|
232
|
-
if (wasPlaying) {
|
|
279
|
+
if (handoff.wasPlaying) {
|
|
233
280
|
ws.play()
|
|
234
281
|
}
|
|
235
282
|
|
|
@@ -237,10 +284,17 @@ export const PreviewCard = forwardRef(function PreviewCard(
|
|
|
237
284
|
...prev,
|
|
238
285
|
active: ws,
|
|
239
286
|
lastPlayed: ws,
|
|
240
|
-
isPlaying: wasPlaying,
|
|
287
|
+
isPlaying: handoff.wasPlaying,
|
|
241
288
|
}))
|
|
242
289
|
},
|
|
243
|
-
[
|
|
290
|
+
[
|
|
291
|
+
wavegroup,
|
|
292
|
+
isSafari,
|
|
293
|
+
progressColor,
|
|
294
|
+
handoffFromPreviousActive,
|
|
295
|
+
showSkeleton,
|
|
296
|
+
onSelect,
|
|
297
|
+
],
|
|
244
298
|
)
|
|
245
299
|
|
|
246
300
|
const onFinish = useCallback(() => {
|
|
@@ -89,7 +89,7 @@ function PreviewCardGroupStory() {
|
|
|
89
89
|
<Flex direction="column" gap="3" width="344px">
|
|
90
90
|
<Text size="1" color="gray">
|
|
91
91
|
Click the card (outside waveform/play) to select. Play on another card
|
|
92
|
-
continues from the same position.
|
|
92
|
+
continues from the same position with hidden progress and auto-repeat.
|
|
93
93
|
</Text>
|
|
94
94
|
{PREVIEW_ITEMS.map((item) => (
|
|
95
95
|
<PreviewCard
|
|
@@ -98,6 +98,7 @@ function PreviewCardGroupStory() {
|
|
|
98
98
|
selected={selectedId === item.id}
|
|
99
99
|
wavegroup={wavegroup}
|
|
100
100
|
hiddenProgress={true}
|
|
101
|
+
autoRepeat={true}
|
|
101
102
|
onSelect={() => setSelectedId(item.id)}
|
|
102
103
|
actions={<MoreButton aria-label={`More options for ${item.label}`} />}
|
|
103
104
|
/>
|