@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moises.ai/design-system",
3
- "version": "4.15.9",
3
+ "version": "4.15.11",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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
- (loading ? (
198
- <div className={styles.listCardsAvatarIcon}>
199
- <SparklesIcon className={styles.loadingIcon} />
200
- </div>
201
- ) : avatarContent ? (
202
- <div className={styles.listCardsAvatarIcon}>{avatarContent}</div>
203
- ) : (
204
- <Avatar src={avatarUrl} className={styles.listCardsAvatar} />
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
- {avatarContent ? (
258
- <div className={styles.avatarContent}>{avatarContent}</div>
259
- ) : (
260
- <Avatar src={avatarUrl} className={styles.listCardsAvatar} />
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
- if (!waveSurferRef.current) return
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
- waveSurferRef.current.setOptions({ progressColor })
116
+ currentWave.setOptions({ progressColor })
65
117
  }
66
- waveSurferRef.current.play()
67
- }, [isSafari, progressColor, wavegroup])
118
+ currentWave.play()
119
+ }, [isSafari, progressColor])
68
120
 
69
121
  const startPlay = useCallback(() => {
70
- if (showSkeleton || !waveSurferRef.current) return
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
- wavegroup.setter((prev) => ({
79
- ...prev,
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
- wavegroup?.state?.active &&
87
- wavegroup.state.active !== waveSurferRef.current &&
88
- wavegroup.state.active.getCurrentTime
89
- ) {
90
- waveSurferRef.current.setTime(wavegroup.state.active.getCurrentTime())
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
- const previousActive = wavegroup.state.active
132
-
133
- if (previousActive && previousActive !== ws) {
134
- const currentTime = previousActive.getCurrentTime()
135
- previousActive.pause()
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
- if (hiddenProgress) {
165
- waveSurferRef.current?.setOptions({ progressColor: waveColor })
166
- }
167
- return
205
+ resetHiddenProgress(waveSurferRef.current)
168
206
  }
169
207
 
170
- wavegroup.setter((prev) => ({
171
- ...prev,
172
- isPlaying: false,
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, hiddenProgress, waveColor])
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
- const previousWasPlaying = Boolean(previousActive?.isPlaying?.())
212
- const wasPlaying = previousWasPlaying || wavegroup.state.isPlaying
263
+
264
+ const handoff = handoffFromPreviousActive(ws, {
265
+ seekTime,
266
+ })
213
267
 
214
268
  pendingSeekTimeRef.current = seekTime
215
269
  skipTimeSyncRef.current = true
216
270
 
217
- wavegroup.state.active = ws
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
- [wavegroup, isSafari, progressColor, showSkeleton, onSelect],
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
  />