@linktr.ee/messaging-react 1.0.2 → 1.1.0-rc-1760927977

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 (39) hide show
  1. package/dist/assets/index.css +1 -0
  2. package/dist/index.d.ts +2 -1
  3. package/dist/index.js +836 -1079
  4. package/dist/index.js.map +1 -1
  5. package/package.json +4 -3
  6. package/src/components/ActionButton/ActionButton.stories.tsx +2 -1
  7. package/src/components/ActionButton/ActionButton.test.tsx +2 -0
  8. package/src/components/Avatar/Avatar.stories.tsx +2 -1
  9. package/src/components/Avatar/index.tsx +2 -1
  10. package/src/components/ChannelList/ChannelList.stories.tsx +4 -2
  11. package/src/components/ChannelList/CustomChannelPreview.stories.tsx +31 -27
  12. package/src/components/ChannelList/CustomChannelPreview.tsx +5 -11
  13. package/src/components/ChannelList/index.tsx +43 -35
  14. package/src/components/ChannelView.tsx +150 -127
  15. package/src/components/CloseButton/index.tsx +4 -5
  16. package/src/components/IconButton/IconButton.stories.tsx +3 -3
  17. package/src/components/Loading/Loading.stories.tsx +2 -1
  18. package/src/components/Loading/index.tsx +7 -9
  19. package/src/components/MessagingShell/EmptyState.stories.tsx +2 -1
  20. package/src/components/MessagingShell/ErrorState.stories.tsx +2 -1
  21. package/src/components/MessagingShell/LoadingState.stories.tsx +2 -1
  22. package/src/components/MessagingShell/LoadingState.tsx +3 -5
  23. package/src/components/MessagingShell/index.tsx +159 -135
  24. package/src/components/ParticipantPicker/ParticipantItem.stories.tsx +4 -2
  25. package/src/components/ParticipantPicker/ParticipantItem.tsx +25 -21
  26. package/src/components/ParticipantPicker/ParticipantPicker.stories.tsx +4 -2
  27. package/src/components/ParticipantPicker/ParticipantPicker.tsx +104 -76
  28. package/src/components/ParticipantPicker/index.tsx +93 -72
  29. package/src/components/SearchInput/SearchInput.stories.tsx +2 -1
  30. package/src/components/SearchInput/SearchInput.test.tsx +4 -2
  31. package/src/components/SearchInput/index.tsx +14 -15
  32. package/src/hooks/useParticipants.ts +1 -0
  33. package/src/index.ts +3 -0
  34. package/src/providers/MessagingProvider.tsx +213 -135
  35. package/src/stories/mocks.tsx +18 -19
  36. package/src/styles.css +75 -0
  37. package/src/test/setup.ts +11 -12
  38. package/src/test/utils.tsx +6 -7
  39. package/src/types.ts +1 -1
