@linktr.ee/messaging-react 1.32.1 → 1.33.0-rc-1777504230

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-ClE_iExA.js → Card-BsqYzZt1.js} +55 -55
  2. package/dist/Card-BsqYzZt1.js.map +1 -0
  3. package/dist/{Card-1CQEn-OT.js → Card-Cnn9V-W7.js} +44 -44
  4. package/dist/Card-Cnn9V-W7.js.map +1 -0
  5. package/dist/assets/index.css +1 -1
  6. package/dist/index-BMfupE8K.js +3130 -0
  7. package/dist/index-BMfupE8K.js.map +1 -0
  8. package/dist/index.d.ts +19 -1
  9. package/dist/index.js +20 -2477
  10. package/dist/index.js.map +1 -1
  11. package/package.json +1 -1
  12. package/src/components/ChannelInfoDialog/index.tsx +3 -1
  13. package/src/components/ChannelView.stories.tsx +38 -0
  14. package/src/components/ChannelView.test.tsx +25 -6
  15. package/src/components/ChannelView.tsx +26 -6
  16. package/src/components/CustomMessageInput/CustomMessageInput.stories.tsx +180 -0
  17. package/src/components/CustomMessageInput/CustomMessageInput.test.tsx +63 -1
  18. package/src/components/CustomMessageInput/index.tsx +24 -5
  19. package/src/components/LockedAttachment/components/Creator/Card.tsx +11 -11
  20. package/src/components/LockedAttachment/components/MediaPlayer.tsx +10 -1
  21. package/src/components/LockedAttachment/components/Visitor/Card.tsx +9 -9
  22. package/src/components/LockedAttachment/components/Visitor/CardActions.tsx +2 -2
  23. package/src/components/MediaMessage/MediaMessage.stories.tsx +233 -0
  24. package/src/components/MediaMessage/MediaMessage.test.tsx +520 -0
  25. package/src/components/MediaMessage/index.tsx +476 -0
  26. package/src/components/MessagingShell/index.tsx +2 -0
  27. package/src/index.ts +2 -0
  28. package/src/styles.css +49 -0
  29. package/src/types.ts +13 -0
  30. package/dist/Card-1CQEn-OT.js.map +0 -1
  31. package/dist/Card-ClE_iExA.js.map +0 -1
  32. package/dist/MediaPlayer-B9Ws2NeE.js +0 -292
  33. package/dist/MediaPlayer-B9Ws2NeE.js.map +0 -1
@@ -7,17 +7,17 @@ import { renderWithProviders, screen } from '../test/utils'
7
7
  import { ChannelView } from './ChannelView'
8
8
 
9
9
  let activeChannel: unknown
10
+ let activeChannelProps: Record<string, unknown> = {}
10
11
 
