@connectycube/react-ui-kit 0.0.20 → 0.0.22

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 (68) hide show
  1. package/configs/dependencies.json +19 -4
  2. package/configs/imports.json +7 -2
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/attachment.d.ts +5 -6
  8. package/dist/types/components/attachment.d.ts.map +1 -1
  9. package/dist/types/components/chat-bubble.d.ts +32 -0
  10. package/dist/types/components/chat-bubble.d.ts.map +1 -0
  11. package/dist/types/components/chat-input.d.ts +27 -0
  12. package/dist/types/components/chat-input.d.ts.map +1 -0
  13. package/dist/types/components/chat-list.d.ts +30 -0
  14. package/dist/types/components/chat-list.d.ts.map +1 -0
  15. package/dist/types/components/checkbox.d.ts +11 -0
  16. package/dist/types/components/checkbox.d.ts.map +1 -0
  17. package/dist/types/components/dialogs-list.d.ts +14 -0
  18. package/dist/types/components/dialogs-list.d.ts.map +1 -0
  19. package/dist/types/components/file-picker.d.ts +1 -1
  20. package/dist/types/components/file-picker.d.ts.map +1 -1
  21. package/dist/types/components/placeholder-text.d.ts.map +1 -1
  22. package/dist/types/components/quick-actions.d.ts +14 -0
  23. package/dist/types/components/quick-actions.d.ts.map +1 -0
  24. package/dist/types/components/status-call.d.ts +8 -0
  25. package/dist/types/components/status-call.d.ts.map +1 -0
  26. package/dist/types/index.d.ts +7 -2
  27. package/dist/types/index.d.ts.map +1 -1
  28. package/gen/components/attachment.jsx +6 -6
  29. package/gen/components/button.jsx +1 -1
  30. package/gen/components/{chat-message.jsx → chat-bubble.jsx} +69 -48
  31. package/gen/components/chat-input.jsx +152 -0
  32. package/gen/components/chat-list.jsx +151 -0
  33. package/gen/components/checkbox.jsx +30 -0
  34. package/gen/components/dialog-item.jsx +1 -1
  35. package/gen/components/dialogs-list.jsx +73 -0
  36. package/gen/components/dismiss-layer.jsx +1 -1
  37. package/gen/components/file-picker.jsx +2 -2
  38. package/gen/components/placeholder-text.jsx +5 -1
  39. package/gen/components/quick-actions.jsx +62 -0
  40. package/gen/components/search.jsx +1 -1
  41. package/gen/components/status-call.jsx +18 -0
  42. package/gen/components/stream-view.jsx +8 -8
  43. package/gen/index.js +14 -2
  44. package/package.json +11 -8
  45. package/src/components/attachment.tsx +16 -14
  46. package/src/components/button.tsx +1 -1
  47. package/src/components/chat-bubble.tsx +176 -0
  48. package/src/components/chat-input.tsx +172 -0
  49. package/src/components/chat-list.tsx +164 -0
  50. package/src/components/checkbox.tsx +40 -0
  51. package/src/components/connectycube-ui/chat-input.tsx +174 -0
  52. package/src/components/dialog-item.tsx +1 -1
  53. package/src/components/dialogs-list.tsx +84 -0
  54. package/src/components/dismiss-layer.tsx +1 -1
  55. package/src/components/file-picker.tsx +3 -3
  56. package/src/components/placeholder-text.tsx +5 -1
  57. package/src/components/quick-actions.tsx +74 -0
  58. package/src/components/search.tsx +1 -1
  59. package/src/components/status-call.tsx +23 -0
  60. package/src/components/stream-view.tsx +8 -8
  61. package/src/index.ts +17 -2
  62. package/dist/types/components/call-message.d.ts +0 -17
  63. package/dist/types/components/call-message.d.ts.map +0 -1
  64. package/dist/types/components/chat-message.d.ts +0 -30
  65. package/dist/types/components/chat-message.d.ts.map +0 -1
  66. package/gen/components/call-message.jsx +0 -62
  67. package/src/components/call-message.tsx +0 -75
  68. package/src/components/chat-message.tsx +0 -138
@@ -5,25 +5,21 @@ import { Spinner } from './spinner';
5
5
  import { cn, getRandomString } from './utils';
6
6
 