@@ -1,13 +1,14 @@
1
- import React, { useCallback, useEffect, useState, useRef } from 'react';
2
- import { ChatCircleDotsIcon } from "@phosphor-icons/react/dist/csr/ChatCircleDots";
3
- import classNames from 'classnames';
1
+ import { ChatCircleDotsIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
3
+ import React, { useCallback, useEffect, useState, useRef } from 'react'
4
4
 
5
- import type { ParticipantPickerProps, Participant } from '../../types';
6
- import { CloseButton } from '../CloseButton';
7
- import { SearchInput } from '../SearchInput';
8
- import Loading from '../Loading';
9
- import { ParticipantItem } from './ParticipantItem';
10
- import { useMessagingContext } from '../../providers/MessagingProvider';
5
+ import { useMessagingContext } from '../../providers/MessagingProvider'
6
+ import type { ParticipantPickerProps, Participant } from '../../types'
7
+ import { CloseButton } from '../CloseButton'
8
+ import Loading from '../Loading'
9
+ import { SearchInput } from '../SearchInput'
10
+
11
+ import { ParticipantItem } from './ParticipantItem'
11
12
 
12
13
  /**
13
14
  * Generic participant picker component for starting conversations
@@ -21,91 +22,105 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
21
22
  searchPlaceholder = 'Search participants...',
22
23
  className,
23
24
  }) => {
24
- const { debug } = useMessagingContext();
25
- const [searchQuery, setSearchQuery] = useState('');
26
- const [participants, setParticipants] = useState<Participant[]>([]);
27
- const [loading, setLoading] = useState(false);
28
- const [error, setError] = useState<string | null>(null);
29
- const [startingChatWithId, setStartingChatWithId] = useState<string | null>(null);
25
+ const { debug } = useMessagingContext()
26
+ const [searchQuery, setSearchQuery] = useState('')
27
+ const [participants, setParticipants] = useState<Participant[]>([])
28
+ const [loading, setLoading] = useState(false)
29
+ const [error, setError] = useState<string | null>(null)
30
+ const [startingChatWithId, setStartingChatWithId] = useState<string | null>(
31
+ null
32
+ )
30
33
 
31
34
  // Track if we've already loaded participants to prevent repeated loading
32
- const loadedRef = useRef(false);
33
-
35
+ const loadedRef = useRef(false)
36
+
34
37
  // Load participants initially - wait for participantSource to finish loading first
35
38
  useEffect(() => {
36
39
  // Wait for the participantSource to finish loading before we try to load participants
37
40
  if (participantSource.loading) {
38
41
  if (debug) {
39
- console.log('[ParticipantPicker] Waiting for participant source to finish loading...');
42
+ console.log(
43
+ '[ParticipantPicker] Waiting for participant source to finish loading...'
44
+ )
40
45
  }
41
- return;
46
+ return
42
47
  }
43
-
44
- if (loadedRef.current) return; // Prevent multiple loads
45
-
48
+
49
+ if (loadedRef.current) return // Prevent multiple loads
50
+
46
51
  const loadInitialParticipants = async () => {
47
52
  if (debug) {
48
- console.log('[ParticipantPicker] Loading initial participants...');
53
+ console.log('[ParticipantPicker] Loading initial participants...')
49
54
  }
50
- setLoading(true);
51
- setError(null);
52
-
55
+ setLoading(true)
56
+ setError(null)
57
+
53
58
  try {
54
- const result = await participantSource.loadParticipants({
59
+ const result = await participantSource.loadParticipants({
55
60
  search: '', // Load all participants initially
56
- limit: 100
57
- });
58
- setParticipants(result.participants);
59
- loadedRef.current = true; // Mark as loaded
61
+ limit: 100,
62
+ })
63
+ setParticipants(result.participants)
64
+ loadedRef.current = true // Mark as loaded
60
65
  if (debug) {
61
- console.log('[ParticipantPicker] Participants loaded successfully:', result.participants.length);
66
+ console.log(
67
+ '[ParticipantPicker] Participants loaded successfully:',
68
+ result.participants.length
69
+ )
62
70
  }
63
71
  } catch (err) {
64
- const errorMessage = err instanceof Error ? err.message : 'Failed to load participants';
65
- setError(errorMessage);
66
- console.error('[ParticipantPicker] Failed to load participants:', err);
72
+ const errorMessage =
73
+ err instanceof Error ? err.message : 'Failed to load participants'
74
+ setError(errorMessage)
75
+ console.error('[ParticipantPicker] Failed to load participants:', err)
67
76
  // Don't mark as loaded on error, allow retry
68
77
  } finally {
69
- setLoading(false);
78
+ setLoading(false)
70
79
  }
71
- };
80
+ }
72
81
 
73
- loadInitialParticipants();
74
- }, [participantSource.loading, debug]); // Re-run when loading state changes
82
+ loadInitialParticipants()
83
+ }, [participantSource, debug]) // Re-run when participantSource or debug changes
75
84
 
76
85
  // Filter participants by search query and existing participants
77
86
  const availableParticipants = participants
78
- .filter(participant => !existingParticipantIds.has(participant.id))
79
- .filter(participant => {
80
- if (!searchQuery) return true;
81
- const searchLower = searchQuery.toLowerCase();
87
+ .filter((participant) => !existingParticipantIds.has(participant.id))
88
+ .filter((participant) => {
89
+ if (!searchQuery) return true
90
+ const searchLower = searchQuery.toLowerCase()
82
91
  return (
83
92
  participant.name.toLowerCase().includes(searchLower) ||
84
93
  participant.email?.toLowerCase().includes(searchLower) ||
85
94
  false
86
- );
87
- });
88
-
89
- const handleSelectParticipant = useCallback(async (participant: Participant) => {
90
- if (startingChatWithId) return; // Prevent multiple clicks
91
-
92
- setStartingChatWithId(participant.id);
93
- try {
94
- await onSelectParticipant(participant);
95
- } catch (error) {
96
- console.error('[ParticipantPicker] Failed to start chat:', error);
97
- // Reset the loading state on error
98
- setStartingChatWithId(null);
99
- }
100
- // Note: Don't reset startingChatWithId on success because the dialog will close
101
- }, [onSelectParticipant, startingChatWithId]);
95
+ )
96
+ })
102
97
 
103
- const handleKeyDown = (event: React.KeyboardEvent, participant: Participant) => {
98
+ const handleSelectParticipant = useCallback(
99
+ async (participant: Participant) => {
100
+ if (startingChatWithId) return // Prevent multiple clicks
101
+
102
+ setStartingChatWithId(participant.id)
103
+ try {
104
+ await onSelectParticipant(participant)
105
+ } catch (error) {
106
+ console.error('[ParticipantPicker] Failed to start chat:', error)
107
+ // Reset the loading state on error
108
+ setStartingChatWithId(null)
109
+ }
110
+ // Note: Don't reset startingChatWithId on success because the dialog will close
111
+ },
112
+ [onSelectParticipant, startingChatWithId]
113
+ )
114
+
115
+ const handleKeyDown = (
116
+ event: React.KeyboardEvent,
117
+ participant: Participant
118
+ ) => {
104
119
  if (event.key === 'Enter' || event.key === ' ') {
105
- event.preventDefault();
106
- handleSelectParticipant(participant);
120
+ event.preventDefault()
121
+ handleSelectParticipant(participant)
107
122
  }
108
- };
123
+ }
109
124
 
110
125
  return (
111
126
  <div className={classNames('flex flex-col h-full', className)}>
@@ -117,10 +132,12 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
117
132
  </h2>
118
133
  <CloseButton onClick={onClose} />
119
134
  </div>
120
-
135
+
121
136
  <p className="text-xs text-stone mb-3">
122
- Select a {participantLabel.slice(0, -1)} to start messaging ({availableParticipants.length} available)
123
- {participantSource.totalCount !== undefined && ` • ${participantSource.totalCount} ${participantLabel} total`}
137
+ Select a {participantLabel.slice(0, -1)} to start messaging (
138
+ {availableParticipants.length} available)
139
+ {participantSource.totalCount !== undefined &&
140
+ ` • ${participantSource.totalCount} ${participantLabel} total`}
124
141
  </p>
125
142
 
126
143
  <SearchInput
@@ -143,7 +160,9 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
143
160
  <div className="h-32 flex items-center justify-center">
144
161
  <div className="flex items-center space-x-2">
145
162
  <div className="w-4 h-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
146
- <span className="text-sm text-stone">Loading {participantLabel}...</span>
163
+ <span className="text-sm text-stone">
164
+ Loading {participantLabel}...
165
+ </span>
147
166
  </div>
148
167
  </div>
149
168
  ) : availableParticipants.length === 0 ? (
@@ -169,19 +188,30 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
169
188
  ) : (
170
189
  <ul className="space-y-0">
171
190
  {availableParticipants.map((participant) => {
172
- const displayName = participant.name || participant.email || participant.id;
173
- const displaySecondary = participant.email && participant.name ? participant.email : participant.phone;
191
+ const displayName =
192
+ participant.name || participant.email || participant.id
193
+ const displaySecondary =
194
+ participant.email && participant.name
195
+ ? participant.email
196
+ : participant.phone
174
197
 
175
198
  return (
176
- <ParticipantItem key={participant.id} participant={participant} handleSelectParticipant={handleSelectParticipant} handleKeyDown={handleKeyDown} displayName={displayName} displaySecondary={displaySecondary} />
177
- );
199
+ <ParticipantItem
200
+ key={participant.id}
201
+ participant={participant}
202
+ handleSelectParticipant={handleSelectParticipant}
203
+ handleKeyDown={handleKeyDown}
204
+ displayName={displayName}
205
+ displaySecondary={displaySecondary}
206
+ />
207
+ )
178
208
  })}
179
209
 
180
210
  {/* Loading indicator */}
