@linktr.ee/messaging-react 1.33.3 → 1.35.0-rc-1777516745

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 (33) hide show
  1. package/dist/Card-BXKUE7JN.js +195 -0
  2. package/dist/Card-BXKUE7JN.js.map +1 -0
  3. package/dist/Card-J-rj8Q6w.js +167 -0
  4. package/dist/Card-J-rj8Q6w.js.map +1 -0
  5. package/dist/assets/index.css +1 -1
  6. package/dist/{index-Ydi1pTAi.js → index-B3H4GLek.js} +1067 -987
  7. package/dist/index-B3H4GLek.js.map +1 -0
  8. package/dist/index.d.ts +5 -2
  9. package/dist/index.js +1 -1
  10. package/package.json +1 -1
  11. package/src/components/Avatar/Avatar.stories.tsx +29 -0
  12. package/src/components/Avatar/index.tsx +43 -20
  13. package/src/components/ChannelView.stories.tsx +45 -1
  14. package/src/components/ChannelView.tsx +9 -5
  15. package/src/components/CustomMessage/CustomMessage.stories.tsx +75 -15
  16. package/src/components/CustomMessage/MessageTag.stories.tsx +1 -1
  17. package/src/components/CustomMessage/index.tsx +8 -5
  18. package/src/components/CustomTypingIndicator/CustomTypingIndicator.stories.tsx +114 -0
  19. package/src/components/CustomTypingIndicator/index.tsx +101 -0
  20. package/src/components/LockedAttachment/LockedAttachment.stories.tsx +94 -66
  21. package/src/components/LockedAttachment/components/Creator/Card.tsx +55 -23
  22. package/src/components/LockedAttachment/components/Creator/CardThumbnail.tsx +2 -2
  23. package/src/components/LockedAttachment/components/Visitor/Card.tsx +9 -16
  24. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +3 -12
  25. package/src/components/LockedAttachment/components/Visitor/CardThumbnail.tsx +2 -2
  26. package/src/components/MediaMessage/MediaMessage.stories.tsx +77 -71
  27. package/src/components/MediaMessage/index.tsx +1 -1
  28. package/src/styles.css +27 -5
  29. package/dist/Card-BfA8wq8O.js +0 -181
  30. package/dist/Card-BfA8wq8O.js.map +0 -1
  31. package/dist/Card-Bpud_enW.js +0 -177
  32. package/dist/Card-Bpud_enW.js.map +0 -1
  33. package/dist/index-Ydi1pTAi.js.map +0 -1
