@linktr.ee/messaging-react 1.27.0 → 1.28.0-rc-1776231821

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.
Files changed (57) hide show
  1. package/dist/Creator-BFpHsh2u.js +318 -0
  2. package/dist/Creator-BFpHsh2u.js.map +1 -0
  3. package/dist/MediaPlayer-DXz4IBLx.js +313 -0
  4. package/dist/MediaPlayer-DXz4IBLx.js.map +1 -0
  5. package/dist/Visitor-C1Fcrgd6.js +199 -0
  6. package/dist/Visitor-C1Fcrgd6.js.map +1 -0
  7. package/dist/assets/index.css +1 -1
  8. package/dist/index.d.ts +13 -16
  9. package/dist/index.js +27 -35
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -2
  12. package/src/components/CustomMessage/CustomMessage.stories.tsx +1 -1
  13. package/src/components/CustomMessage/index.tsx +0 -1
  14. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +143 -49
  15. package/src/components/LockedAttachment/components/Creator.tsx +406 -114
  16. package/src/components/LockedAttachment/components/MediaPlayer.tsx +162 -80
  17. package/src/components/LockedAttachment/components/Visitor.tsx +205 -145
  18. package/src/components/LockedAttachment/index.tsx +7 -7
  19. package/src/components/LockedAttachment/types.ts +1 -5
  20. package/src/components/LockedAttachment/utils/icons.ts +2 -1
  21. package/src/components/LockedAttachment/utils/mimeType.test.ts +29 -7
  22. package/src/components/LockedAttachment/utils/mimeType.ts +3 -1
  23. package/src/types.ts +0 -1
  24. package/dist/Creator-B6M8dB0U.js +0 -87
  25. package/dist/Creator-B6M8dB0U.js.map +0 -1
  26. package/dist/MediaPlayer-DsjlYGGH.js +0 -539
  27. package/dist/MediaPlayer-DsjlYGGH.js.map +0 -1
  28. package/dist/Preview-DqAv16NS.js +0 -87
  29. package/dist/Preview-DqAv16NS.js.map +0 -1
  30. package/dist/Visitor-CpmFZRGO.js +0 -175
  31. package/dist/Visitor-CpmFZRGO.js.map +0 -1
  32. package/dist/dash.all.min-Duv4lvGS.js +0 -18858
  33. package/dist/dash.all.min-Duv4lvGS.js.map +0 -1
  34. package/dist/hls-Bogc7CBn.js +0 -21710
  35. package/dist/hls-Bogc7CBn.js.map +0 -1
  36. package/dist/index-Da-xN4Yq.js +0 -16142
  37. package/dist/index-Da-xN4Yq.js.map +0 -1
  38. package/dist/index-Dj9rqWcU.js +0 -69
  39. package/dist/index-Dj9rqWcU.js.map +0 -1
  40. package/dist/mixin-B6jYfIcp.js +0 -808
  41. package/dist/mixin-B6jYfIcp.js.map +0 -1
  42. package/dist/react-BxlQMOfz.js +0 -419
  43. package/dist/react-BxlQMOfz.js.map +0 -1
  44. package/dist/react-COAP-MIW.js +0 -377
  45. package/dist/react-COAP-MIW.js.map +0 -1
  46. package/dist/react-Cn4WlMcl.js +0 -3108
  47. package/dist/react-Cn4WlMcl.js.map +0 -1
  48. package/dist/react-CwTJArKY.js +0 -459
  49. package/dist/react-CwTJArKY.js.map +0 -1
  50. package/dist/react-DkfS_atT.js +0 -373
  51. package/dist/react-DkfS_atT.js.map +0 -1
  52. package/dist/react-Pea5fum1.js +0 -286
  53. package/dist/react-Pea5fum1.js.map +0 -1
  54. package/dist/react-RiBbsUDd.js +0 -534
  55. package/dist/react-RiBbsUDd.js.map +0 -1
  56. package/dist/react-dS1WBxxz.js +0 -238
  57. package/dist/react-dS1WBxxz.js.map +0 -1
@@ -1,169 +1,461 @@
1
- import { CheckCircleIcon, EyeIcon, EyeSlashIcon } from '@phosphor-icons/react'
1
+ import {
2
+ CheckCircleIcon,
3
+ EyeIcon,
4
+ EyeSlashIcon,
5
+ PauseIcon,
6
+ PlayIcon,
7
+ XIcon,
8
+ } from '@phosphor-icons/react'
2
9
  import classNames from 'classnames'