181
211
  {loading && (
182
212
  <li className="p-4 flex justify-center">
183
213
  <div className="flex items-center space-x-2">
184
- <Loading className='w-6 h-6' />
214
+ <Loading className="w-6 h-6" />
185
215
  <span className="text-sm text-stone">Loading more...</span>
186
216
  </div>
187
217
  </li>
@@ -190,7 +220,5 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
190
220
  )}
191
221
  </div>
192
222
  </div>
193
- );
194
- };
195
-
196
-
223
+ )
224
+ }
@@ -1,13 +1,12 @@
1
- import React, { useCallback, useEffect, useState, useRef } from 'react';
2
- import { ChatCircleDotsIcon } from "@phosphor-icons/react/dist/csr/ChatCircleDots";
3
- import { SpinnerGapIcon } from "@phosphor-icons/react/dist/csr/SpinnerGap";
4
- import classNames from 'classnames';
1
+ import { ChatCircleDotsIcon, SpinnerGapIcon } from '@phosphor-icons/react'
2
+ import classNames from 'classnames'
3
+ import React, { useCallback, useEffect, useState, useRef } from 'react'
5
4
 
6
- import type { ParticipantPickerProps, Participant } from '../../types';
7
- import { CloseButton } from '../CloseButton';
8
- import { SearchInput } from '../SearchInput';
9
- import { useMessagingContext } from '../../providers/MessagingProvider';
10
- import { Avatar } from '../Avatar';
5
+ import { useMessagingContext } from '../../providers/MessagingProvider'
6
+ import type { ParticipantPickerProps, Participant } from '../../types'
7
+ import { Avatar } from '../Avatar'
8
+ import { CloseButton } from '../CloseButton'
9
+ import { SearchInput } from '../SearchInput'
11
10
 