@@ -0,0 +1,101 @@
1
+ import React from 'react'
2
+ import type { Event } from 'stream-chat'
3
+ import {
4
+ useChannelStateContext,
5
+ useChatContext,
6
+ useTypingContext,
7
+ } from 'stream-chat-react'
8
+
9
+ import { Avatar } from '../Avatar'
10
+
11
+ interface CustomTypingIndicatorProps {
12
+ threadList?: boolean
13
+ }
14
+
15
+ const Circle = ({ cx, index }: { cx: string; index: number }) => (
16
+ <circle className="fill-pebble" cx={cx} cy="4" r="3.9">
17
+ <animateTransform
18
+ attributeName="transform"
19
+ type="translate"
20
+ values="0 0; 0 -2.25; 0 0;"
21
+ dur="900ms"
22
+ begin={`${120 * index}ms`} // 0ms, 120ms, 240ms
23
+ repeatCount="indefinite"
24
+ />
25
+ </circle>
26
+ )
27
+
28
+ const CustomTypingIndicator = ({ threadList }: CustomTypingIndicatorProps) => {
29
+ const { channel, channelConfig, thread } = useChannelStateContext()
30
+ const { client } = useChatContext()
31
+ const { typing = {} } = useTypingContext()
32
+
33
+ if (channelConfig?.typing_events === false) {
34
+ return null
35
+ }
36
+
37
+ const typingInChannel = !threadList
38
+ ? Object.values(typing).filter(
39
+ ({ parent_id, user }: Event) =>
40
+ user?.id !== client.user?.id && !parent_id
41
+ )
42
+ : []
43
+
44
+ const typingInThread = threadList
45
+ ? Object.values(typing).filter(
46
+ ({ parent_id, user }: Event) =>
47
+ user?.id !== client.user?.id && parent_id === thread?.id
48
+ )
49
+ : []
50
+
51
+ const typingUsers = threadList ? typingInThread : typingInChannel
52
+ if (!typingUsers.length) {
53
+ return null
54
+ }
55
+
56
+ const typingUser = typingUsers[0]?.user
57
+ const memberUser =
58
+ typingUser?.id && channel.state.members[typingUser.id]
59
+ ? channel.state.members[typingUser.id].user
60
+ : undefined
61
+
62
+ const avatarId = typingUser?.id ?? memberUser?.id ?? 'typing-user'
63
+ const avatarName =
64
+ typingUser?.name ?? memberUser?.name ?? typingUser?.id ?? 'Typing user'
65
+ const avatarImage = typingUser?.image ?? memberUser?.image
66
+
67
+ return (
68
+ <div
69
+ className="str-chat__typing-indicator !items-end"
70
+ data-testid="typing-indicator"
71
+ style={{ insetInlineStart: 0, insetInlineEnd: 'auto' }}
72
+ >
73
+ <div className="shrink-0" aria-hidden="true">
74
+ <Avatar
75
+ id={avatarId}
76
+ name={avatarName}
77
+ image={avatarImage}
78
+ size={24}
79
+ shape="circle"
80
+ />
81
+ </div>
82
+
83
+ <div className="px-4 py-3 rounded-lg bg-[#F1F0EE] h-12 flex flex-col justify-end">
84
+ <svg
85
+ aria-hidden="true"
86
+ className="block overflow-visible"
87
+ viewBox="0 0 32 8"
88
+ width="32"
89
+ height="8"
90
+ overflow="visible"
91
+ >
92
+ <Circle cx="4" index={0} />
93
+ <Circle cx="16" index={1} />
94
+ <Circle cx="28" index={2} />
95
+ </svg>
96
+ </div>
97
+ </div>
98
+ )
99
+ }
100
+
101
+ export default CustomTypingIndicator
@@ -20,8 +20,8 @@ const AUDIO_THUMBNAIL = '/audio-thumbnail.jpg'
20
20
  const AUDIO_SOURCE = '/audio-source.mp3'
21
21
 
22
22
  const meta: Meta = {
23
- title: 'Components/LockedAttachment',
24
- parameters: { layout: 'centered' },
23
+ title: 'LockedAttachment',
24
+ parameters: { layout: 'fullscreen' },
25
25
  }
26
26
  export default meta
27
27
 
@@ -62,10 +62,19 @@ const VARIANTS = [
62
62
  thumbnailUnlockedUrl: DOCUMENT_THUMBNAIL,
63
63
  sourceUrl: DOCUMENT_SOURCE,
64
64
  },
65
+ {
66
+ label: 'Unknown',
67
+ title: 'Unknown Attachment',
68
+ mimeType: 'application/octet-stream',
69
+ detail: '2.4 MB',
70
+ thumbnailUrl: undefined,
71
+ thumbnailUnlockedUrl: undefined,
72
+ sourceUrl: DOCUMENT_SOURCE,
73
+ },
65
74
  ]
66
75
 
67
76
  const Table = ({ children }: { children: React.ReactNode }) => (
68
- <div className="p-12">
77
+ <div className="min-h-screen w-full p-12 bg-[#F9F7F4]">
69
78
  <table className="border-separate border-spacing-4">{children}</table>
70
79
  </div>
71
80
  )