11
12
  vi.mock('stream-chat-react', () => ({
12
- Channel: ({
13
- channel,
14
- children,
15
- }: {
13
+ Channel: (props: {
16
14
  channel: unknown
17
15
  children: React.ReactNode
16
+ [key: string]: unknown
18
17
  }) => {
19
- activeChannel = channel
20
- return <div data-testid="channel">{children}</div>
18
+ activeChannel = props.channel
19
+ activeChannelProps = props
20
+ return <div data-testid="channel">{props.children}</div>
21
21
  },
22
22
  Window: ({ children }: { children: React.ReactNode }) => (
23
23
  <div data-testid="window">{children}</div>
@@ -96,6 +96,7 @@ const createChannel = () =>
96
96
  describe('ChannelView', () => {
97
97
  beforeEach(() => {
98
98
  activeChannel = undefined
99
+ activeChannelProps = {}
99
100
  avatarRenderCalls.length = 0
100
101
  mockIsStarred = false
101
102
  })
@@ -193,4 +194,22 @@ describe('ChannelView', () => {
193
194
  )
194
195
  expect(renderMessageInputActions).toHaveBeenCalledWith(channel)
195
196
  })
197
+
198
+ it('passes sendButton to Channel as SendButton when provided', () => {
199
+ const CustomSendButton = () => (
200
+ <button type="button" aria-label="Custom Send" />
201
+ )
202
+
203
+ renderWithProviders(
204
+ <ChannelView channel={createChannel()} sendButton={CustomSendButton} />
205
+ )
206
+
207
+ expect(activeChannelProps.SendButton).toBe(CustomSendButton)
208
+ })
209
+
210
+ it('does not pass SendButton to Channel when sendButton is not provided', () => {
211
+ renderWithProviders(<ChannelView channel={createChannel()} />)
212
+
213
+ expect(activeChannelProps.SendButton).toBeUndefined()
214
+ })
196
215
  })
@@ -1,4 +1,4 @@
1
- import { ArrowLeftIcon, DotsThreeIcon, StarIcon } from '@phosphor-icons/react'
1
+ import { ArrowLeftIcon, CaretRightIcon, DotsThreeIcon, StarIcon } from '@phosphor-icons/react'
2
2
  import classNames from 'classnames'
3
3
  import React, { useCallback, useRef } from 'react'
4
4
  import { Channel as ChannelType } from 'stream-chat'
@@ -96,9 +96,15 @@ const CustomChannelHeader: React.FC<{
96
96
  starred={showStarButton && isStarred}
97
97
  size={40}
98
98
  />
99
- <h1 className="text-xs font-medium text-black/90">
99
+ <button
100
+ type="button"
101
+ onClick={onShowInfo}
102
+ className="flex items-center gap-0.5 rounded-full bg-black/[0.05] px-3 py-1 text-xs font-medium text-black/90 hover:bg-black/[0.08] transition-colors"
103
+ aria-label={`View info for ${participantName}`}
104
+ >
100
105
  {participantName}
101
- </h1>
106
+ <CaretRightIcon className="size-3 shrink-0" />
107
+ </button>
102
108
  </div>
103
109
  <div className="flex justify-end items-center gap-2">
104
110
  {showStarButton && (
@@ -150,9 +156,21 @@ const CustomChannelHeader: React.FC<{
150
156
  size={40}
151
157
  />
152
158
  <div className="min-w-0">
153
- <h1 className="font-medium text-black/90 truncate">
154
- {participantName}
155
- </h1>
159
+ {canShowInfo ? (
160
+ <button
161
+ type="button"
162
+ onClick={onShowInfo}
163
+ className="flex items-center gap-1 font-medium text-black/90 truncate hover:text-black/70 transition-colors"
164
+ aria-label={`View info for ${participantName}`}
165
+ >
166
+ <span className="truncate">{participantName}</span>
167
+ <CaretRightIcon className="size-4 shrink-0" />
168
+ </button>
169
+ ) : (
170
+ <h1 className="font-medium text-black/90 truncate">
171
+ {participantName}
172
+ </h1>
173
+ )}
156
174
  </div>
157
175
  </div>
158
176
  <div className="flex items-center gap-2">
@@ -376,6 +394,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
376
394
  customProfileContent,
377
395
  customChannelActions,
378
396
  renderMessage,
397
+ sendButton,
379
398
  }) => {
380
399
  // Custom send message handler that:
381
400
  // 1. Applies messageMetadata if provided
@@ -435,6 +454,7 @@ export const ChannelView = React.memo<ChannelViewProps>(
435
454
  LoadingIndicator={LoadingState}
436
455
  DateSeparator={CustomDateSeparator}
437
456
  doSendMessageRequest={doSendMessageRequest}
457
+ {...(sendButton ? { SendButton: sendButton } : {})}
438
458
  >
439
459
  <ChannelViewInner
440
460
  onBack={onBack}
@@ -0,0 +1,180 @@
1
+ import type { Meta, StoryFn } from '@storybook/react'
2
+ import React, { useEffect } from 'react'
3
+ import { QueryChannelAPIResponse, StreamChat } from 'stream-chat'
4
+ import { Channel, Chat } from 'stream-chat-react'
5
+
6
+ import { CustomMessageInput } from '.'
7
+
8
+ // ---------------------------------------------------------------------------
9
+ // Setup helpers (mirrors ChannelView.stories.tsx)
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const mockUser = {
13
+ id: 'storybook-user',
14
+ name: 'Storybook User',
15
+ image: 'https://i.pravatar.cc/150?img=1',
16
+ }
17
+
18
+ const mockParticipant = {
19
+ id: 'participant-1',
20
+ name: 'Jane Creator',
21
+ image: 'https://i.pravatar.cc/150?img=5',
22
+ }
23
+
24
+ const createMockChannel = async (
25
+ client: StreamChat,
26
+ opts: { frozen?: boolean } = {}
27
+ ) => {
28
+ const channelData = { members: [mockUser.id, mockParticipant.id], frozen: opts.frozen ?? false }
29
+ const ch = client.channel('messaging', 'storybook-cmi-channel', channelData)
30
+
31
+ ch.watch = async () => {
32
+ ch.state.members = {
33
+ [mockUser.id]: { user: mockUser, user_id: mockUser.id },
34
+ [mockParticipant.id]: { user: mockParticipant, user_id: mockParticipant.id },
35
+ }
36
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
37
+ ;(ch as any)._data = channelData
38
+ return { channel: channelData, members: [], messages: [], watchers: [], pinned_messages: [], duration: '0ms' } as unknown as QueryChannelAPIResponse
39
+ }
40
+
41
+ try { await ch.watch() } catch (_) { /* mock */ }
42
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
43
+ ;(ch as any)._data = channelData
44
+ return ch
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Story wrapper — mounts the input inside a real Stream Channel context
49
+ // ---------------------------------------------------------------------------
50
+
51
+ type WrapperProps = {
52
+ frozen?: boolean
53
+ sendButton?: React.ComponentType<{
54
+ sendMessage: (...args: unknown[]) => unknown
55
+ disabled?: boolean
56
+ [key: string]: unknown
57
+ }>
58
+ renderActions?: () => React.ReactNode
59
+ }
60
+
61
+ const Wrapper: React.FC<WrapperProps> = ({ frozen, sendButton, renderActions }) => {
62
+ const [client] = React.useState(() => {
63
+ const c = new StreamChat('mock-api-key', { allowServerSideConnect: true })
64
+ c.userID = mockUser.id
65
+ c.user = mockUser
66
+ return c
67
+ })
68
+
69
+ const [channel, setChannel] = React.useState<ReturnType<StreamChat['channel']> | null>(null)
70
+
71
+ useEffect(() => {
72
+ createMockChannel(client, { frozen }).then(setChannel)
73
+ }, [client, frozen])
74
+
75
+ if (!channel) return <div className="p-4 text-sm text-gray-400">Loading…</div>
76
+
77
+ return (
78
+ <Chat client={client}>
79
+ <Channel
80
+ channel={channel}
81
+ {...(sendButton ? { SendButton: sendButton } : {})}
82
+ >
83
+ <div className="bg-white" style={{ minWidth: 360 }}>
84
+ <CustomMessageInput renderActions={renderActions} />
85
+ </div>
86
+ </Channel>
87
+ </Chat>
88
+ )
89
+ }
90
+
91
+ // ---------------------------------------------------------------------------
92
+ // Meta
93
+ // ---------------------------------------------------------------------------
94
+
95
+ const meta: Meta<WrapperProps> = {
96
+ title: 'CustomMessageInput',
97
+ component: CustomMessageInput,
98
+ parameters: {
99
+ layout: 'centered',
100
+ },
101
+ }
102
+ export default meta
103
+
104
+ // ---------------------------------------------------------------------------
105
+ // Stories
106
+ // ---------------------------------------------------------------------------
107
+
108
+ export const Default: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
109
+ Default.args = {}
110
+ Default.parameters = {
111
+ docs: {
112
+ description: {
113
+ story:
114
+ 'Default message input. The send button (arrow) is disabled until the user types text — `useMessageComposerHasSendableData` controls this.',
115
+ },
116
+ },
117
+ }
118
+
119
+ export const Frozen: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
120
+ Frozen.args = { frozen: true }
121
+ Frozen.parameters = {
122
+ docs: {
123
+ description: {
124
+ story:
125
+ 'Frozen channel state: the entire input area fades out (`aria-disabled`, `inert`), the textarea becomes read-only, and the send button is disabled.',
126
+ },
127
+ },
128
+ }
129
+
130
+ export const WithCustomActionButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
131
+ WithCustomActionButton.args = {
132
+ renderActions: () => (
133
+ <button
134
+ type="button"
135
+ aria-label="Attach media"
136
+ className="flex items-center justify-center size-10 rounded-full bg-[#F1F0EE] hover:bg-[#E5E4E1] shrink-0"
137
+ onClick={() => console.log('media picker')}
138
+ >
139
+ 📷
140
+ </button>
141
+ ),
142
+ }
143
+ WithCustomActionButton.parameters = {
144
+ docs: {
145
+ description: {
146
+ story:
147
+ 'Shows how `renderActions` injects extra controls (e.g. a media picker button) to the left of the message bubble.',
148
+ },
149
+ },
150
+ }
151
+
152
+ /** A custom send button that turns purple to signal a media attachment is staged. */
153
+ const PurpleMediaSendButton: React.FC<{
154
+ sendMessage: (...args: unknown[]) => unknown
155
+ disabled?: boolean
156
+ [key: string]: unknown
157
+ }> = ({ sendMessage, disabled, ...rest }) => (
158
+ <button
159
+ {...rest}
160
+ type="button"
161
+ aria-label="Send"
162
+ disabled={disabled}
163
+ onClick={() => sendMessage()}
164
+ className="str-chat__send-button mt-auto flex items-center justify-center shrink-0 rounded-full size-8 bg-[#6D28D9] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white"
165
+ style={{ fontSize: 15 }}
166
+ >
167
+ 🚀
168
+ </button>
169
+ )
170
+
171
+ export const WithCustomSendButton: StoryFn<WrapperProps> = (args) => <Wrapper {...args} />
172
+ WithCustomSendButton.args = { sendButton: PurpleMediaSendButton }
173
+ WithCustomSendButton.parameters = {
174
+ docs: {
175
+ description: {
176
+ story:
177
+ 'Passes a custom `SendButton` via `Channel`\'s `SendButton` prop (which is how `ChannelView`\'s `sendButton` prop surfaces it). `CustomMessageInput` reads the button from `useComponentContext` and falls back to the default when none is provided.',
178
+ },
179
+ },
180
+ }
@@ -1,5 +1,5 @@
1
1
  import React from 'react'
2
- import { describe, expect, it, vi } from 'vitest'
2
+ import { beforeEach, describe, expect, it, vi } from 'vitest'
3
3
 
4
4
  import { renderWithProviders, screen } from '../../test/utils'
5
5
 
@@ -7,6 +7,21 @@ import { CustomMessageInput } from '.'
7
7
 
8
8
  let mockChannelData: Record<string, unknown> = {}
9
9
 
10
+ let mockContextSendButton:
11
+ | React.ComponentType<{
12
+ sendMessage: (...args: unknown[]) => unknown
13
+ disabled?: boolean
14
+ }>
15
+ | undefined = ({ sendMessage, disabled }) => (
16
+ <button
17
+ type="button"
18
+ data-testid="send-button"
19
+ aria-label="Send"
20
+ disabled={disabled}
21
+ onClick={() => sendMessage()}
22
+ />
23
+ )
24
+
10
25
  vi.mock('stream-chat-react', () => ({
11
26
  MessageInput: ({
12
27
  Input,
@@ -35,6 +50,9 @@ vi.mock('stream-chat-react', () => ({
35
50
  handleSubmit: vi.fn(),
36
51
  }),
37
52
  useMessageComposerHasSendableData: () => false,
53
+ useComponentContext: () => ({
54
+ SendButton: mockContextSendButton,
55
+ }),
38
56
  }))
39
57
 
40
58
  vi.mock('../CustomLinkPreviewList', () => ({
@@ -42,6 +60,18 @@ vi.mock('../CustomLinkPreviewList', () => ({
42
60
  }))
43
61
 
44
62
  describe('CustomMessageInput', () => {
63
+ beforeEach(() => {
64
+ mockContextSendButton = ({ sendMessage, disabled }) => (
65
+ <button
66
+ type="button"
67
+ data-testid="send-button"
68
+ aria-label="Send"
69
+ disabled={disabled}
70
+ onClick={() => sendMessage()}
71
+ />
72
+ )
73
+ })
74
+
45
75
  it('renders the interactive message input when channel is not frozen', () => {
46
76
  mockChannelData = {}
47
77
 
@@ -124,4 +154,36 @@ describe('CustomMessageInput', () => {
124
154
  expect(messageInput).not.toHaveAttribute('aria-disabled')
125
155
  expect(screen.getByTestId('custom-action')).toBeInTheDocument()
126
156
  })
157
+
158
+ it('renders the custom SendButton from component context', () => {
159
+ mockChannelData = {}
160
+ mockContextSendButton = () => (
161
+ <button type="button" data-testid="custom-context-send-button" aria-label="Custom Send" />
162
+ )
163
+
164
+ renderWithProviders(<CustomMessageInput />)
165
+
166
+ expect(screen.getByTestId('custom-context-send-button')).toBeInTheDocument()
167
+ expect(screen.queryByTestId('default-send-button')).not.toBeInTheDocument()
168
+ })
169
+
170
+ it('falls back to the arrow send button when component context provides none', () => {
171
+ mockChannelData = {}
172
+ mockContextSendButton = undefined
173
+
174
+ renderWithProviders(<CustomMessageInput />)
175
+
176
+ expect(screen.getByTestId('send-button')).toBeInTheDocument()
177
+ expect(screen.queryByTestId('custom-context-send-button')).not.toBeInTheDocument()
178
+ })
179
+
180
+ it('preserves disabled state on the send button when there is no sendable data', () => {
181
+ mockChannelData = {}
182
+
183
+ renderWithProviders(<CustomMessageInput />)
184
+
185
+ const sendButton = screen.getByRole('button', { name: /send/i })
186
+ // useMessageComposerHasSendableData is mocked to return false
187
+ expect(sendButton).toBeDisabled()
188
+ })
127
189
  })
@@ -7,16 +7,37 @@ import {
7
7
  SimpleAttachmentSelector,
8
8
  TextareaComposer,
9
9
  useChannelStateContext,
10
+ useComponentContext,
10
11
  useMessageComposerHasSendableData,
11
12
  useMessageInputContext,
12
13
  } from 'stream-chat-react'
13
14
 
14
15
  import { CustomLinkPreviewList } from '../CustomLinkPreviewList'
15
16
 
17
+ const DefaultSendButton: React.FC<{
18
+ sendMessage: () => void
19
+ disabled?: boolean
20
+ [key: string]: unknown
21
+ }> = ({ sendMessage, disabled, ...rest }) => (
22
+ <button
23
+ {...rest}
24
+ type="button"
25
+ aria-label="Send"
26
+ disabled={disabled}
27
+ onClick={sendMessage}
28
+ >
29
+ <ArrowUpIcon weight="bold" className="size-4" />
30
+ </button>
31
+ )
32
+
16
33
  const CustomMessageInputInner: React.FC = () => {
17
34
  const { channel } = useChannelStateContext()
18
35
  const isFrozen = channel?.data?.frozen === true
19
36
  const { handleSubmit } = useMessageInputContext()
37
+ const { SendButton: SendButtonFromContext } = useComponentContext(
38
+ 'CustomMessageInput',
39
+ )
40
+ const SendButton = SendButtonFromContext ?? DefaultSendButton
20
41
  const hasSendableData = useMessageComposerHasSendableData()
21
42
  const isSendDisabled = isFrozen || !hasSendableData
22
43
 
@@ -44,16 +65,14 @@ const CustomMessageInputInner: React.FC = () => {
44
65
  tabIndex={isFrozen ? -1 : undefined}
45
66
  />
46
67
  </div>
47
- <button
68
+ <SendButton
69
+ sendMessage={handleSubmit}
48
70
  aria-label="Send"
49
71
  className="str-chat__send-button mt-auto flex justify-center items-center flex-shrink-0 rounded-full size-8 bg-[#121110] disabled:bg-[#F1F0EE] disabled:text-black/20 text-white focus-ring"
50
72
  data-testid="send-button"
51
73
  disabled={isSendDisabled}
52
- onClick={handleSubmit}
53
74
  type="button"
54
- >
55
- <ArrowUpIcon className="size-4" />
56
- </button>
75
+ />
57
76
  </div>
58
77
  </div>
59
78
  </>
@@ -67,11 +67,11 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
67
67
  onToggle={onPreviewClick ? handleToggle : undefined}
68
68
  />
69
69
 
70
- <div className="px-4 pb-3 pt-3">
70
+ <div className="px-4 pb-3 pt-3 bg-black">
71
71
  <p
72
72
  className={classNames('mb-1.5 truncate text-base font-medium', {
73
- 'text-black/30': !title,
74
- 'text-black': !!title,
73
+ 'text-white/30': !title,
74
+ 'text-white': !!title,
75
75
  })}
76
76
  >
77
77
  {title || placeholderTitle}
@@ -79,30 +79,30 @@ const CreatorCard: React.FC<CreatorCardProps> = ({
79
79
 
80
80
  <div className="flex items-center gap-1">
81
81
  {renderTypeIcon(mimeType, {
82
- className: 'size-5 shrink-0 text-black/55',
82
+ className: 'size-5 shrink-0 text-white/55',
83
83
  weight: 'regular',
84
84
  })}
85
85
 
86
86
  {detail && (
87
- <span className="text-xs font-medium text-black/55">{detail}</span>
87
+ <span className="text-xs font-medium text-white/55">{detail}</span>
88
88
  )}
89
89
 
90
90
  {paymentStatus === 'paid' ? (
91
91
  <React.Fragment>
92
- <span className="text-xs font-medium text-black/55">&bull;</span>
93
- <span className="text-xs font-medium text-[#008236]">Sold</span>
92
+ <span className="text-xs font-medium text-white/55">&bull;</span>
93
+ <span className="text-xs font-medium text-[#4ade80]">Sold</span>
94
94
  <CheckCircleIcon
95
- className="size-4 text-[#008236]"
95
+ className="size-4 text-[#4ade80]"
96
96
  weight="bold"
97
97
  />
98
98
  </React.Fragment>
99
99
  ) : (
100
100
  <React.Fragment>
101
- <span className="text-xs font-medium text-black/55">&bull;</span>
101
+ <span className="text-xs font-medium text-white/55">&bull;</span>
102
102
  <span
103
103
  className={classNames('text-xs font-medium', {
104
- 'text-black/30': !amountText,
105
- 'text-black/55': !!amountText,
104
+ 'text-white/30': !amountText,
105
+ 'text-white/55': !!amountText,
106
106
  })}
107
107
  >
108
108
  {amountText || placeholderAmountText}
@@ -31,6 +31,12 @@ export interface MediaPlayerProps {
31
31
  showProgress?: boolean
32
32
  /** When true, requests muted playback (helps autoplay policies on video). */
33
33
  muted?: boolean
34
+ /**
35
+ * When provided, overrides the default click-to-play-toggle behaviour on the
36
+ * player container. The play/pause button (which calls stopPropagation) is
37
+ * unaffected, so inline playback still works.
38
+ */
39
+ onContainerClick?: (e: React.MouseEvent) => void
34
40
  }
35
41
 
36
42
  const MediaPlayer: React.FC<MediaPlayerProps> = ({
@@ -43,6 +49,7 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
43
49
  controls = true,
44
50
  showProgress = false,
45
51
  muted = false,
52
+ onContainerClick,
46
53
  }) => {
47
54
  // --- Derived ---
48
55
  const sourceType = getSourceType(mimeType)
@@ -185,13 +192,15 @@ const MediaPlayer: React.FC<MediaPlayerProps> = ({
185
192
  tabIndex={0}
186
193
  className={`relative cursor-pointer overflow-hidden bg-black ${aspectClass}`}
187
194
  style={aspectStyle}
188
- onClick={() => {
195
+ onClick={(e) => {
196
+ if (onContainerClick) { onContainerClick(e); return }
189
197
  if (manualPlayRequired) return
190
198
  if (controls) setPlaying((p) => !p)
191
199
  }}
192
200
  onKeyDown={(e) => {
193
201
  if (e.key !== 'Enter' && e.key !== ' ') return
194
202
  e.preventDefault()
203
+ if (onContainerClick) { onContainerClick(e as unknown as React.MouseEvent); return }
195
204
  if (manualPlayRequired) return
196
205
  if (controls) setPlaying((p) => !p)
197
206
  }}
@@ -111,35 +111,35 @@ const VisitorCard: React.FC<VisitorCardProps> = ({
111
111
  paymentStatus={paymentStatus}
112
112
  />
113
113
 
114
- <div className="px-4 pb-3 pt-3">
115
- <p className="mb-1.5 truncate text-base font-medium text-black">
114
+ <div className="px-4 pb-3 pt-3 bg-black">
115
+ <p className="mb-1.5 truncate text-base font-medium text-white">
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-black/55',
120
+ className: 'size-5 shrink-0 text-white/55',
121
121
  weight: 'regular',
122
122
  })}
123
123
 
124
124
  {detail && (
125
- <span className="text-xs font-medium text-black/55">{detail}</span>
125
+ <span className="text-xs font-medium text-white/55">{detail}</span>
126
126
  )}
127
127
 
128
128
  {paymentStatus === 'paid' ? (
129
129
  <React.Fragment>
130
- <span className="text-xs font-medium text-black/55">&bull;</span>
131
- <span className="text-xs font-medium text-[#008236]">
130
+ <span className="text-xs font-medium text-white/55">&bull;</span>
131
+ <span className="text-xs font-medium text-[#4ade80]">
132
132
  Purchased
133
133
  </span>
134
134
  <CheckCircleIcon
135
- className="size-4 text-[#008236]"
135
+ className="size-4 text-[#4ade80]"
136
136
  weight="bold"
137
137
  />
138
138
  </React.Fragment>
139
139
  ) : amountText != null ? (
140
140
  <React.Fragment>
141
- <span className="text-xs font-medium text-black/55">&bull;</span>
142
- <span className="text-xs font-medium text-black/55">
141
+ <span className="text-xs font-medium text-white/55">&bull;</span>
142
+ <span className="text-xs font-medium text-white/55">
143
143
  {amountText}
144
144
  </span>
145
145
  </React.Fragment>
@@ -26,7 +26,7 @@ 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-[#121110] px-4 text-sm font-medium leading-none text-white hover:bg-[#2a2928] disabled:opacity-70"
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"
30
30
  >
31
31
  {isUnlocking ? (
32
32
  <LoadingDots />
@@ -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-[#121110] px-4 text-sm font-medium leading-none !text-white hover:bg-[#2a2928]"
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"
51
51
  >
52
52
  <DownloadSimpleIcon className="size-4" weight="bold" />
53
53
  Download