12
11
  /**
13
12
  * Generic participant picker component for starting conversations
@@ -21,91 +20,105 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
21
20
  searchPlaceholder = 'Search participants...',
22
21
  className,
23
22
  }) => {
24
- const { debug } = useMessagingContext();
25
- const [searchQuery, setSearchQuery] = useState('');
26
- const [participants, setParticipants] = useState<Participant[]>([]);
27
- const [loading, setLoading] = useState(false);
28
- const [error, setError] = useState<string | null>(null);
29
- const [startingChatWithId, setStartingChatWithId] = useState<string | null>(null);
23
+ const { debug } = useMessagingContext()
24
+ const [searchQuery, setSearchQuery] = useState('')
25
+ const [participants, setParticipants] = useState<Participant[]>([])
26
+ const [loading, setLoading] = useState(false)
27
+ const [error, setError] = useState<string | null>(null)
28
+ const [startingChatWithId, setStartingChatWithId] = useState<string | null>(
29
+ null
30
+ )
30
31
 
31
32
  // Track if we've already loaded participants to prevent repeated loading
32
- const loadedRef = useRef(false);
33
-
33
+ const loadedRef = useRef(false)
34
+
34
35
  // Load participants initially - wait for participantSource to finish loading first
35
36
  useEffect(() => {
36
37
  // Wait for the participantSource to finish loading before we try to load participants
37
38
  if (participantSource.loading) {
38
39
  if (debug) {
39
- console.log('[ParticipantPicker] Waiting for participant source to finish loading...');
40
+ console.log(
41
+ '[ParticipantPicker] Waiting for participant source to finish loading...'
42
+ )
40
43
  }
41
- return;
44
+ return
42
45
  }
43
-
44
- if (loadedRef.current) return; // Prevent multiple loads
45
-
46
+
47
+ if (loadedRef.current) return // Prevent multiple loads
48
+
46
49
  const loadInitialParticipants = async () => {
47
50
  if (debug) {
48
- console.log('[ParticipantPicker] Loading initial participants...');
51
+ console.log('[ParticipantPicker] Loading initial participants...')
49
52
  }
50
- setLoading(true);
51
- setError(null);
52
-
53
+ setLoading(true)
54
+ setError(null)
55
+
53
56
  try {
54
- const result = await participantSource.loadParticipants({
57
+ const result = await participantSource.loadParticipants({
55
58
  search: '', // Load all participants initially
56
- limit: 100
57
- });
58
- setParticipants(result.participants);
59
- loadedRef.current = true; // Mark as loaded
59
+ limit: 100,
60
+ })
61
+ setParticipants(result.participants)
62
+ loadedRef.current = true // Mark as loaded
60
63
  if (debug) {
61
- console.log('[ParticipantPicker] Participants loaded successfully:', result.participants.length);
64
+ console.log(
65
+ '[ParticipantPicker] Participants loaded successfully:',
66
+ result.participants.length
67
+ )
62
68
  }
63
69
  } catch (err) {
64
- const errorMessage = err instanceof Error ? err.message : 'Failed to load participants';
65
- setError(errorMessage);
66
- console.error('[ParticipantPicker] Failed to load participants:', err);
70
+ const errorMessage =
71
+ err instanceof Error ? err.message : 'Failed to load participants'
72
+ setError(errorMessage)
73
+ console.error('[ParticipantPicker] Failed to load participants:', err)
67
74
  // Don't mark as loaded on error, allow retry
68
75
  } finally {
69
- setLoading(false);
76
+ setLoading(false)
70
77
  }
71
- };
78
+ }
72
79
 
73
- loadInitialParticipants();
74
- }, [participantSource.loading, debug]); // Re-run when loading state changes
80
+ loadInitialParticipants()
81
+ }, [participantSource.loading, debug]) // Re-run when loading state changes
75
82
 
76
83
  // Filter participants by search query and existing participants
77
84
  const availableParticipants = participants
78
- .filter(participant => !existingParticipantIds.has(participant.id))
79
- .filter(participant => {
80
- if (!searchQuery) return true;
81
- const searchLower = searchQuery.toLowerCase();
85
+ .filter((participant) => !existingParticipantIds.has(participant.id))
86
+ .filter((participant) => {
87
+ if (!searchQuery) return true
88
+ const searchLower = searchQuery.toLowerCase()
82
89
  return (
83
90
  participant.name.toLowerCase().includes(searchLower) ||
84
91
  participant.email?.toLowerCase().includes(searchLower) ||
85
92
  false
86
- );
87
- });
88
-
89
- const handleSelectParticipant = useCallback(async (participant: Participant) => {
90
- if (startingChatWithId) return; // Prevent multiple clicks
91
-
92
- setStartingChatWithId(participant.id);
93
- try {
94
- await onSelectParticipant(participant);
95
- } catch (error) {
96
- console.error('[ParticipantPicker] Failed to start chat:', error);
97
- // Reset the loading state on error
98
- setStartingChatWithId(null);
99
- }
100
- // Note: Don't reset startingChatWithId on success because the dialog will close
101
- }, [onSelectParticipant, startingChatWithId]);
93
+ )
94
+ })
102
95
 
103
- const handleKeyDown = (event: React.KeyboardEvent, participant: Participant) => {
96
+ const handleSelectParticipant = useCallback(
97
+ async (participant: Participant) => {
98
+ if (startingChatWithId) return // Prevent multiple clicks
99
+
100
+ setStartingChatWithId(participant.id)
101
+ try {
102
+ await onSelectParticipant(participant)
103
+ } catch (error) {
104
+ console.error('[ParticipantPicker] Failed to start chat:', error)
105
+ // Reset the loading state on error
106
+ setStartingChatWithId(null)
107
+ }
108
+ // Note: Don't reset startingChatWithId on success because the dialog will close
109
+ },
110
+ [onSelectParticipant, startingChatWithId]
111
+ )
112
+
113
+ const handleKeyDown = (
114
+ event: React.KeyboardEvent,
115
+ participant: Participant
116
+ ) => {
104
117
  if (event.key === 'Enter' || event.key === ' ') {
105
- event.preventDefault();
106
- handleSelectParticipant(participant);
118
+ event.preventDefault()
119
+ handleSelectParticipant(participant)
107
120
  }
108
- };
121
+ }
109
122
 
110
123
  return (
111
124
  <div className={classNames('flex flex-col h-full', className)}>
@@ -117,10 +130,12 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
117
130
  </h2>
118
131
  <CloseButton onClick={onClose} />
119
132
  </div>
120
-
133
+
121
134
  <p className="text-xs text-stone mb-3">
122
- Select a {participantLabel.slice(0, -1)} to start messaging ({availableParticipants.length} available)
123
- {participantSource.totalCount !== undefined && ` • ${participantSource.totalCount} ${participantLabel} total`}
135
+ Select a {participantLabel.slice(0, -1)} to start messaging (
136
+ {availableParticipants.length} available)
137
+ {participantSource.totalCount !== undefined &&
138
+ ` • ${participantSource.totalCount} ${participantLabel} total`}
124
139
  </p>
125
140
 
126
141
  <SearchInput
@@ -143,7 +158,9 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
143
158
  <div className="h-32 flex items-center justify-center">
144
159
  <div className="flex items-center space-x-2">
145
160
  <div className="w-4 h-4 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
146
- <span className="text-sm text-stone">Loading {participantLabel}...</span>
161
+ <span className="text-sm text-stone">
162
+ Loading {participantLabel}...
163
+ </span>
147
164
  </div>
148
165
  </div>
149
166
  ) : availableParticipants.length === 0 ? (
@@ -169,8 +186,12 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
169
186
  ) : (
170
187
  <ul className="space-y-0">
171
188
  {availableParticipants.map((participant) => {
172
- const displayName = participant.name || participant.email || participant.id;
173
- const displaySecondary = participant.email && participant.name ? participant.email : participant.phone;
189
+ const displayName =
190
+ participant.name || participant.email || participant.id
191
+ const displaySecondary =
192
+ participant.email && participant.name
193
+ ? participant.email
194
+ : participant.phone
174
195
 
175
196
  return (
176
197
  <li key={participant.id}>
@@ -214,7 +235,7 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
214
235
  </div>
215
236
  </button>
216
237
  </li>
217
- );
238
+ )
218
239
  })}
219
240
 
220
241
  {/* Loading indicator */}