@@ -90,8 +99,9 @@ const TableHead = ({
90
99
  </thead>
91
100
  )
92
101
 
93
- export const Visitor: StoryFn = () => {
102
+ export const Received: StoryFn = () => {
94
103
  const [isPaid, setPaid] = useState<string | undefined>()
104
+ const [isUnlocking, setUnlocking] = useState<string | undefined>()
95
105
 
96
106
  return (
97
107
  <Table>
@@ -118,38 +128,24 @@ export const Visitor: StoryFn = () => {
118
128
  detail={detail}
119
129
  paymentStatus={isPaid === mimeType ? 'paid' : undefined}
120
130
  amountText="AU$9.99"
121
- onUnlockClick={() => setPaid(mimeType)}
122
- onDownloadClick={() => alert('Download clicked')}
123
- onFetchSource={async () => {
124
- return Promise.resolve({
125
- sourceUrl: sourceUrl,
126
- thumbnailUrl: thumbnailUnlockedUrl,
127
- })
131
+ isUnlocking={isUnlocking === mimeType}
132
+ onUnlockClick={() => {
133
+ setUnlocking(mimeType)
134
+ setTimeout(() => {
135
+ setUnlocking(undefined)
136
+ setPaid(mimeType)
137
+ }, 1500)
128
138
  }}
139
+ onDownloadClick={() => alert('Download clicked')}
140
+ onFetchSource={async () => ({
141
+ sourceUrl: sourceUrl,
142
+ thumbnailUrl: thumbnailUnlockedUrl,
143
+ })}
129
144
  />
130
145
  </td>
131
146
  )
132
147
  )}
133
148
  </tr>
134
- <tr>
135
- <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
136
- Purchased
137
- </td>
138
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
139
- <td key={mimeType} className="align-top">
140
- <LockedAttachment.Visitor
141
- title={title}
142
- thumbnailUrl={thumbnailUrl}
143
- mimeType={mimeType}
144
- detail={detail}
145
- amountText="AU$9.99"
146
- paymentStatus="paid"
147
- onUnlockClick={() => alert('Unlock clicked')}
148
- onDownloadClick={() => alert('Download clicked')}
149
- />
150
- </td>
151
- ))}
152
- </tr>
153
149
  <tr>
154
150
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
155
151
  Unlocked
@@ -171,15 +167,12 @@ export const Visitor: StoryFn = () => {
171
167
  detail={detail}
172
168
  amountText="AU$9.99"
173
169
  paymentStatus="paid"
174
- onUnlockClick={() => console.log('Unlock clicked')}
170
+ onUnlockClick={() => alert('Unlock clicked')}
175
171
  onDownloadClick={() => alert('Download clicked')}
176
- onFetchSource={async () => {
177
- await new Promise((resolve) => setTimeout(resolve, 500))
178
- return Promise.resolve({
179
- sourceUrl: sourceUrl,
180
- thumbnailUrl: thumbnailUnlockedUrl,
181
- })
182
- }}
172
+ onFetchSource={async () => ({
173
+ sourceUrl: sourceUrl,
174
+ thumbnailUrl: thumbnailUnlockedUrl,
175
+ })}
183
176
  />
184
177
  </td>
185
178
  )
@@ -190,7 +183,7 @@ export const Visitor: StoryFn = () => {
190
183
  )
191
184
  }
192
185
 