7
7
  interface AttachmentProps {
8
+ pending?: boolean;
8
9
  uid?: string;
9
10
  url?: string;
10
11
  mimeType?: string;
11
- pending?: boolean;
12
- onReady?: (skipOnce?: boolean) => void;
12
+ onReady?: () => void;
13
13
  linkProps?: AttachmentLinkProps;
14
14
  containerProps?: React.ComponentProps<'div'>;
15
15
  }
16
16
 
17
- interface AttachmentLinkProps
18
- extends React.ComponentProps<'a'>, Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
19
- children?: React.ReactNode;
20
- }
17
+ interface AttachmentLinkProps extends React.ComponentProps<'a'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
21
18
 
22
19
  interface AttachmentImageProps
23
20
  extends React.ComponentProps<'img'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
24
21
 
25
- interface AttachmentAudioProps
26
- extends React.ComponentProps<'audio'>, Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
22
+ interface AttachmentAudioProps extends React.ComponentProps<'audio'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {}
27
23
 
28
24
  interface AttachmentVideoProps extends React.ComponentProps<'video'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
29
25
  maxSize?: number;
@@ -110,7 +106,7 @@ function AttachmentVideoBase(
110
106
  }
111
107
  };
112
108
 
113
- useImperativeHandle(ref, () => playerRef.current!, []);
109
+ useImperativeHandle(ref, () => playerRef.current || ({} as HTMLVideoElement), []);
114
110
 
