@moises.ai/design-system 4.15.0 → 4.15.2

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.0",
3
+ "version": "4.15.2",
4
4
  "description": "Design System package based on @radix-ui/themes with custom defaults",
5
5
  "private": false,
6
6
  "type": "module",
@@ -21,7 +21,8 @@ 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
- - **Rich formatting** - Support for formatted descriptions with separators
24
+ - **Auto description** - Renders \`item.description\` (or the \`description\` prop) by default
25
+ - **Rich formatting** - Override the description by passing \`children\` for separators, ellipsis, etc.
25
26
 
26
27
  ### Usage Examples
27
28
 
@@ -83,6 +84,7 @@ import { ListCards } from '@moises.ai/design-system'
83
84
  id?: string; // Unique identifier
84
85
  name?: string; // Display name
85
86
  avatar?: string; // Avatar image URL
87
+ description?: string; // Description text (rendered when no children are provided)
86
88
 
87
89
  // Interactive features
88
90
  favorite?: boolean; // Whether item is favorited (shows/hides favorite icon)
@@ -95,7 +97,7 @@ import { ListCards } from '@moises.ai/design-system'
95
97
  avatarContent?: React.ReactNode; // Custom content to render in avatar area
96
98
 
97
99
  // Content
98
- children?: React.ReactNode; // Custom content for the description area
100
+ children?: React.ReactNode; // Custom content for the description area (overrides description)
99
101
  }
100
102
  \`\`\`
101
103
  `,
@@ -151,6 +153,96 @@ const CardDetailsExample = ({ title, children }) => (
151
153
  </Flex>
152
154
  )
153
155
 