193
- export const Creator: StoryFn = () => (
186
+ export const Sent: StoryFn = () => (
194
187
  <Table>
195
188
  <TableHead variants={VARIANTS} />
196
189
  <tbody>
@@ -213,10 +206,13 @@ export const Creator: StoryFn = () => (
213
206
  thumbnailUrl={thumbnailUrl}
214
207
  mimeType={mimeType}
215
208
  detail={detail}
216
- onPreviewClick={() => ({
217
- sourceUrl: sourceUrl,
218
- thumbnailUrl: thumbnailUnlockedUrl,
219
- })}
209
+ onFetchSource={async () => {
210
+ await new Promise((resolve) => setTimeout(resolve, 1500))
211
+ return {
212
+ sourceUrl: sourceUrl,
213
+ thumbnailUrl: thumbnailUnlockedUrl,
214
+ }
215
+ }}
220
216
  />
221
217
  </td>
222
218
  )
@@ -243,34 +239,66 @@ export const Creator: StoryFn = () => (
243
239
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
244
240
  Sent
245
241
  </td>
246
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
247
- <td key={mimeType} className="align-top">
248
- <LockedAttachment.Creator
249
- title={title}
250
- thumbnailUrl={thumbnailUrl}
251
- mimeType={mimeType}
252
- detail={detail}
253
- amountText="AU$9.99"
254
- />
255
- </td>
256
- ))}
242
+ {VARIANTS.map(
243
+ ({
244
+ title,
245
+ mimeType,
246
+ detail,
247
+ thumbnailUrl,
248
+ thumbnailUnlockedUrl,
249
+ sourceUrl,
250
+ }) => (
251
+ <td key={mimeType} className="align-top">
252
+ <LockedAttachment.Creator
253
+ title={title}
254
+ thumbnailUrl={thumbnailUrl}
255
+ mimeType={mimeType}
256
+ detail={detail}
257
+ amountText="AU$9.99"
258
+ onFetchSource={async () => {
259
+ await new Promise((resolve) => setTimeout(resolve, 1500))
260
+ return {
261
+ sourceUrl: sourceUrl,
262
+ thumbnailUrl: thumbnailUnlockedUrl,
263
+ }
264
+ }}
265
+ />
266
+ </td>
267
+ )
268
+ )}
257
269
  </tr>
258
270
  <tr>
259
271
  <td className="text-xs text-right font-medium text-black/40 pr-4 align-top pt-2">
260
272
  Sold
261
273
  </td>
262
- {VARIANTS.map(({ title, mimeType, detail, thumbnailUrl }) => (
263
- <td key={mimeType} className="align-top">
264
- <LockedAttachment.Creator
265
- title={title}
266
- thumbnailUrl={thumbnailUrl}
267
- mimeType={mimeType}
268
- detail={detail}
269
- amountText="AU$9.99"
270
- paymentStatus="paid"
271
- />
272
- </td>
273
- ))}
274
+ {VARIANTS.map(
275
+ ({
276
+ title,
277
+ mimeType,
278
+ detail,
279
+ thumbnailUrl,
280
+ thumbnailUnlockedUrl,
281
+ sourceUrl,
282
+ }) => (
283
+ <td key={mimeType} className="align-top">
284
+ <LockedAttachment.Creator
285
+ title={title}
286
+ thumbnailUrl={thumbnailUrl}
287
+ mimeType={mimeType}
288
+ detail={detail}
289
+ amountText="AU$9.99"
290
+ paymentStatus="paid"
291
+ onFetchSource={async () => {
292
+ await new Promise((resolve) => setTimeout(resolve, 1500))
293
+ return {
294
+ sourceUrl: sourceUrl,
295
+ thumbnailUrl: thumbnailUnlockedUrl,
296
+ }
297
+ }}
298
+ />
299
+ </td>
300
+ )
301
+ )}
274
302
  </tr>
275
303
  </tbody>
276
304
  </Table>
@@ -7,7 +7,7 @@ import {
7
7
  XIcon,
8
8
  } from '@phosphor-icons/react'
9
9
  import classNames from 'classnames'
10
- import React, { useCallback, useState } from 'react'
10
+ import React, { useCallback, useRef, useState } from 'react'
11
11
 