@@ -230,5 +251,5 @@ export const ParticipantPicker: React.FC<ParticipantPickerProps> = ({
230
251
  )}
231
252
  </div>
232
253
  </div>
233
- );
234
- };
254
+ )
255
+ }
@@ -1,7 +1,8 @@
1
1
  import type { Meta, StoryFn } from '@storybook/react'
2
- import { SearchInput } from '.'
3
2
  import React from 'react'
4
3
 
4
+ import { SearchInput } from '.'
5
+
5
6
  type ComponentProps = React.ComponentProps<typeof SearchInput>
6
7
 
7
8
  const meta: Meta<ComponentProps> = {
@@ -1,5 +1,7 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
+
2
3
  import { renderWithProviders, screen, userEvent } from '../../test/utils';
4
+
3
5
  import { SearchInput } from './index';
4
6
 
5
7
  describe('SearchInput', () => {
@@ -54,7 +56,7 @@ describe('SearchInput', () => {
54
56
  <SearchInput searchQuery="test" setSearchQuery={vi.fn()} placeholder="Search" />
55
57
  );
56
58
 
57
- const clearButton = screen.getByLabelText(/clear/i);
59
+ const clearButton = screen.getByRole('button', { name: /clear/i });
58
60
  expect(clearButton).toBeInTheDocument();
59
61
  });
60
62
 
@@ -75,7 +77,7 @@ describe('SearchInput', () => {
75
77
  <SearchInput searchQuery="test" setSearchQuery={handleChange} placeholder="Search" />
76
78
  );
77
79
 
78
- const clearButton = screen.getByLabelText(/clear/i);
80
+ const clearButton = screen.getByRole('button', { name: /clear/i });
79
81
  await user.click(clearButton);
80
82
 
81
83
  expect(handleChange).toHaveBeenCalledWith('');
@@ -1,12 +1,12 @@
1
- import React, { useRef } from 'react';
2
- import { IconButton } from '../IconButton';
3
- import { MagnifyingGlassIcon } from "@phosphor-icons/react/dist/csr/MagnifyingGlass";
4
- import { XIcon } from "@phosphor-icons/react/dist/csr/X";
1
+ import { MagnifyingGlassIcon, XIcon } from '@phosphor-icons/react'
2
+ import { useRef } from 'react'
3
+
4
+ import { IconButton } from '../IconButton'
5
5
 
6
6
  interface SearchInputProps {
7
- searchQuery: string;
8
- setSearchQuery: (value: string) => void;
9
- placeholder: string;
7
+ searchQuery: string
8
+ setSearchQuery: (value: string) => void
9
+ placeholder: string
10
10
  }
11
11
 
12
12
  export function SearchInput({
@@ -14,7 +14,7 @@ export function SearchInput({
14
14
  setSearchQuery,
15
15
  placeholder,
16
16
  }: SearchInputProps) {
17
- const searchInputRef = useRef<HTMLInputElement>(null);
17
+ const searchInputRef = useRef<HTMLInputElement>(null)
18
18
 
19
19
  return (
20
20
  <div className="relative">
@@ -22,7 +22,7 @@ export function SearchInput({
22
22
  className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-stone"
23
23
  weight="bold"
24
24
  />
25
-
25
+
26
26
  <input
27
27
  ref={searchInputRef}
28
28
  type="text"
@@ -31,20 +31,19 @@ export function SearchInput({
31
31
  onChange={(e) => setSearchQuery(e.target.value)}
32
32
  className="w-full pl-10 pr-10 py-3 text-sm border border-sand rounded-xl focus:outline-none focus:ring-2 focus:ring-black focus:border-transparent"
33
33
  />
34
-
34
+
35
35
  {searchQuery && (
36
36
  <IconButton
37
37
  label="Clear search"
38
38
  onClick={() => {
39
- setSearchQuery('');
40
- searchInputRef.current?.focus();
39
+ setSearchQuery('')
40
+ searchInputRef.current?.focus()
41
41
  }}
42
42
  className="absolute right-3 top-1/2 -translate-y-1/2 p-1 text-stone hover:text-charcoal"
43
43
  >
44
- <XIcon className="h-4 w-4" weight="bold" />
44
+ <XIcon className="h-4 w-4" weight="bold" />
45
45
  </IconButton>
46
46
  )}
47
47
  </div>
48
- );
48
+ )
49
49
  }
50
-
@@ -1,4 +1,5 @@
1
1
  import { useState, useEffect, useCallback } from 'react';
2
+
2
3
  import type { ParticipantSource, Participant } from '../types';
3
4
 
4
5
  /**
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // Import styles so they're bundled with the package
2
+ import './styles.css'
3
+
1
4
  // Components
2
5
  export { MessagingShell } from './components/MessagingShell';
3
6
  export { ChannelList } from './components/ChannelList';