156
+ export const WithDescriptionFromItem = {
157
+ render: () => (
158
+ <CardDetailsExample title="Description rendered automatically from item.description">
159
+ {sampleItems.map((item) => (
160
+ <ListCards.CardDetails key={item.id} item={item} />
161
+ ))}
162
+ </CardDetailsExample>
163
+ ),
164
+ parameters: {
165
+ docs: {
166
+ description: {
167
+ story:
168
+ 'When no `children` are provided, `CardDetails` automatically renders `item.description` (or the `description` prop) with text-overflow ellipsis. Pass `children` to fully customize the description area.',
169
+ },
170
+ },
171
+ },
172
+ }
173
+
174
+ const longDescriptionItems = [
175
+ {
176
+ id: '1',
177
+ name: 'Vintage Tube Amp',
178
+ description:
179
+ 'Warm tube saturation, expressive dynamics, and gritty lead breakup',
180
+ avatar:
181
+ 'https://storage.googleapis.com/moises-api-assets/voice/ana-avatar.jpg',
182
+ },
183
+ {
184
+ id: '2',
185
+ name: 'Modern Lead',
186
+ description:
187
+ 'Warm tube saturation, expressive dynamics, and gritty lead breakup ... and a lot more text to make it really long',
188
+ avatar:
189
+ 'https://storage.googleapis.com/moises-api-assets/voice/carrie-avatar.jpg',
190
+ },
191
+ {
192
+ id: '3',
193
+ name: 'Crunch',
194
+ description: 'Short one',
195
+ avatar:
196
+ 'https://storage.googleapis.com/moises-api-assets/voice/chris-avatar.jpg',
197
+ },
198
+ ]
199
+
200
+ export const WithLongDescription = {
201
+ render: () => (
202
+ <CardDetailsExample title="Long description (truncated with ellipsis)">
203
+ {longDescriptionItems.map((item) => (
204
+ <ListCards.CardDetails
205
+ key={item.id}
206
+ item={item}
207
+ // onSeeDetails={() => console.log('See details clicked for', item.name)}
208
+ />
209
+ ))}
210
+ </CardDetailsExample>
211
+ ),
212
+ parameters: {
213
+ docs: {
214
+ description: {
215
+ story:
216
+ 'Long descriptions are clipped to a single line with `text-overflow: ellipsis` so the card height stays consistent (68px).',
217
+ },
218
+ },
219
+ },
220
+ }
221
+
222
+ export const WithDescriptionProp = {
223
+ render: () => (
224
+ <CardDetailsExample title="Description rendered from the description prop">
225
+ {sampleItems.map((item) => (
226
+ <ListCards.CardDetails
227
+ key={item.id}
228
+ id={item.id}
229
+ name={item.name}
230
+ avatar={item.avatar}
231
+ description={item.description}
232
+ />
233
+ ))}
234
+ </CardDetailsExample>
235
+ ),
236
+ parameters: {
237
+ docs: {
238
+ description: {
239
+ story:
240
+ 'You can also pass `description` directly as a prop instead of using the `item` object.',
241
+ },
242
+ },
243
+ },
244
+ }
245
+
154
246
  export const WithFavoriteButton = {
155
247
  render: () => {
156
248
  const [favoriteItems, setFavoriteItems] = useState(['1'])
@@ -131,6 +131,7 @@ const CardDetails = ({
131
131
  className,
132
132
  avatar,
133
133
  name,
134
+ description,
134
135
  id,
135
136
  item,
136
137
  favorite,
@@ -150,6 +151,7 @@ const CardDetails = ({
150
151
  const itemId = item?.id || id
151
152
  const itemName = item?.name || name
152
153
  const itemAvatar = item?.avatar || avatar
154
+ const itemDescription = item?.description || description
153
155
 
154
156
  // Extract the avatar URL from the pattern data URL
155
157
  const avatarUrl =
@@ -176,7 +178,7 @@ const CardDetails = ({
176
178
  style={{
177
179
  '--color-surface': 'transparent',
178
180
  width: '100%',
179
- height: '68px',
181
+ minHeight: '68px',
180
182
  ...style,
181
183
  }}
182
184
  {...props}
@@ -201,23 +203,38 @@ const CardDetails = ({
201
203
  ) : (
202
204
  <Avatar src={avatarUrl} className={styles.listCardsAvatar} />
203
205
  ))}
204
- <Flex direction="column" justify="center">
206
+ <Flex
207
+ direction="column"
208
+ justify="center"
209
+ style={{ flex: 1, minWidth: 0 }}
210
+ >
205
211
  <Text as="div" size="2" className={styles.listCardsItemText}>
206
212
  {itemName}
207
213
  </Text>
208
- <Flex
209
- direction="row"
210
- gap="1"
211
- align="center"
212
- style={{
213
- flexWrap: 'nowrap',
214
- overflow: 'hidden',
215
- width: '100%',
216
- whiteSpace: 'nowrap',
217
- }}
218
- >
219
- {children}
220
- </Flex>
214
+ {children ? (
215
+ <Flex
216
+ direction="row"
217
+ gap="1"
218
+ align="center"
219
+ style={{
220
+ flexWrap: 'nowrap',
221
+ overflow: 'hidden',
222
+ width: '100%',
223
+ whiteSpace: 'nowrap',
224
+ minWidth: 0,
225
+ }}
226
+ >
227
+ {children}
228
+ </Flex>
229
+ ) : (
230
+ <Text
231
+ as="div"
232
+ size="1"
233
+ className={styles.listCardsItemDescriptionWrappable}
234
+ >
235
+ {itemDescription}
236
+ </Text>
237
+ )}
221
238
  </Flex>
222
239
  </Flex>
223
240
  </RadioCards.Item>
@@ -230,10 +247,10 @@ const CardDetails = ({
230
247
  onClick={
231
248
  onAvatarClick
232
249
  ? (e) => {
233
- e.stopPropagation()
234
- e.preventDefault()
235
- onAvatarClick(e)
236
- }
250
+ e.stopPropagation()
251
+ e.preventDefault()
252
+ onAvatarClick(e)
253
+ }
237
254
  : undefined
238
255
  }
239
256
  >
@@ -150,6 +150,7 @@
150
150
  -webkit-box-orient: vertical;
151
151
  overflow: hidden;
152
152
  text-overflow: ellipsis;
153
+ padding-right: 12px;
153
154
  }
154
155
 
155
156
  .listCardsDetailDescription {
@@ -0,0 +1,256 @@
1
+ import { useState, useCallback, useEffect, useRef } from 'react'
2
+ import WavesurferPlayer from '@wavesurfer/react'
3
+ import classNames from 'classnames'
4
+ import { PlayIcon, PauseIcon } from '../../icons'
5
+ import styles from './PreviewCard.module.css'
6
+
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
+ }) {
20
+ const [isPlaying, setIsPlaying] = useState(false)
21
+ const [isReady, setIsReady] = useState(false)
22
+ const [isSafari, setIsSafari] = useState(false)
23
+
24
+ const waveSurferRef = useRef(null)
25
+
26
+ const showSkeleton = loading || !isReady
27
+
28
+ useEffect(() => {
29
+ if (typeof navigator !== 'undefined') {
30
+ setIsSafari(/^((?!chrome|android).)*safari/i.test(navigator.userAgent))
31
+ }
32
+ }, [])
33
+
34
+ const pause = useCallback(() => {
35
+ waveSurferRef.current?.pause()
36
+ }, [])
37
+
38
+ const play = useCallback(() => {
39
+ if (!waveSurferRef.current) return
40
+
41
+ if (
42
+ wavegroup?.state?.active &&
43
+ wavegroup.state.active !== waveSurferRef.current
44
+ ) {
45
+ wavegroup.state.active.pause()
46
+ }
47
+
48
+ if (!isSafari) {
49
+ waveSurferRef.current.setOptions({ progressColor })
50
+ }
51
+ waveSurferRef.current.play()
52
+ }, [isSafari, progressColor, wavegroup])
53
+
54
+ useEffect(() => {
55
+ if (waveSurferRef.current && audio) {
56
+ waveSurferRef.current.load(audio)
57
+ setIsReady(false)
58
+ }
59
+ }, [audio])
60
+
61
+ const onReady = useCallback((ws) => {
62
+ waveSurferRef.current = ws
63
+ setIsReady(true)
64
+ setIsPlaying(false)
65
+ }, [])
66
+
67
+ const onPlay = useCallback(
68
+ (ws) => {
69
+ setIsPlaying(true)
70
+ onPlayStateChange?.(true)
71
+
72
+ 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)
77
+ }
78
+
79
+ wavegroup.setter((prev) => ({
80
+ ...prev,
81
+ isPlaying: true,
82
+ lastPlayed: ws,
83
+ active: ws,
84
+ }))
85
+ }
86
+ },
87
+ [onPlayStateChange, wavegroup],
88
+ )
89
+
90
+ const onPause = useCallback(() => {
91
+ setIsPlaying(false)
92
+ onPlayStateChange?.(false)
93
+
94
+ if (wavegroup) {
95
+ wavegroup.setter((prev) => ({
96
+ ...prev,
97
+ isPlaying: false,
98
+ }))
99
+ }
100
+ }, [onPlayStateChange, wavegroup])
101
+
102
+ const onSeeking = useCallback(
103
+ (ws) => {
104
+ if (!wavegroup) return
105
+
106
+ wavegroup.setter((prev) => ({
107
+ ...prev,
108
+ active: ws,
109
+ }))
110
+ },
111
+ [wavegroup],
112
+ )
113
+
114
+ const onFinish = useCallback(() => {
115
+ if (autoRepeat) {
116
+ waveSurferRef.current?.play()
117
+ } else {
118
+ setIsPlaying(false)
119
+ onPlayStateChange?.(false)
120
+
121
+ if (wavegroup) {
122
+ wavegroup.setter((prev) => ({
123
+ ...prev,
124
+ isPlaying: false,
125
+ active: null,
126
+ }))
127
+ }
128
+ }
129
+ }, [autoRepeat, onPlayStateChange, wavegroup])
130
+
131
+ const handleTogglePlay = useCallback(() => {
132
+ if (showSkeleton || !waveSurferRef.current) return
133
+
134
+ if (waveSurferRef.current.isPlaying()) {
135
+ pause()
136
+ return
137
+ }
138
+
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])
162
+
163
+ const handleCardClick = useCallback(() => {
164
+ if (showSkeleton) return
165
+ onSelect?.()
166
+ }, [showSkeleton, onSelect])
167
+
168
+ return (
169
+ <div
170
+ className={classNames(
171
+ styles.previewCard,
172
+ selected && styles.selected,
173
+ isPlaying && styles.playing,
174
+ showSkeleton && styles.skeleton,
175
+ className,
176
+ )}
177
+ onClick={handleCardClick}
178
+ role="button"
179
+ tabIndex={showSkeleton ? -1 : 0}
180
+ onKeyDown={(e) => {
181
+ if (e.key === 'Enter' || e.key === ' ') {
182
+ e.preventDefault()
183
+ handleCardClick()
184
+ }
185
+ }}
186
+ >
187
+ <button
188
+ type="button"
189
+ className={classNames(styles.leftButton, isPlaying && styles.isPlaying)}
190
+ onClick={(e) => {
191
+ e.stopPropagation()
192
+ handleTogglePlay()
193
+ }}
194
+ disabled={showSkeleton}
195
+ aria-label={isPlaying ? 'Pause' : 'Play'}
196
+ >
197
+ <span className={styles.equalizer} aria-hidden="true">
198
+ <span className={styles.equalizerBar} />
199
+ <span className={styles.equalizerBar} />
200
+ <span className={styles.equalizerBar} />
201
+ <span className={styles.equalizerBar} />
202
+ <span className={styles.equalizerBar} />
203
+ </span>
204
+ <PauseIcon
205
+ width={18}
206
+ height={18}
207
+ className={styles.pauseOnHover}
208
+ aria-hidden="true"
209
+ />
210
+ <PlayIcon
211
+ width={18}
212
+ height={18}
213
+ className={styles.playIcon}
214
+ aria-hidden="true"
215
+ />
216
+ </button>
217
+
218
+ <div
219
+ className={styles.waveformContainer}
220
+ onClick={(e) => e.stopPropagation()}
221
+ >
222
+ {audio && (
223
+ <WavesurferPlayer
224
+ url={audio}
225
+ waveColor={waveColor}
226
+ progressColor={progressColor}
227
+ normalize
228
+ barWidth={2}
229
+ barHeight={6}
230
+ cursorWidth={0}
231
+ height={40}
232
+ onReady={onReady}
233
+ onSeeking={onSeeking}
234
+ onPlay={onPlay}
235
+ onPause={onPause}
236
+ onFinish={onFinish}
237
+ backend={isSafari ? 'WebAudio' : 'MediaElement'}
238
+ />
239
+ )}
240
+ {showSkeleton && (
241
+ <span className={styles.skeletonWaveform} aria-hidden="true">
242
+ <span className={styles.skeletonMask} />
243
+ </span>
244
+ )}
245
+ </div>
246
+
247
+ {actions && (
248
+ <div className={styles.actions} onClick={(e) => e.stopPropagation()}>
249
+ {actions}
250
+ </div>
251
+ )}
252
+ </div>
253
+ )
254
+ }
255
+
256
+ PreviewCard.displayName = 'PreviewCard'