115
111
  return (
116
112
  <div
@@ -228,7 +224,13 @@ function AttachmentFailed({
228
224
 
229
225
  AttachmentFailed.displayName = 'AttachmentFailed';
230
226
 
231
- function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
227
+ function AttachmentBase({
228
+ mimeType,
229
+ onReady = () => {},
230
+ containerProps = {},
231
+ linkProps = {},
232
+ ...props
233
+ }: AttachmentProps) {
232
234
  const [type = ''] = mimeType?.split('/') || [];
233
235
 
234
236
  if (!props.url) {
@@ -237,13 +239,13 @@ function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
237
239
 
238
240
  switch (type) {
239
241
  case 'image':
240
- return <AttachmentImage {...props} />;
242
+ return <AttachmentImage onReady={onReady} linkProps={linkProps} {...props} />;
241
243
  case 'video':
242
- return <AttachmentVideo {...props} />;
244
+ return <AttachmentVideo onReady={onReady} containerProps={containerProps} {...props} />;
243
245
  case 'audio':
244
- return <AttachmentAudio {...props} />;
246
+ return <AttachmentAudio containerProps={containerProps} {...props} />;
245
247
  default:
246
- return <AttachmentFile name={mimeType} {...props} />;
248
+ return <AttachmentFile name={mimeType} containerProps={containerProps} {...props} />;
247
249
  }
248
250
  }
249
251
 
@@ -46,7 +46,7 @@ function ButtonBase(
46
46
  <Comp
47
47
  ref={ref}
48
48
  {...props}
49
- className={cn(buttonVariants({ variant, size, className }), 'transition-all ease-in-out duration-300')}
49
+ className={cn(buttonVariants({ variant, size, className }), 'transition-all ease-out duration-300')}
50
50
  />
51
51
  );
52
52
  }
@@ -0,0 +1,176 @@
1
+ import type React from 'react';
2
+ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
3
+ import { useInView } from 'react-intersection-observer';
4
+ import { Avatar, type AvatarProps } from './avatar';
5
+ import { FormattedDate, type FormattedDateProps } from './formatted-date';
6
+ import { StatusSent, type StatusSentProps } from './status-sent';
7
+ import { cn } from './utils';
8
+
9
+ interface ChatBubbleProps extends React.ComponentProps<'div'> {
10
+ onView?: () => void;
11
+ isLast?: boolean;
12
+ date?: FormattedDateProps['date'];
13
+ language?: FormattedDateProps['language'];
14
+ formattedDateProps?: FormattedDateProps;
15
+ }
16
+
17
+ interface ChatBubbleMessageProps extends ChatBubbleProps {
18
+ fromMe: boolean;
19
+ sameSenderAbove: boolean;
20
+ title?: string;
21
+ senderName?: string;
22
+ senderAvatar?: AvatarProps['src'];
23
+ statusSent?: StatusSentProps['status'];
24
+ avatarProps?: AvatarProps;
25
+ bubbleProps?: React.ComponentProps<'div'>;
26
+ titleProps?: React.ComponentProps<'span'>;
27
+ statusSentProps?: StatusSentProps;
28
+ }
29
+
30
+ interface ChatBubbleInfoProps extends ChatBubbleProps {
31
+ info?: string | undefined;
32
+ iconElement?: React.ReactNode;
33
+ infoProps?: React.ComponentProps<'span'>;
34
+ }
35
+
36
+ function ChatBubbleBase(
37
+ { onView = () => {}, isLast, children, ...props }: ChatBubbleProps,
38
+ ref: React.ForwardedRef<HTMLDivElement>
39
+ ) {
40
+ const [setRef, inView] = useInView();
41
+ const messageRef = useRef<HTMLDivElement>(null);
42
+ const setRefs = useCallback(
43
+ (node: HTMLDivElement) => {
44
+ messageRef.current = node;
45
+ setRef(node);
46
+ },
47
+ [setRef]
48
+ );
49
+
50
+ useEffect(() => {
51
+ if (inView) {
52
+ onView();
53
+ }
54
+ }, [inView, onView]);
55
+
56
+ useImperativeHandle(ref, () => messageRef.current || ({} as HTMLDivElement), []);
57
+
58
+ return (
59
+ <div ref={setRefs} {...props} className={cn('mt-2', isLast && 'mb-2', inView && 'view', props?.className)}>
60
+ {children}
61
+ </div>
62
+ );
63
+ }
64
+
65
+ const ChatBubble = forwardRef<HTMLDivElement, ChatBubbleProps>(ChatBubbleBase);
66
+
67
+ function ChatBubbleMessageBase(
68
+ {
69
+ fromMe,
70
+ sameSenderAbove,
71
+ title,
72
+ senderName,
73
+ senderAvatar,
74
+ date = new Date(),
75
+ language = 'en',
76
+ statusSent,
77
+ avatarProps,
78
+ bubbleProps,
79
+ titleProps,
80
+ formattedDateProps,
81
+ statusSentProps,
82
+ children,
83
+ ...props
84
+ }: ChatBubbleMessageProps,
85
+ ref: React.ForwardedRef<HTMLDivElement>
86
+ ) {
87
+ const hasAvatar = Boolean((avatarProps || senderAvatar) && !sameSenderAbove);
88
+ const hasAvatarMargin = Boolean((avatarProps || senderAvatar) && sameSenderAbove);
89
+
90
+ return (
91
+ <ChatBubble
92
+ ref={ref}
93
+ {...props}
94
+ className={cn(
95
+ `flex relative text-left whitespace-pre-wrap`,
96
+ fromMe ? 'self-end flex-row-reverse ml-12' : `self-start mr-12`,
97
+ sameSenderAbove && 'mt-1'
98
+ )}
99
+ >
100
+ {hasAvatar && (
101
+ <Avatar
102
+ name={senderName}
103
+ src={senderAvatar}
104
+ imageProps={{ className: 'bg-blue-200' }}
105
+ fallbackProps={{ className: 'bg-blue-200' }}
106
+ {...avatarProps}
107
+ className={cn('mt-1 mr-1', avatarProps?.className)}
108
+ />
109
+ )}
110
+
111
+ <div
112
+ className={cn(
113
+ 'relative flex flex-col min-w-42 max-w-120 rounded-xl px-2 pt-2 pb-6 shadow-sm',
114
+ fromMe ? 'bg-gray-200' : 'bg-blue-200',
115
+ hasAvatarMargin && 'ml-9',
116
+ bubbleProps?.className
117
+ )}
118
+ >
119
+ {(title || senderName) && (
120
+ <span
121
+ {...titleProps}
122
+ className={cn(
123
+ 'font-semibold',
124
+ title && 'mb-1 py-1.5 text-xs text-muted-foreground italic border-b',
125
+ titleProps?.className
126
+ )}
127
+ >
128
+ {title || senderName}
129
+ </span>
130
+ )}
131
+ {children}
132
+ <div className="absolute bottom-1 right-2 flex items-center gap-1 italic">
133
+ <FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
134
+ <StatusSent status={statusSent} {...statusSentProps} />
135
+ </div>
136
+ </div>
137
+ </ChatBubble>
138
+ );
139
+ }
140
+
141
+ const ChatBubbleMessage = memo(forwardRef<HTMLDivElement, ChatBubbleMessageProps>(ChatBubbleMessageBase));
142
+
143
+ ChatBubbleMessage.displayName = 'ChatBubbleMessage';
144
+
145
+ function ChatBubbleInfoBase(
146
+ {
147
+ info = '',
148
+ iconElement,
149
+ date = new Date(),
150
+ language = 'en',
151
+ infoProps,
152
+ formattedDateProps,
153
+ ...props
154
+ }: ChatBubbleInfoProps,
155
+ ref: React.ForwardedRef<HTMLDivElement>
156
+ ) {
157
+ return (
158
+ <ChatBubble
159
+ ref={ref}
160
+ {...props}
161
+ className={cn('flex items-center justify-center gap-2 rounded-full w-fit bg-ring/20 mx-auto px-3 py-1.5 mt-2')}
162
+ >
163
+ {iconElement}
164
+ <span {...infoProps} className={cn('text-sm mb-px', infoProps?.className)}>
165
+ {info}
166
+ </span>
167
+ <FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
168
+ </ChatBubble>
169
+ );
170
+ }
171
+
172
+ const ChatBubbleInfo = memo(forwardRef<HTMLDivElement, ChatBubbleInfoProps>(ChatBubbleInfoBase));
173
+
174
+ ChatBubbleInfo.displayName = 'ChatBubbleInfo';
175
+
176
+ export { ChatBubbleMessage, ChatBubbleInfo, type ChatBubbleMessageProps, type ChatBubbleInfoProps };
@@ -0,0 +1,172 @@
1
+ import type React from 'react';
2
+ import { ChangeEvent, forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
3
+ import TextareaAutosize, { type TextareaAutosizeProps, type TextareaHeightChangeMeta } from 'react-textarea-autosize';
4
+ import { SendHorizontal, type LucideProps } from 'lucide-react';
5
+ import { Label, type LabelProps } from './label';
6
+ import { cn } from './utils';
7
+
8
+ interface ChatInputSendProps extends LabelProps {
9
+ onSend: () => void;
10
+ iconElement?: React.ReactNode;
11
+ iconProps?: LucideProps;
12
+ }
13
+
14
+ interface ChatInputProps extends TextareaAutosizeProps, Omit<React.ComponentProps<'textarea'>, 'style'> {
15
+ children?: React.ReactNode;
16
+ pending?: boolean;
17
+ draft?: string;
18
+ onSend?: (value?: string) => void;
19
+ onDraft?: (value?: string) => void;
20
+ onTyping?: (typing?: boolean) => void;
21
+ onHeightGrow?: (data: { height: number; shift: number }) => void;
22
+ hideChatInputSend?: boolean;
23
+ chatInputSendProps?: ChatInputSendProps;
24
+ containerProps?: React.ComponentProps<'div'>;
25
+ }
26
+
27
+ function ChatInputSendBase(
28
+ { onSend = () => {}, iconElement, iconProps, ...props }: ChatInputSendProps,
29
+ ref: React.ForwardedRef<HTMLLabelElement>
30
+ ) {
31
+ return (
32
+ <Label
33
+ ref={ref}
34
+ {...props}
35
+ onClick={onSend}
36
+ className={cn('group rounded-full py-2 pl-2.5 pr-1.5 cursor-pointer bg-ring/90 hover:bg-ring', props?.className)}
37
+ >
38
+ {iconElement || (
39
+ <SendHorizontal
40
+ {...iconProps}
41
+ className={cn(
42
+ 'text-background group-hover:scale-110 transition-all duration-200 ease-out',
43
+ iconProps?.className
44
+ )}
45
+ />
46
+ )}
47
+ </Label>
48
+ );
49
+ }
50
+
51
+ const ChatInputSend = forwardRef<HTMLLabelElement, ChatInputSendProps>(ChatInputSendBase);
52
+
53
+ ChatInputSend.displayName = 'ChatInputSend';
54
+
55
+ function ChatInputBase(
56
+ {
57
+ pending = false,
58
+ draft,
59
+ onSend = () => {},
60
+ onDraft = () => {},
61
+ onTyping = () => {},
62
+ onHeightGrow = () => {},
63
+ hideChatInputSend = false,
64
+ chatInputSendProps,
65
+ containerProps,
66
+ children,
67
+ ...props
68
+ }: ChatInputProps,
69
+ ref: React.ForwardedRef<HTMLTextAreaElement>
70
+ ) {
71
+ const [value, setValue] = useState<string>();
72
+ const textareaRef = useRef<HTMLTextAreaElement>(null);
73
+ const textareaHeightRef = useRef<number>(0);
74
+ const typingRef = useRef<boolean>(false);
75
+ const typingTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
76
+ const handleStopTyping = () => {
77
+ typingRef.current = false;
78
+ onTyping(false);
79
+
80
+ if (typingTimeoutRef.current) {
81
+ clearTimeout(typingTimeoutRef.current);
82
+ typingTimeoutRef.current = undefined;
83
+ }
84
+ };
85
+ const handleOnChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
86
+ const { data } = event.nativeEvent as InputEvent;
87
+
88
+ setValue(event.target.value);
89
+
90
+ if (!typingRef.current && typeof data === 'string' && data.length > 0) {
91
+ typingRef.current = true;
92
+ onTyping(true);
93
+ }
94
+
95
+ clearTimeout(typingTimeoutRef.current);
96
+ typingTimeoutRef.current = setTimeout(handleStopTyping, 5000);
97
+ };
98
+ const handleOnSend = async () => {
99
+ handleStopTyping();
100
+
101
+ if (typeof value === 'string' && value.length > 0) {
102
+ onSend(value.trim());
103
+ setValue('');
104
+ textareaRef.current?.focus();
105
+ }
106
+ };
107
+ const handleOnHeightChange = (height: number, meta: TextareaHeightChangeMeta) => {
108
+ if (!height && !meta) return;
109
+
110
+ if (height !== textareaHeightRef.current) {
111
+ const shift = height > textareaHeightRef.current ? meta.rowHeight : -meta.rowHeight;
112
+
113
+ onHeightGrow({ height, shift });
114
+ }
115
+
116
+ textareaHeightRef.current = height;
117
+ };
118
+ const handleOnKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
119
+ if (event.key === 'Enter' && event.shiftKey === false) {
120
+ event.preventDefault();
121
+ props.onKeyDown?.(event);
122
+ handleOnSend();
123
+ }
124
+ };
125
+
126
+ useEffect(() => {
127
+ const textarea = textareaRef.current;
128
+
129
+ handleStopTyping();
130
+ queueMicrotask(() => setValue(draft));
131
+ textarea?.focus();
132
+
133
+ return () => {
134
+ onDraft(textarea?.value);
135
+ };
136
+ }, [props.key]);
137
+
138
+ useImperativeHandle(ref, () => textareaRef.current || ({} as HTMLTextAreaElement), []);
139
+
140
+ return (
141
+ <div {...containerProps} className={cn('flex items-end gap-2', containerProps?.className)}>
142
+ <TextareaAutosize
143
+ name="textarea-autosize"
144
+ ref={textareaRef}
145
+ autoFocus
146
+ minRows={1}
147
+ maxRows={8}
148
+ {...props}
149
+ value={value}
150
+ onChange={handleOnChange}
151
+ onKeyDown={handleOnKeyDown}
152
+ onHeightChange={handleOnHeightChange}
153
+ className={cn(
154
+ 'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-11 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
155
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring',
156
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
157
+ 'resize-none py-2.5',
158
+ pending && 'pointer-events-none bg-muted-foreground border-ring ring-ring/50 animate-pulse',
159
+ props?.className
160
+ )}
161
+ />
162
+ {!hideChatInputSend && <ChatInputSend onSend={handleOnSend} {...chatInputSendProps} />}
163
+ {children}
164
+ </div>
165
+ );
166
+ }
167
+
168
+ const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(ChatInputBase);
169
+
170
+ ChatInput.displayName = 'ChatInput';
171
+
172
+ export { ChatInput, type ChatInputProps };
@@ -0,0 +1,164 @@
1
+ import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, useCallback } from 'react';
2
+ import { Virtualizer, VirtualizerProps, type VirtualizerHandle } from 'virtua';
3
+ import { FilePickerDropzone, type FilePickerDropzoneProps } from './file-picker';
4
+ import { QuickActions, type QuickActionsProps } from './quick-actions';
5
+ import { Spinner } from './spinner';
6
+ import { cn } from './utils';
7
+
8
+ interface ChatListProps extends VirtualizerProps {
9
+ key: string;
10
+ loading?: boolean;
11
+ textareaMeasurement: { height: number; shift: number };
12
+ offsetToReach?: number;
13
+ onScrollStartReached?: () => void;
14
+ onScrollEndReached?: () => void;
15
+ onListReset?: (prev: string | undefined, next: string | undefined) => void;
16
+ onListCreate?: () => void;
17
+ onListGrow?: () => void;
18
+ enableFilePickerDropzone?: boolean;
19
+ containerProps?: React.ComponentProps<'div'>;
20
+ filePickerDropzoneProps?: FilePickerDropzoneProps;
21
+ quickActionsProps?: QuickActionsProps;
22
+ quickActionsVisible?: boolean;
23
+ minItemsCount?: number;
24
+ }
25
+
26
+ interface ChatListHandle extends VirtualizerHandle {
27
+ scrollToBottom: (force?: boolean) => void;
28
+ }
29
+
30
+ function DefaultChatListWrapper({ children, ...props }: React.ComponentProps<'div'>) {
31
+ return (
32
+ <div {...props} className={cn('relative size-full min-h-0', props?.className)}>
33
+ {children}
34
+ </div>
35
+ );
36
+ }
37
+
38
+ function ChatListBase(
39
+ {
40
+ key,
41
+ loading = false,
42
+ textareaMeasurement,
43
+ offsetToReach = 200,
44
+ onScrollStartReached,
45
+ onScrollEndReached,
46
+ onListReset,
47
+ onListCreate,
48
+ onListGrow,
49
+ enableFilePickerDropzone = true,
50
+ containerProps,
51
+ filePickerDropzoneProps,
52
+ quickActionsProps,
53
+ children,
54
+ quickActionsVisible,
55
+ minItemsCount = 1,
56
+ ...props
57
+ }: ChatListProps,
58
+ ref: React.ForwardedRef<ChatListHandle>
59
+ ) {
60
+ const ChatListWrapper = enableFilePickerDropzone ? FilePickerDropzone : DefaultChatListWrapper;
61
+ const chatListWrapperProps = enableFilePickerDropzone ? filePickerDropzoneProps : containerProps;
62
+ const itemsCount = Array.isArray(children) ? children.length : Array.isArray(props.data) ? props.data.length : 0;
63
+ const itemsCountRef = useRef<number>(0);
64
+ const keyRef = useRef<string | undefined>(undefined);
65
+ const [shouldPrependMessages, setShouldPrependMessages] = useState<boolean>(false);
66
+ const [isPreparing, setIsPreparing] = useState<boolean>(false);
67
+ const virtuaRef = useRef<VirtualizerHandle>(null);
68
+ const prepareNextVirtualizer = () => {
69
+ queueMicrotask(() => {
70
+ setIsPreparing(true);
71
+ queueMicrotask(() => setIsPreparing(false));
72
+ });
73
+ };
74
+ const handleOnScroll = async (offset: number) => {
75
+ props.onScroll?.(offset);
76
+
77
+ if (!virtuaRef.current) return;
78
+
79
+ if (typeof onScrollStartReached === 'function' && offset < offsetToReach) {
80
+ setShouldPrependMessages(true);
81
+ onScrollStartReached();
82
+ }
83
+
84
+ if (
85
+ typeof onScrollEndReached === 'function' &&
86
+ virtuaRef.current.viewportSize + offset + offsetToReach > virtuaRef.current.scrollSize
87
+ ) {
88
+ onScrollEndReached();
89
+ }
90
+ };
91
+ const scrollToBottom = useCallback(
92
+ (force: boolean = false) => {
93
+ if (!virtuaRef.current) return;
94
+
95
+ if (force || virtuaRef.current.scrollSize - virtuaRef.current.scrollOffset < virtuaRef.current.viewportSize * 2) {
96
+ queueMicrotask(() => virtuaRef.current?.scrollToIndex(itemsCount - 1, { align: 'start' }));
97
+ }
98
+ },
99
+ [itemsCount]
100
+ );
101
+
102
+ useEffect(() => {
103
+ if (key !== keyRef.current) {
104
+ prepareNextVirtualizer();
105
+ onListReset?.(keyRef.current, key);
106
+ keyRef.current = key;
107
+ itemsCountRef.current = 0;
108
+ }
109
+ }, [key, onListReset]);
110
+
111
+ useEffect(() => {
112
+ if (itemsCountRef.current === 0 && itemsCount > 0) {
113
+ prepareNextVirtualizer();
114
+ onListCreate?.();
115
+ itemsCountRef.current = itemsCount;
116
+ scrollToBottom(true);
117
+ }
118
+
119
+ if (itemsCount > itemsCountRef.current) {
120
+ onListGrow?.();
121
+ itemsCountRef.current = itemsCount;
122
+
123
+ if (shouldPrependMessages) {
124
+ queueMicrotask(() => setShouldPrependMessages(false));
125
+ }
126
+
127
+ scrollToBottom();
128
+ }
129
+ }, [itemsCount, shouldPrependMessages, onListCreate, onListGrow, scrollToBottom]);
130
+
131
+ useEffect(() => {
132
+ virtuaRef.current?.scrollBy(textareaMeasurement.shift);
133
+ }, [textareaMeasurement]);
134
+
135
+ useImperativeHandle(ref, () => ({ ...(virtuaRef.current || ({} as VirtualizerHandle)), scrollToBottom }), [
136
+ scrollToBottom,
137
+ ]);
138
+
139
+ if (isPreparing) {
140
+ return null;
141
+ }
142
+
143
+ if (minItemsCount >= itemsCount) {
144
+ return quickActionsVisible ? <QuickActions {...quickActionsProps} /> : <Spinner loading layout="centered" />;
145
+ }
146
+
147
+ return (
148
+ <ChatListWrapper {...chatListWrapperProps}>
149
+ <Spinner loading={loading} className="my-5" />
150
+ <div className="flex flex-col h-full overflow-y-auto px-2">
151
+ <div className="grow" />
152
+ <Virtualizer ref={virtuaRef} onScroll={handleOnScroll} shift={shouldPrependMessages} {...props}>
153
+ {children}
154
+ </Virtualizer>
155
+ </div>
156
+ </ChatListWrapper>
157
+ );
158
+ }
159
+
160
+ const ChatList = forwardRef<ChatListHandle, ChatListProps>(ChatListBase);
161
+
162
+ ChatList.displayName = 'ChatList';
163
+
164
+ export { ChatList, type ChatListProps, type ChatListHandle };
@@ -0,0 +1,40 @@
1
+ import type React from 'react';
2
+ import { forwardRef } from 'react';
3
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
4
+ import { CheckIcon, type LucideProps } from 'lucide-react';
5
+ import { cn } from './utils';
6
+
7
+ interface CheckboxProps extends CheckboxPrimitive.CheckboxProps {
8
+ iconElement?: React.ReactNode;
9
+ iconProps?: LucideProps;
10
+ indicatorProps?: CheckboxPrimitive.CheckboxIndicatorProps;
11
+ }
12
+
13
+ function CheckboxBase(
14
+ { iconElement, iconProps, indicatorProps, ...props }: CheckboxProps,
15
+ ref: React.ForwardedRef<HTMLButtonElement>
16
+ ) {
17
+ return (
18
+ <CheckboxPrimitive.Root
19
+ ref={ref}
20
+ {...props}
21
+ className={cn(
22
+ 'peer border-input dark:bg-input/30 data-[state=checked]:bg-ring data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-ring data-[state=checked]:border-ring focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
23
+ props?.className
24
+ )}
25
+ >
26
+ <CheckboxPrimitive.Indicator
27
+ {...indicatorProps}
28
+ className={cn('flex items-center justify-center text-current transition-none', indicatorProps?.className)}
29
+ >
30
+ {iconElement || <CheckIcon {...iconProps} className={cn('size-3.5', iconProps?.className)} />}
31
+ </CheckboxPrimitive.Indicator>
32
+ </CheckboxPrimitive.Root>
33
+ );
34
+ }
35
+
36
+ const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(CheckboxBase);
37
+
38
+ Checkbox.displayName = 'Checkbox';
39
+
40
+ export { Checkbox, type CheckboxProps };