12
12
  import type {
13
13
  LockedAttachmentBaseProps,
@@ -21,8 +21,10 @@ import CardThumbnail from './CardThumbnail'
21
21
  export interface CreatorCardProps extends LockedAttachmentBaseProps {
22
22
  placeholderTitle?: string
23
23
  placeholderAmountText?: string
24
+ isUnlocking?: boolean
24
25
  onDismiss?: () => void
25
- onPreviewClick?: () => LockedAttachmentSource
26
+ onPreviewClick?: () => void
27
+ onFetchSource?: () => Promise<LockedAttachmentSource | void>
26
28
  }
27
29
 
28
30
  const CreatorCard: React.FC<CreatorCardProps> = ({
@@ -34,29 +36,56 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
34
36
  placeholderTitle = 'Attachment title',
35
37
  placeholderAmountText,
36
38
  paymentStatus,
39
+ isUnlocking,
37
40
  onDismiss,
38
41
  onPreviewClick,
42
+ onFetchSource,
39
43
  }) => {
40
44
  const [source, setSource] = useState<LockedAttachmentSource | undefined>()
41
-
42
- const effectiveSourceUrl = source?.sourceUrl
43
- const effectiveThumbnailUrl = source?.thumbnailUrl ?? thumbnailUrl
44
-
45
- const handleToggle = useCallback(() => {
45
+ const [isPreviewVisible, setIsPreviewVisible] = useState(false)
46
+ const [isLoadingPreview, setLoadingPreview] = useState(false)
47
+ const fetchingRef = useRef(false)
48
+
49
+ const effectiveSourceUrl = isPreviewVisible ? source?.sourceUrl : undefined
50
+ const effectiveThumbnailUrl = isPreviewVisible ? (source?.thumbnailUrl ?? thumbnailUrl) : thumbnailUrl
51
+ const isBusy = isLoadingPreview || isUnlocking
52
+
53
+ const handleToggle = useCallback(async () => {
54
+ onPreviewClick?.()
55
+ if (isPreviewVisible) {
56
+ setIsPreviewVisible(false)
57
+ return
58
+ }
46
59
  if (source) {
47
- setSource(undefined)
48
- } else if (onPreviewClick) {
49
- setSource(onPreviewClick())
60
+ setIsPreviewVisible(true)
61
+ return
50
62
  }
51
- }, [source, onPreviewClick])
63
+ if (!onFetchSource) return
64
+ if (fetchingRef.current) return
65
+ fetchingRef.current = true
66
+ setLoadingPreview(true)
67
+ try {
68
+ const result = await onFetchSource()
69
+ if (result) {
70
+ setSource(result)
71
+ setIsPreviewVisible(true)
72
+ }
73
+ } finally {
74
+ fetchingRef.current = false
75
+ setLoadingPreview(false)
76
+ }
77
+ }, [isPreviewVisible, source, onPreviewClick, onFetchSource])
78
+
79
+ const toggleHandler = onFetchSource || onPreviewClick ? handleToggle : undefined
52
80
 
53
81
  return (
54
- <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
82
+ <div className="relative w-[280px] select-none overflow-hidden rounded-[24px] bg-[#121110] shadow-[0_0_0_1px_rgba(0,0,0,0.04),0_4px_8px_rgba(0,0,0,0.06)]">
55
83
  <CardHeader
56
84
  onDismiss={onDismiss}
57
- onToggle={onPreviewClick ? handleToggle : undefined}
58
- isExpanded={!!source}
85
+ onToggle={isBusy ? undefined : toggleHandler}
86
+ isExpanded={isPreviewVisible}
59
87
  paymentStatus={paymentStatus}
88
+ isLoading={isBusy}
60
89
  />
61
90
 
62
91
  <CardThumbnail
@@ -64,12 +93,12 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
64
93
  sourceUrl={effectiveSourceUrl}
65
94
  thumbnailUrl={effectiveThumbnailUrl}
66
95
  mimeType={mimeType}
67
- onToggle={onPreviewClick ? handleToggle : undefined}
96
+ onToggle={isBusy ? undefined : toggleHandler}
68
97
  />
69
98
 
70
- <div className="px-4 pb-3 pt-3 bg-black">
99
+ <div className="px-4 pb-3 pt-3">
71
100
  <p
72
- className={classNames('mb-1.5 truncate text-base font-medium', {
101
+ className={classNames('mb-0.5 truncate text-base font-medium', {
73
102
  'text-white/30': !title,
74
103
  'text-white': !!title,
75
104
  })}
@@ -90,11 +119,8 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
90
119
  {paymentStatus === 'paid' ? (
91
120
  <React.Fragment>
92
121
  <span className="text-xs font-medium text-white/55">&bull;</span>
93
- <span className="text-xs font-medium text-[#4ade80]">Sold</span>
94
- <CheckCircleIcon
95
- className="size-4 text-[#4ade80]"
96
- weight="bold"
97
- />
122
+ <span className="text-xs font-medium text-[#34c759]">Sold</span>
123
+ <CheckCircleIcon className="size-4 text-[#34c759]" weight="bold" />
98
124
  </React.Fragment>
99
125
  ) : (
100
126
  <React.Fragment>
@@ -120,6 +146,7 @@ interface CardHeaderProps {
120
146
  onToggle?: () => void
121
147
  isExpanded?: boolean
122
148
  paymentStatus?: PaymentStatus
149
+ isLoading?: boolean
123
150
  }
124
151
 
125
152
  const CardHeader: React.FC<CardHeaderProps> = ({
@@ -127,6 +154,7 @@ const CardHeader: React.FC<CardHeaderProps> = ({
127
154
  onToggle,
128
155
  isExpanded,
129
156
  paymentStatus,
157
+ isLoading,
130
158
  }) => {
131
159
  if (onDismiss) {
132
160
  return (
@@ -160,7 +188,11 @@ const CardHeader: React.FC<CardHeaderProps> = ({
160
188
 
161
189
  return (
162
190
  <div className="absolute top-3 z-50 flex size-8 items-center justify-center rounded-full bg-black/60 text-white left-3">
163
- <Icon className="size-4" weight="fill" />
191
+ {isLoading ? (
192
+ <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
193
+ ) : (
194
+ <Icon className="size-4" weight="fill" />
195
+ )}
164
196
  </div>
165
197
  )
166
198
  }
@@ -27,7 +27,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
27
27
  type="button"
28
28
  disabled={!onToggle}
29
29
  className={classNames(
30
- 'relative block w-full overflow-hidden border-0 bg-black/5 p-0 text-left appearance-none',
30
+ 'relative block w-full overflow-hidden border-0 bg-white/10 p-0 text-left appearance-none',
31
31
  { 'cursor-pointer': !!onToggle, 'cursor-default': !onToggle }
32
32
  )}
33
33
  onClick={onToggle}
@@ -51,7 +51,7 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
51
51
  ) : (
52
52
  <div className="aspect-video flex items-center justify-center">
53
53
  {renderTypeIcon(mimeType, {
54
- className: 'size-12 text-black/20',
54
+ className: 'size-12 text-white/20',
55
55
  weight: 'regular',
56
56
  })}
57
57
  </div>
@@ -111,37 +111,30 @@ const VisitorCard: React.FC<VisitorCardProps> = ({
111
111
  paymentStatus={paymentStatus}
112
112
  />
113
113
 
114
- <div className="px-4 pb-3 pt-3 bg-black">
115
- <p className="mb-1.5 truncate text-base font-medium text-white">
114
+ <div className="px-4 pb-3 pt-3">
115
+ <p className="mb-0.5 truncate text-base font-medium text-black">
116
116
  {title}
117
117
  </p>
118
118
  <div className="flex items-center gap-1">
119
119
  {renderTypeIcon(mimeType, {
120
- className: 'size-5 shrink-0 text-white/55',
120
+ className: 'size-5 shrink-0 text-black/55',
121
121
  weight: 'regular',
122
122
  })}
123
123
 
124
124
  {detail && (
125
- <span className="text-xs font-medium text-white/55">{detail}</span>
125
+ <span className="text-xs font-medium text-black/55">{detail}</span>
126
126
  )}
127
127
 
128
128
  {paymentStatus === 'paid' ? (
129
129
  <React.Fragment>
130
- <span className="text-xs font-medium text-white/55">&bull;</span>
131
- <span className="text-xs font-medium text-[#4ade80]">
132
- Purchased
133
- </span>
134
- <CheckCircleIcon
135
- className="size-4 text-[#4ade80]"
136
- weight="bold"
137
- />
130
+ <span className="text-xs font-medium text-black/55">&bull;</span>
131
+ <span className="text-xs font-medium text-[#008236]">Purchased</span>
132
+ <CheckCircleIcon className="size-4 text-[#008236]" weight="bold" />
138
133
  </React.Fragment>
139
134
  ) : amountText != null ? (
140
135
  <React.Fragment>
141
- <span className="text-xs font-medium text-white/55">&bull;</span>
142
- <span className="text-xs font-medium text-white/55">
143
- {amountText}
144
- </span>
136
+ <span className="text-xs font-medium text-black/55">&bull;</span>
137
+ <span className="text-xs font-medium text-black/55">{amountText}</span>
145
138
  </React.Fragment>
146
139
  ) : null}
147
140
  </div>
@@ -26,10 +26,10 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
26
26
  type="button"
27
27
  onClick={onUnlockClicked}
28
28
  disabled={isUnlocking}
29
- className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-medium leading-none text-[#121110] hover:bg-white/90 disabled:opacity-70"
29
+ className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none text-white hover:bg-[#2a2928] disabled:opacity-70"
30
30
  >
31
31
  {isUnlocking ? (
32
- <LoadingDots />
32
+ <span className="size-4 animate-spin rounded-full border-2 border-white/30 border-t-white" />
33
33
  ) : (
34
34
  <React.Fragment>
35
35
  <LockSimpleIcon className="size-4" weight="fill" />
@@ -47,7 +47,7 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
47
47
  target="_blank"
48
48
  rel="noopener noreferrer"
49
49
  onClick={onDownloadClicked}
50
- className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-white px-4 text-sm font-medium leading-none !text-[#121110] hover:bg-white/90"
50
+ className="mt-3 inline-flex h-10 w-full items-center justify-center gap-2 rounded-full bg-[#121110] px-4 text-sm font-medium leading-none !text-white hover:bg-[#2a2928]"
51
51
  >
52
52
  <DownloadSimpleIcon className="size-4" weight="bold" />
53
53
  Download
@@ -58,14 +58,5 @@ const CardActions: React.FC<CardActionsProps> = (props) => {
58
58
  return null
59
59
  }
60
60
 
61
- const LoadingDots: React.FC = () => {
62
- return (
63
- <span className="flex items-center gap-1">
64
- <span className="size-1 rounded-full bg-white animate-bounce [animation-delay:-0.3s]" />
65
- <span className="size-1 rounded-full bg-white animate-bounce [animation-delay:-0.15s]" />
66
- <span className="size-1 rounded-full bg-white animate-bounce" />
67
- </span>
68
- )
69
- }
70
61
 
71
62
  export default CardActions
@@ -38,11 +38,11 @@ const CardThumbnail: React.FC<CardThumbnailProps> = ({
38
38
  }
39
39
 
40
40
  return (
41
- <div className="relative overflow-hidden bg-black/5">
41
+ <div className="relative aspect-video overflow-hidden bg-black/5">
42
42
  <img
43
43
  src={sourceType === 'document' ? thumbnailUrl : sourceUrl}
44
44
  alt={title}
45
- className={`block w-full transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
45
+ className={`absolute inset-0 h-full w-full object-contain transition-opacity duration-300 ${sourceReady ? 'opacity-100' : 'opacity-0'}`}
46
46
  draggable={false}
47
47
  onLoad={() => setSourceReady(true)}
48
48
  />