3
- import React, { useState } from 'react'
10
+ import React, { useCallback, useEffect, useRef, useState } from 'react'
4
11
 
5
- import type { LockedAttachmentBaseProps, PaymentStatus } from '../types'
12
+ import type { LockedAttachmentBaseProps } from '../types'
6
13
  import { renderTypeIcon } from '../utils/icons'
7
- import { getSourceType, type AttachmentSourceType } from '../utils/mimeType'
14
+ import { getSourceType } from '../utils/mimeType'
8
15
 
9
16
  import MediaPlayer from './MediaPlayer'
10
17
 
11
18
  export interface CreatorCardProps extends LockedAttachmentBaseProps {
12
- title?: string
19
+ isPreview?: boolean
13
20
  placeholderTitle?: string
14
21
  placeholderAmountText?: string
22
+ onDismiss?: () => void
23
+ }
24
+
25
+ interface CloseButtonProps {
26
+ onClose: () => void
15
27
  }
16
28
 
17
- const CloseButton = ({ onClose }: { onClose: () => void }) => (
18
- <button
19
- type="button"
20
- onClick={onClose}
21
- className="absolute right-3 top-3 z-40 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
22
- aria-label="Close preview"
23
- >
24
- <EyeIcon className="size-4" weight="fill" />
25
- </button>
26
- )
29
+ const CloseButton: React.FC<CloseButtonProps> = (props) => {
30
+ const { onClose } = props
31
+ return (
32
+ <button
33
+ type="button"
34
+ onClick={onClose}
35
+ className="absolute left-3 top-3 z-40 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
36
+ aria-label="Close preview"
37
+ >
38
+ <EyeIcon className="size-4" weight="fill" />
39
+ </button>
40
+ )
41
+ }
27
42
 
28
43
  interface CollapsedThumbnailProps {
29
44
  thumbnail?: string
30
45
  mimeType: string
31
- canExpand: boolean
32
- onExpand: () => void
46
+ overlayIcon?: React.ElementType
47
+ onClick?: () => void
33
48
  }
34
49
 
35
- const CollapsedThumbnail: React.FC<CollapsedThumbnailProps> = ({ thumbnail, mimeType, canExpand, onExpand }) => {
50
+ const CollapsedThumbnail: React.FC<CollapsedThumbnailProps> = (props) => {
51
+ const { thumbnail, mimeType, overlayIcon: OverlayIcon, onClick } = props
36
52
  return (
37
53
  <button
38
54
  type="button"
39
- disabled={!canExpand}
40
- className={classNames('relative aspect-video block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none', { 'cursor-pointer': canExpand, 'cursor-default': !canExpand })}
41
- onClick={onExpand}
42
- aria-label={canExpand ? 'Expand attachment preview' : undefined}
55
+ disabled={!onClick}
56
+ className={classNames(
57
+ 'relative aspect-video block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none',
58
+ { 'cursor-pointer': !!onClick, 'cursor-default': !onClick }
59
+ )}
60
+ onClick={onClick}
61
+ aria-label={OverlayIcon ? 'Toggle preview' : undefined}
43
62
  >
44
- {thumbnail
45
- ? <img src={thumbnail} alt="" className="absolute inset-0 h-full w-full object-cover" />
46
- : <div className="absolute inset-0 flex items-center justify-center">{renderTypeIcon(mimeType, { className: 'size-12 text-black/20', weight: 'regular' })}</div>
47
- }
48
- {canExpand && (
49
- <div className="pointer-events-none absolute right-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
50
- <EyeSlashIcon className="size-4" weight="fill" />
63
+ {thumbnail ? (
64
+ <img
65
+ src={thumbnail}
66
+ alt=""
67
+ className="absolute inset-0 h-full w-full object-cover"
68
+ />
69
+ ) : (
70
+ <div className="absolute inset-0 flex items-center justify-center">
71
+ {renderTypeIcon(mimeType, {
72
+ className: 'size-12 text-black/20',
73
+ weight: 'regular',
74
+ })}
75
+ </div>
76
+ )}
77
+ {OverlayIcon && (
78
+ <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
79
+ <OverlayIcon className="size-4" weight="fill" />
51
80
  </div>
52
81
  )}
53
82
  </button>
54
83
  )
55
84
  }
56
85
 
57
- interface ExpandedMediaProps {
58
- source: string
59
- mimeType: string
60
- sourceType: AttachmentSourceType
61
- poster?: string
86
+
87
+ interface AudioPreviewProps {
88
+ source?: string
62
89
  thumbnail?: string
63
- title?: string
64
- onCollapse: () => void
90
+ mimeType: string
65
91
  }
66
92
 
67
- const getExpandedImgSrc = (sourceType: AttachmentSourceType, source: string, poster?: string, thumbnail?: string) =>
68
- sourceType === 'document' ? (poster ?? thumbnail) : source
93
+ const AudioPreview: React.FC<AudioPreviewProps> = (props) => {
94
+ const { source, thumbnail, mimeType } = props
95
+ const [playing, setPlaying] = useState(false)
96
+ const [played, setPlayed] = useState(0)
97
+ const [seeking, setSeeking] = useState(false)
98
+ const audioRef = useRef<HTMLAudioElement>(null)
99
+ const trackRef = useRef<HTMLDivElement>(null)
100
+ const rafRef = useRef<number | null>(null)
101
+
102
+ useEffect(() => {
103
+ const el = audioRef.current
104
+ if (!el) return
105
+ if (playing) {
106
+ void el.play().catch(() => setPlaying(false))
107
+ } else {
108
+ el.pause()
109
+ }
110
+ }, [playing])
111
+
112
+ useEffect(() => {
113
+ if (!playing) {
114
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
115
+ return
116
+ }
117
+ const tick = () => {
118
+ const el = audioRef.current
119
+ if (el && el.duration && !seeking) setPlayed(el.currentTime / el.duration)
120
+ rafRef.current = requestAnimationFrame(tick)
121
+ }
122
+ rafRef.current = requestAnimationFrame(tick)
123
+ return () => {
124
+ if (rafRef.current !== null) cancelAnimationFrame(rafRef.current)
125
+ }
126
+ }, [playing, seeking])
127
+
128
+ useEffect(() => {
129
+ setPlaying(false)
130
+ setPlayed(0)
131
+ }, [source])
132
+
133
+ const getFraction = useCallback(
134
+ (e: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {
135
+ const track = trackRef.current
136
+ if (!track) return 0
137
+ const clientX =
138
+ 'touches' in e
139
+ ? (e.touches[0]?.clientX ?? e.changedTouches[0]?.clientX ?? 0)
140
+ : e.clientX
141
+ const rect = track.getBoundingClientRect()
142
+ return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
143
+ },
144
+ []
145
+ )
146
+
147
+ const seekTo = useCallback((fraction: number) => {
148
+ const el = audioRef.current
149
+ if (el && el.duration) el.currentTime = fraction * el.duration
150
+ }, [])
151
+
152
+ useEffect(() => {
153
+ if (!seeking) return
154
+ const onMove = (e: MouseEvent | TouchEvent) => {
155
+ const f = getFraction(e)
156
+ setPlayed(f)
157
+ seekTo(f)
158
+ }
159
+ const onUp = (e: MouseEvent | TouchEvent) => {
160
+ setSeeking(false)
161
+ seekTo(getFraction(e))
162
+ }
163
+ window.addEventListener('mousemove', onMove)
164
+ window.addEventListener('mouseup', onUp)
165
+ window.addEventListener('touchmove', onMove, { passive: true })
166
+ window.addEventListener('touchend', onUp)
167
+ return () => {
168
+ window.removeEventListener('mousemove', onMove)
169
+ window.removeEventListener('mouseup', onUp)
170
+ window.removeEventListener('touchmove', onMove)
171
+ window.removeEventListener('touchend', onUp)
172
+ }
173
+ }, [seeking, getFraction, seekTo])
174
+
175
+ const toggle = useCallback(() => setPlaying((p) => !p), [])
69
176
 
70
- const ExpandedMedia: React.FC<ExpandedMediaProps> = ({ source, mimeType, sourceType, poster, thumbnail, title, onCollapse }) => {
71
- if (sourceType === 'video' || sourceType === 'audio') {
72
- return (
73
- <div className="relative">
74
- <MediaPlayer
75
- source={source}
76
- mimeType={mimeType}
77
- poster={poster ?? thumbnail}
78
- autoPlay
79
- loop
80
- controls={false}
81
- showProgress
82
- onContainerClick={onCollapse}
83
- muted={sourceType === 'video'}
84
- />
85
- <CloseButton onClose={onCollapse} />
86
- </div>
87
- )
88
- }
89
- const imgSrc = getExpandedImgSrc(sourceType, source, poster, thumbnail)
90
177
  return (
91
178
  <div className="relative">
92
- <button type="button" className="block w-full cursor-pointer border-0 p-0 text-left appearance-none" onClick={onCollapse} aria-label="Close preview">
93
- <img src={imgSrc} alt={title ?? ''} className="block w-full" />
94
- </button>
95
- <CloseButton onClose={onCollapse} />
179
+ {source && (
180
+ <audio
181
+ ref={audioRef}
182
+ src={source}
183
+ loop
184
+ onEnded={() => {
185
+ setPlaying(false)
186
+ setPlayed(0)
187
+ }}
188
+ >
189
+ <track kind="captions" />
190
+ </audio>
191
+ )}
192
+ <CollapsedThumbnail
193
+ thumbnail={thumbnail}
194
+ mimeType={mimeType}
195
+ overlayIcon={source ? (playing ? PauseIcon : PlayIcon) : undefined}
196
+ onClick={source ? toggle : undefined}
197
+ />
198
+ {source && (
199
+ <div className="absolute inset-x-0 bottom-0 px-3 pb-2.5 pt-6 bg-gradient-to-t from-black/40 to-transparent">
200
+ <div
201
+ ref={trackRef}
202
+ role="slider"
203
+ aria-label="Playback position"
204
+ aria-valuenow={Math.round(played * 100)}
205
+ aria-valuemin={0}
206
+ aria-valuemax={100}
207
+ tabIndex={0}
208
+ className="relative flex h-4 w-full cursor-pointer items-center"
209
+ onMouseDown={(e) => {
210
+ e.stopPropagation()
211
+ setSeeking(true)
212
+ const f = getFraction(e)
213
+ setPlayed(f)
214
+ seekTo(f)
215
+ }}
216
+ onTouchStart={(e) => {
217
+ e.stopPropagation()
218
+ setSeeking(true)
219
+ const f = getFraction(e)
220
+ setPlayed(f)
221
+ seekTo(f)
222
+ }}
223
+ onClick={(e) => e.stopPropagation()}
224
+ onKeyDown={(e) => {
225
+ if (e.key === 'ArrowRight') seekTo(Math.min(1, played + 0.05))
226
+ if (e.key === 'ArrowLeft') seekTo(Math.max(0, played - 0.05))
227
+ }}
228
+ >
229
+ <div className="w-full overflow-hidden rounded-full bg-white/30 h-1">
230
+ <div
231
+ className="h-full rounded-full bg-white"
232
+ style={{ width: `${Math.round(played * 100)}%` }}
233
+ />
234
+ </div>
235
+ </div>
236
+ </div>
237
+ )}
96
238
  </div>
97
239
  )
98
240
  }
99
241
 
100
- interface CreatorCardMetaProps {
101
- title?: string
102
- placeholderTitle: string
242
+ interface VideoPreviewProps {
243
+ source?: string
244
+ thumbnail?: string
103
245
  mimeType: string
104
- detail?: string
105
- paymentStatus?: PaymentStatus
106
- displayAmountText?: string
107
- isPlaceholderAmount: boolean
108
246
  }
109
247
 
110
- const CreatorCardMeta: React.FC<CreatorCardMetaProps> = ({ title, placeholderTitle, mimeType, detail, paymentStatus, displayAmountText, isPlaceholderAmount }) => {
248
+ const VideoPreview: React.FC<VideoPreviewProps> = (props) => {
249
+ const { source, thumbnail, mimeType } = props
250
+ const [expanded, setExpanded] = useState(false)
251
+ const collapse = () => setExpanded(false)
252
+
253
+ useEffect(() => {
254
+ setExpanded(false)
255
+ }, [source])
256
+
257
+ if (!source) {
258
+ return <CollapsedThumbnail thumbnail={thumbnail} mimeType={mimeType} />
259
+ }
260
+
111
261
  return (
112
- <div className="px-4 pb-3 pt-3">
113
- <p className={classNames('mb-1.5 truncate text-base font-medium', { 'text-black/30': !title, 'text-black': !!title })}>
114
- {title || placeholderTitle}
115
- </p>
116
- <div className="flex items-center gap-1">
117
- {renderTypeIcon(mimeType, { className: 'size-5 shrink-0 text-black/55', weight: 'regular' })}
118
- {detail && <span className="text-xs font-medium text-black/55">{detail}</span>}
119
- {paymentStatus === 'paid' ? (
120
- <>
121
- <span className="text-xs font-medium text-black/55">•</span>
122
- <span className="text-xs font-medium text-[#008236]">Purchased</span>
123
- <CheckCircleIcon className="size-4 text-[#008236]" weight="bold" />
124
- </>
125
- ) : displayAmountText && (
126
- <>
127
- <span className={classNames('text-xs font-medium', { 'text-black/30': isPlaceholderAmount, 'text-black/55': !isPlaceholderAmount })}>•</span>
128
- <span className={classNames('text-xs font-medium', { 'text-black/30': isPlaceholderAmount, 'text-black/55': !isPlaceholderAmount })}>{displayAmountText}</span>
129
- </>
130
- )}
131
- </div>
262
+ <div
263
+ className={classNames('relative overflow-hidden', {
264
+ 'aspect-video': !expanded,
265
+ })}
266
+ >
267
+ <MediaPlayer
268
+ source={source}
269
+ mimeType={mimeType}
270
+ poster={thumbnail}
271
+ playing={expanded}
272
+ loop
273
+ controls={false}
274
+ showProgress
275
+ onContainerClick={collapse}
276
+ muted
277
+ />
278
+ {!expanded && (
279
+ <button
280
+ type="button"
281
+ className="absolute inset-0 block cursor-pointer border-0 p-0 text-left appearance-none"
282
+ onClick={() => setExpanded(true)}
283
+ aria-label="Expand video preview"
284
+ >
285
+ {thumbnail ? (
286
+ <img
287
+ src={thumbnail}
288
+ alt=""
289
+ className="absolute inset-0 h-full w-full object-cover"
290
+ />
291
+ ) : (
292
+ <div className="absolute inset-0 flex items-center justify-center">
293
+ {renderTypeIcon(mimeType, {
294
+ className: 'size-12 text-black/20',
295
+ weight: 'regular',
296
+ })}
297
+ </div>
298
+ )}
299
+ <div className="pointer-events-none absolute left-3 top-3 flex size-8 items-center justify-center rounded-full bg-black/60 text-white">
300
+ <EyeSlashIcon className="size-4" weight="fill" />
301
+ </div>
302
+ </button>
303
+ )}
304
+ {expanded && <CloseButton onClose={collapse} />}
132
305
  </div>
133
306
  )
134
307
  }
135
308
 
136
- const CreatorCard: React.FC<CreatorCardProps> = ({
137
- title,
138
- mimeType = 'application/octet-stream',
139
- thumbnail,
140
- poster,
141
- source,
142
- detail,
143
- amountText,
144
- placeholderTitle = 'Attachment title',
145
- placeholderAmountText,
146
- paymentStatus,
147
- }) => {
148
- const sourceType = getSourceType(mimeType)
309
+ interface ImagePreviewProps {
310
+ source?: string
311
+ thumbnail?: string
312
+ mimeType: string
313
+ title?: string
314
+ }
315
+
316
+ const ImagePreview: React.FC<ImagePreviewProps> = (props) => {
317
+ const { source, thumbnail, mimeType, title } = props
149
318
  const [expanded, setExpanded] = useState(false)
319
+ const collapse = () => setExpanded(false)
150
320
 
151
- const displayAmountText = placeholderAmountText ?? amountText
152
- const isPlaceholderAmount = !!placeholderAmountText
153
- const canExpand = sourceType === 'document'
154
- ? !!(source && (poster || thumbnail))
155
- : !!source
321
+ useEffect(() => {
322
+ setExpanded(false)
323
+ }, [source])
156
324
 
157
- const collapse = () => setExpanded(false)
325
+ if (expanded && source) {
326
+ return (
327
+ <div className="relative">
328
+ <button
329
+ type="button"
330
+ className="block w-full cursor-pointer border-0 p-0 text-left appearance-none"
331
+ onClick={collapse}
332
+ aria-label="Close preview"
333
+ >
334
+ <img src={source} alt={title ?? ''} className="block w-full" />
335
+ </button>
336
+ <CloseButton onClose={collapse} />
337
+ </div>
338
+ )
339
+ }
340
+
341
+ return (
342
+ <CollapsedThumbnail
343
+ thumbnail={thumbnail}
344
+ mimeType={mimeType}
345
+ overlayIcon={source ? EyeSlashIcon : undefined}
346
+ onClick={source ? () => setExpanded(true) : undefined}
347
+ />
348
+ )
349
+ }
158
350
 
159
- const mediaPreview = expanded && source
160
- ? <ExpandedMedia source={source} mimeType={mimeType} sourceType={sourceType} poster={poster} thumbnail={thumbnail} title={title} onCollapse={collapse} />
161
- : <CollapsedThumbnail thumbnail={thumbnail} mimeType={mimeType} canExpand={canExpand} onExpand={() => setExpanded(true)} />
351
+
352
+ const CreatorCard: React.FC<CreatorCardProps> = (props) => {
353
+ const {
354
+ title,
355
+ mimeType = 'application/octet-stream',
356
+ thumbnail,
357
+ source,
358
+ detail,
359
+ amountText,
360
+ placeholderTitle = 'Attachment title',
361
+ placeholderAmountText,
362
+ paymentStatus,
363
+ onDismiss,
364
+ isPreview = false,
365
+ } = props
366
+ const sourceType = getSourceType(mimeType)
367
+ const displayAmountText = amountText ?? placeholderAmountText
368
+ const isPlaceholderAmount = !amountText && !!placeholderAmountText
369
+
370
+ let mediaPreview: React.ReactNode
371
+ if (isPreview && sourceType === 'audio') {
372
+ mediaPreview = (
373
+ <AudioPreview source={source} thumbnail={thumbnail} mimeType={mimeType} />
374
+ )
375
+ } else if (isPreview && sourceType === 'video') {
376
+ mediaPreview = (
377
+ <VideoPreview source={source} thumbnail={thumbnail} mimeType={mimeType} />
378
+ )
379
+ } else if (isPreview && sourceType === 'image') {
380
+ mediaPreview = (
381
+ <ImagePreview
382
+ source={source}
383
+ thumbnail={thumbnail}
384
+ mimeType={mimeType}
385
+ title={title}
386
+ />
387
+ )
388
+ } else {
389
+ mediaPreview = (
390
+ <CollapsedThumbnail thumbnail={thumbnail} mimeType={mimeType} />
391
+ )
392
+ }
162
393
 
163
394
  return (
164
- <div className="w-[280px] overflow-hidden rounded-3xl bg-white shadow-[0px_0px_0px_1px_rgba(0,0,0,0.04),0px_1px_2px_0px_rgba(0,0,0,0.04),0px_8px_32px_0px_rgba(0,0,0,0.1)]">
395
+ <div className="relative w-[280px] select-none overflow-hidden rounded-3xl bg-white shadow-card">
396
+ {onDismiss && (
397
+ <button
398
+ type="button"
399
+ onClick={onDismiss}
400
+ className="absolute right-3 top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white"
401
+ aria-label="Dismiss attachment"
402
+ >
403
+ <XIcon className="size-4" weight="bold" />
404
+ </button>
405
+ )}
165
406
  {mediaPreview}
166
- <CreatorCardMeta title={title} placeholderTitle={placeholderTitle} mimeType={mimeType} detail={detail} paymentStatus={paymentStatus} displayAmountText={displayAmountText} isPlaceholderAmount={isPlaceholderAmount} />
407
+ <div className="px-4 pb-3 pt-3">
408
+ <p
409
+ className={classNames('mb-1.5 truncate text-base font-medium', {
410
+ 'text-black/30': !title,
411
+ 'text-black': !!title,
412
+ })}
413
+ >
414
+ {title || placeholderTitle}
415
+ </p>
416
+ <div className="flex items-center gap-1">
417
+ {renderTypeIcon(mimeType, {
418
+ className: 'size-5 shrink-0 text-black/55',
419
+ weight: 'regular',
420
+ })}
421
+ {detail && (
422
+ <span className="text-xs font-medium text-black/55">{detail}</span>
423
+ )}
424
+ {paymentStatus === 'paid' ? (
425
+ <>
426
+ <span className="text-xs font-medium text-black/55">•</span>
427
+ <span className="text-xs font-medium text-[#008236]">
428
+ Purchased
429
+ </span>
430
+ <CheckCircleIcon
431
+ className="size-4 text-[#008236]"
432
+ weight="bold"
433
+ />
434
+ </>
435
+ ) : (
436
+ displayAmountText && (
437
+ <>
438
+ <span
439
+ className={classNames('text-xs font-medium', {
440
+ 'text-black/30': isPlaceholderAmount,
441
+ 'text-black/55': !isPlaceholderAmount,
442
+ })}
443
+ >
444
+
445
+ </span>
446
+ <span
447
+ className={classNames('text-xs font-medium', {
448
+ 'text-black/30': isPlaceholderAmount,
449
+ 'text-black/55': !isPlaceholderAmount,
450
+ })}
451
+ >
452
+ {displayAmountText}
453
+ </span>
454
+ </>
455
+ )
456
+ )}
457
+ </div>
458
+ </div>
167
459
  </div>
168
460
  )
169
461
  }