@connectycube/react-ui-kit 0.0.19 → 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 (78) hide show
  1. package/configs/dependencies.json +21 -0
  2. package/configs/imports.json +7 -0
  3. package/dist/index.cjs +1 -36
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.js +1 -35
  6. package/dist/index.js.map +1 -1
  7. package/dist/types/components/attachment.d.ts +7 -8
  8. package/dist/types/components/attachment.d.ts.map +1 -1
  9. package/dist/types/components/avatar.d.ts +1 -0
  10. package/dist/types/components/avatar.d.ts.map +1 -1
  11. package/dist/types/components/badge.d.ts +1 -1
  12. package/dist/types/components/button.d.ts +2 -2
  13. package/dist/types/components/chat-bubble.d.ts +32 -0
  14. package/dist/types/components/chat-bubble.d.ts.map +1 -0
  15. package/dist/types/components/chat-input.d.ts +27 -0
  16. package/dist/types/components/chat-input.d.ts.map +1 -0
  17. package/dist/types/components/chat-list.d.ts +30 -0
  18. package/dist/types/components/chat-list.d.ts.map +1 -0
  19. package/dist/types/components/checkbox.d.ts +11 -0
  20. package/dist/types/components/checkbox.d.ts.map +1 -0
  21. package/dist/types/components/dialog-item.d.ts.map +1 -1
  22. package/dist/types/components/dialogs-list.d.ts +14 -0
  23. package/dist/types/components/dialogs-list.d.ts.map +1 -0
  24. package/dist/types/components/file-picker.d.ts +1 -1
  25. package/dist/types/components/file-picker.d.ts.map +1 -1
  26. package/dist/types/components/linkify-text.d.ts +6 -1
  27. package/dist/types/components/linkify-text.d.ts.map +1 -1
  28. package/dist/types/components/placeholder-text.d.ts.map +1 -1
  29. package/dist/types/components/quick-actions.d.ts +14 -0
  30. package/dist/types/components/quick-actions.d.ts.map +1 -0
  31. package/dist/types/components/status-call.d.ts +8 -0
  32. package/dist/types/components/status-call.d.ts.map +1 -0
  33. package/dist/types/components/switch.d.ts.map +1 -1
  34. package/dist/types/index.d.ts +8 -0
  35. package/dist/types/index.d.ts.map +1 -1
  36. package/gen/components/attachment.jsx +27 -25
  37. package/gen/components/avatar.jsx +14 -2
  38. package/gen/components/button.jsx +1 -1
  39. package/gen/components/chat-bubble.jsx +141 -0
  40. package/gen/components/chat-input.jsx +152 -0
  41. package/gen/components/chat-list.jsx +151 -0
  42. package/gen/components/checkbox.jsx +30 -0
  43. package/gen/components/dialog-item.jsx +5 -2
  44. package/gen/components/dialogs-list.jsx +73 -0
  45. package/gen/components/dismiss-layer.jsx +1 -1
  46. package/gen/components/file-picker.jsx +2 -2
  47. package/gen/components/linkify-text.jsx +41 -2
  48. package/gen/components/placeholder-text.jsx +5 -1
  49. package/gen/components/quick-actions.jsx +62 -0
  50. package/gen/components/search.jsx +1 -1
  51. package/gen/components/status-call.jsx +18 -0
  52. package/gen/components/stream-view.jsx +8 -8
  53. package/gen/components/switch.jsx +0 -2
  54. package/gen/index.js +16 -0
  55. package/package.json +17 -13
  56. package/src/components/attachment.tsx +38 -37
  57. package/src/components/avatar.tsx +3 -1
  58. package/src/components/button.tsx +1 -1
  59. package/src/components/chat-bubble.tsx +176 -0
  60. package/src/components/chat-input.tsx +172 -0
  61. package/src/components/chat-list.tsx +164 -0
  62. package/src/components/checkbox.tsx +40 -0
  63. package/src/components/connectycube-ui/attachment.tsx +269 -0
  64. package/src/components/connectycube-ui/chat-input.tsx +174 -0
  65. package/src/components/connectycube-ui/chat-message.tsx +138 -0
  66. package/src/components/connectycube-ui/link-preview.tsx +149 -0
  67. package/src/components/dialog-item.tsx +5 -2
  68. package/src/components/dialogs-list.tsx +84 -0
  69. package/src/components/dismiss-layer.tsx +1 -1
  70. package/src/components/file-picker.tsx +3 -3
  71. package/src/components/linkify-text.tsx +44 -3
  72. package/src/components/placeholder-text.tsx +5 -1
  73. package/src/components/quick-actions.tsx +74 -0
  74. package/src/components/search.tsx +1 -1
  75. package/src/components/status-call.tsx +23 -0
  76. package/src/components/stream-view.tsx +8 -8
  77. package/src/components/switch.tsx +0 -2
  78. package/src/index.ts +21 -0
@@ -0,0 +1,141 @@
1
+ import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
2
+ import { useInView } from 'react-intersection-observer';
3
+ import { Avatar } from './avatar';
4
+ import { FormattedDate } from './formatted-date';
5
+ import { StatusSent } from './status-sent';
6
+ import { cn } from './utils';
7
+
8
+ function ChatBubbleBase({ onView = () => {}, isLast, children, ...props }, ref) {
9
+ const [setRef, inView] = useInView();
10
+ const messageRef = useRef(null);
11
+ const setRefs = useCallback(
12
+ (node) => {
13
+ messageRef.current = node;
14
+ setRef(node);
15
+ },
16
+ [setRef]
17
+ );
18
+
19
+ useEffect(() => {
20
+ if (inView) {
21
+ onView();
22
+ }
23
+ }, [inView, onView]);
24
+
25
+ useImperativeHandle(ref, () => messageRef.current || {}, []);
26
+
27
+ return (
28
+ <div ref={setRefs} {...props} className={cn('mt-2', isLast && 'mb-2', inView && 'view', props?.className)}>
29
+ {children}
30
+ </div>
31
+ );
32
+ }
33
+
34
+ const ChatBubble = forwardRef(ChatBubbleBase);
35
+
36
+ function ChatBubbleMessageBase(
37
+ {
38
+ fromMe,
39
+ sameSenderAbove,
40
+ title,
41
+ senderName,
42
+ senderAvatar,
43
+ date = new Date(),
44
+ language = 'en',
45
+ statusSent,
46
+ avatarProps,
47
+ bubbleProps,
48
+ titleProps,
49
+ formattedDateProps,
50
+ statusSentProps,
51
+ children,
52
+ ...props
53
+ },
54
+ ref
55
+ ) {
56
+ const hasAvatar = Boolean((avatarProps || senderAvatar) && !sameSenderAbove);
57
+ const hasAvatarMargin = Boolean((avatarProps || senderAvatar) && sameSenderAbove);
58
+
59
+ return (
60
+ <ChatBubble
61
+ ref={ref}
62
+ {...props}
63
+ className={cn(
64
+ `flex relative text-left whitespace-pre-wrap`,
65
+ fromMe ? 'self-end flex-row-reverse ml-12' : `self-start mr-12`,
66
+ sameSenderAbove && 'mt-1'
67
+ )}
68
+ >
69
+ {hasAvatar && (
70
+ <Avatar
71
+ name={senderName}
72
+ src={senderAvatar}
73
+ imageProps={{
74
+ className: 'bg-blue-200',
75
+ }}
76
+ fallbackProps={{
77
+ className: 'bg-blue-200',
78
+ }}
79
+ {...avatarProps}
80
+ className={cn('mt-1 mr-1', avatarProps?.className)}
81
+ />
82
+ )}
83
+
84
+ <div
85
+ className={cn(
86
+ 'relative flex flex-col min-w-42 max-w-120 rounded-xl px-2 pt-2 pb-6 shadow-sm',
87
+ fromMe ? 'bg-gray-200' : 'bg-blue-200',
88
+ hasAvatarMargin && 'ml-9',
89
+ bubbleProps?.className
90
+ )}
91
+ >
92
+ {(title || senderName) && (
93
+ <span
94
+ {...titleProps}
95
+ className={cn(
96
+ 'font-semibold',
97
+ title && 'mb-1 py-1.5 text-xs text-muted-foreground italic border-b',
98
+ titleProps?.className
99
+ )}
100
+ >
101
+ {title || senderName}
102
+ </span>
103
+ )}
104
+ {children}
105
+ <div className="absolute bottom-1 right-2 flex items-center gap-1 italic">
106
+ <FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
107
+ <StatusSent status={statusSent} {...statusSentProps} />
108
+ </div>
109
+ </div>
110
+ </ChatBubble>
111
+ );
112
+ }
113
+
114
+ const ChatBubbleMessage = memo(forwardRef(ChatBubbleMessageBase));
115
+
116
+ ChatBubbleMessage.displayName = 'ChatBubbleMessage';
117
+
118
+ function ChatBubbleInfoBase(
119
+ { info = '', iconElement, date = new Date(), language = 'en', infoProps, formattedDateProps, ...props },
120
+ ref
121
+ ) {
122
+ return (
123
+ <ChatBubble
124
+ ref={ref}
125
+ {...props}
126
+ 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')}
127
+ >
128
+ {iconElement}
129
+ <span {...infoProps} className={cn('text-sm mb-px', infoProps?.className)}>
130
+ {info}
131
+ </span>
132
+ <FormattedDate date={date} language={language} distanceToNow {...formattedDateProps} />
133
+ </ChatBubble>
134
+ );
135
+ }
136
+
137
+ const ChatBubbleInfo = memo(forwardRef(ChatBubbleInfoBase));
138
+
139
+ ChatBubbleInfo.displayName = 'ChatBubbleInfo';
140
+
141
+ export { ChatBubbleMessage, ChatBubbleInfo };
@@ -0,0 +1,152 @@
1
+ import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
2
+ import TextareaAutosize from 'react-textarea-autosize';
3
+ import { SendHorizontal } from 'lucide-react';
4
+ import { Label } from './label';
5
+ import { cn } from './utils';
6
+
7
+ function ChatInputSendBase({ onSend = () => {}, iconElement, iconProps, ...props }, ref) {
8
+ return (
9
+ <Label
10
+ ref={ref}
11
+ {...props}
12
+ onClick={onSend}
13
+ className={cn('group rounded-full py-2 pl-2.5 pr-1.5 cursor-pointer bg-ring/90 hover:bg-ring', props?.className)}
14
+ >
15
+ {iconElement || (
16
+ <SendHorizontal
17
+ {...iconProps}
18
+ className={cn(
19
+ 'text-background group-hover:scale-110 transition-all duration-200 ease-out',
20
+ iconProps?.className
21
+ )}
22
+ />
23
+ )}
24
+ </Label>
25
+ );
26
+ }
27
+
28
+ const ChatInputSend = forwardRef(ChatInputSendBase);
29
+
30
+ ChatInputSend.displayName = 'ChatInputSend';
31
+
32
+ function ChatInputBase(
33
+ {
34
+ pending = false,
35
+ draft,
36
+ onSend = () => {},
37
+ onDraft = () => {},
38
+ onTyping = () => {},
39
+ onHeightGrow = () => {},
40
+ hideChatInputSend = false,
41
+ chatInputSendProps,
42
+ containerProps,
43
+ children,
44
+ ...props
45
+ },
46
+ ref
47
+ ) {
48
+ const [value, setValue] = useState();
49
+ const textareaRef = useRef(null);
50
+ const textareaHeightRef = useRef(0);
51
+ const typingRef = useRef(false);
52
+ const typingTimeoutRef = useRef(undefined);
53
+ const handleStopTyping = () => {
54
+ typingRef.current = false;
55
+ onTyping(false);
56
+
57
+ if (typingTimeoutRef.current) {
58
+ clearTimeout(typingTimeoutRef.current);
59
+ typingTimeoutRef.current = undefined;
60
+ }
61
+ };
62
+ const handleOnChange = (event) => {
63
+ const { data } = event.nativeEvent;
64
+
65
+ setValue(event.target.value);
66
+
67
+ if (!typingRef.current && typeof data === 'string' && data.length > 0) {
68
+ typingRef.current = true;
69
+ onTyping(true);
70
+ }
71
+
72
+ clearTimeout(typingTimeoutRef.current);
73
+ typingTimeoutRef.current = setTimeout(handleStopTyping, 5000);
74
+ };
75
+ const handleOnSend = async () => {
76
+ handleStopTyping();
77
+
78
+ if (typeof value === 'string' && value.length > 0) {
79
+ onSend(value.trim());
80
+ setValue('');
81
+ textareaRef.current?.focus();
82
+ }
83
+ };
84
+ const handleOnHeightChange = (height, meta) => {
85
+ if (!height && !meta) return;
86
+
87
+ if (height !== textareaHeightRef.current) {
88
+ const shift = height > textareaHeightRef.current ? meta.rowHeight : -meta.rowHeight;
89
+
90
+ onHeightGrow({
91
+ height,
92
+ shift,
93
+ });
94
+ }
95
+
96
+ textareaHeightRef.current = height;
97
+ };
98
+ const handleOnKeyDown = (event) => {
99
+ if (event.key === 'Enter' && event.shiftKey === false) {
100
+ event.preventDefault();
101
+ props.onKeyDown?.(event);
102
+ handleOnSend();
103
+ }
104
+ };
105
+
106
+ useEffect(() => {
107
+ const textarea = textareaRef.current;
108
+
109
+ handleStopTyping();
110
+ queueMicrotask(() => setValue(draft));
111
+ textarea?.focus();
112
+
113
+ return () => {
114
+ onDraft(textarea?.value);
115
+ };
116
+ }, [props.key]);
117
+
118
+ useImperativeHandle(ref, () => textareaRef.current || {}, []);
119
+
120
+ return (
121
+ <div {...containerProps} className={cn('flex items-end gap-2', containerProps?.className)}>
122
+ <TextareaAutosize
123
+ name="textarea-autosize"
124
+ ref={textareaRef}
125
+ autoFocus
126
+ minRows={1}
127
+ maxRows={8}
128
+ {...props}
129
+ value={value}
130
+ onChange={handleOnChange}
131
+ onKeyDown={handleOnKeyDown}
132
+ onHeightChange={handleOnHeightChange}
133
+ className={cn(
134
+ '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',
135
+ 'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring',
136
+ 'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
137
+ 'resize-none py-2.5',
138
+ pending && 'pointer-events-none bg-muted-foreground border-ring ring-ring/50 animate-pulse',
139
+ props?.className
140
+ )}
141
+ />
142
+ {!hideChatInputSend && <ChatInputSend onSend={handleOnSend} {...chatInputSendProps} />}
143
+ {children}
144
+ </div>
145
+ );
146
+ }
147
+
148
+ const ChatInput = forwardRef(ChatInputBase);
149
+
150
+ ChatInput.displayName = 'ChatInput';
151
+
152
+ export { ChatInput };
@@ -0,0 +1,151 @@
1
+ import React, { useRef, useEffect, useState, useImperativeHandle, forwardRef, useCallback } from 'react';
2
+ import { Virtualizer } from 'virtua';
3
+ import { FilePickerDropzone } from './file-picker';
4
+ import { QuickActions } from './quick-actions';
5
+ import { Spinner } from './spinner';
6
+ import { cn } from './utils';
7
+
8
+ function DefaultChatListWrapper({ children, ...props }) {
9
+ return (
10
+ <div {...props} className={cn('relative size-full min-h-0', props?.className)}>
11
+ {children}
12
+ </div>
13
+ );
14
+ }
15
+
16
+ function ChatListBase(
17
+ {
18
+ key,
19
+ loading = false,
20
+ textareaMeasurement,
21
+ offsetToReach = 200,
22
+ onScrollStartReached,
23
+ onScrollEndReached,
24
+ onListReset,
25
+ onListCreate,
26
+ onListGrow,
27
+ enableFilePickerDropzone = true,
28
+ containerProps,
29
+ filePickerDropzoneProps,
30
+ quickActionsProps,
31
+ children,
32
+ quickActionsVisible,
33
+ minItemsCount = 1,
34
+ ...props
35
+ },
36
+ ref
37
+ ) {
38
+ const ChatListWrapper = enableFilePickerDropzone ? FilePickerDropzone : DefaultChatListWrapper;
39
+ const chatListWrapperProps = enableFilePickerDropzone ? filePickerDropzoneProps : containerProps;
40
+ const itemsCount = Array.isArray(children) ? children.length : Array.isArray(props.data) ? props.data.length : 0;
41
+ const itemsCountRef = useRef(0);
42
+ const keyRef = useRef(undefined);
43
+ const [shouldPrependMessages, setShouldPrependMessages] = useState(false);
44
+ const [isPreparing, setIsPreparing] = useState(false);
45
+ const virtuaRef = useRef(null);
46
+ const prepareNextVirtualizer = () => {
47
+ queueMicrotask(() => {
48
+ setIsPreparing(true);
49
+ queueMicrotask(() => setIsPreparing(false));
50
+ });
51
+ };
52
+ const handleOnScroll = async (offset) => {
53
+ props.onScroll?.(offset);
54
+
55
+ if (!virtuaRef.current) return;
56
+
57
+ if (typeof onScrollStartReached === 'function' && offset < offsetToReach) {
58
+ setShouldPrependMessages(true);
59
+ onScrollStartReached();
60
+ }
61
+
62
+ if (
63
+ typeof onScrollEndReached === 'function' &&
64
+ virtuaRef.current.viewportSize + offset + offsetToReach > virtuaRef.current.scrollSize
65
+ ) {
66
+ onScrollEndReached();
67
+ }
68
+ };
69
+ const scrollToBottom = useCallback(
70
+ (force = false) => {
71
+ if (!virtuaRef.current) return;
72
+
73
+ if (force || virtuaRef.current.scrollSize - virtuaRef.current.scrollOffset < virtuaRef.current.viewportSize * 2) {
74
+ queueMicrotask(() =>
75
+ virtuaRef.current?.scrollToIndex(itemsCount - 1, {
76
+ align: 'start',
77
+ })
78
+ );
79
+ }
80
+ },
81
+ [itemsCount]
82
+ );
83
+
84
+ useEffect(() => {
85
+ if (key !== keyRef.current) {
86
+ prepareNextVirtualizer();
87
+ onListReset?.(keyRef.current, key);
88
+ keyRef.current = key;
89
+ itemsCountRef.current = 0;
90
+ }
91
+ }, [key, onListReset]);
92
+
93
+ useEffect(() => {
94
+ if (itemsCountRef.current === 0 && itemsCount > 0) {
95
+ prepareNextVirtualizer();
96
+ onListCreate?.();
97
+ itemsCountRef.current = itemsCount;
98
+ scrollToBottom(true);
99
+ }
100
+
101
+ if (itemsCount > itemsCountRef.current) {
102
+ onListGrow?.();
103
+ itemsCountRef.current = itemsCount;
104
+
105
+ if (shouldPrependMessages) {
106
+ queueMicrotask(() => setShouldPrependMessages(false));
107
+ }
108
+
109
+ scrollToBottom();
110
+ }
111
+ }, [itemsCount, shouldPrependMessages, onListCreate, onListGrow, scrollToBottom]);
112
+
113
+ useEffect(() => {
114
+ virtuaRef.current?.scrollBy(textareaMeasurement.shift);
115
+ }, [textareaMeasurement]);
116
+
117
+ useImperativeHandle(
118
+ ref,
119
+ () => ({
120
+ ...(virtuaRef.current || {}),
121
+ scrollToBottom,
122
+ }),
123
+ [scrollToBottom]
124
+ );
125
+
126
+ if (isPreparing) {
127
+ return null;
128
+ }
129
+
130
+ if (minItemsCount >= itemsCount) {
131
+ return quickActionsVisible ? <QuickActions {...quickActionsProps} /> : <Spinner loading layout="centered" />;
132
+ }
133
+
134
+ return (
135
+ <ChatListWrapper {...chatListWrapperProps}>
136
+ <Spinner loading={loading} className="my-5" />
137
+ <div className="flex flex-col h-full overflow-y-auto px-2">
138
+ <div className="grow" />
139
+ <Virtualizer ref={virtuaRef} onScroll={handleOnScroll} shift={shouldPrependMessages} {...props}>
140
+ {children}
141
+ </Virtualizer>
142
+ </div>
143
+ </ChatListWrapper>
144
+ );
145
+ }
146
+
147
+ const ChatList = forwardRef(ChatListBase);
148
+
149
+ ChatList.displayName = 'ChatList';
150
+
151
+ export { ChatList };
@@ -0,0 +1,30 @@
1
+ import { forwardRef } from 'react';
2
+ import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
3
+ import { CheckIcon } from 'lucide-react';
4
+ import { cn } from './utils';
5
+
6
+ function CheckboxBase({ iconElement, iconProps, indicatorProps, ...props }, ref) {
7
+ return (
8
+ <CheckboxPrimitive.Root
9
+ ref={ref}
10
+ {...props}
11
+ className={cn(
12
+ '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',
13
+ props?.className
14
+ )}
15
+ >
16
+ <CheckboxPrimitive.Indicator
17
+ {...indicatorProps}
18
+ className={cn('flex items-center justify-center text-current transition-none', indicatorProps?.className)}
19
+ >
20
+ {iconElement || <CheckIcon {...iconProps} className={cn('size-3.5', iconProps?.className)} />}
21
+ </CheckboxPrimitive.Indicator>
22
+ </CheckboxPrimitive.Root>
23
+ );
24
+ }
25
+
26
+ const Checkbox = forwardRef(CheckboxBase);
27
+
28
+ Checkbox.displayName = 'Checkbox';
29
+
30
+ export { Checkbox };
@@ -60,7 +60,7 @@ function DialogItemBase(
60
60
  onClick={onSelect}
61
61
  className={cn(
62
62
  'flex items-start gap-2 px-2 flex-1 cursor-pointer',
63
- 'transition-colors duration-200 ease-linear',
63
+ 'transition-colors duration-200 ease-out',
64
64
  `${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
65
65
  props?.className
66
66
  )}
@@ -98,7 +98,10 @@ function DialogItemBase(
98
98
  <div {...footerProps} className={cn('flex items-start justify-between gap-1', footerProps?.className)}>
99
99
  <span
100
100
  {...lastMessageProps}
101
- className={cn('text-sm text-left text-muted-foreground line-clamp-2', lastMessageProps?.className)}
101
+ className={cn(
102
+ 'text-sm text-left text-muted-foreground break-all line-clamp-2',
103
+ lastMessageProps?.className
104
+ )}
102
105
  >
103
106
  {typingStatusText ||
104
107
  (draft ? (
@@ -0,0 +1,73 @@
1
+ import { forwardRef, useImperativeHandle, useRef } from 'react';
2
+ import { VList } from 'virtua';
3
+ import { PlaceholderText } from './placeholder-text';
4
+
5
+ const PendingItem = () => (
6
+ <div className="flex flex-row gap-2 mx-2 border-b">
7
+ <div className="size-13 my-2 rounded-full bg-muted animate-pulse" />
8
+ <div className="flex-1 text-muted">
9
+ <div className="flex flex-row items-center justify-between h-6">
10
+ <div className="w-2/3 h-4 rounded-full bg-muted animate-pulse" />
11
+ <div className="w-1/6 h-3.5 rounded-full bg-muted animate-pulse" />
12
+ </div>
13
+ <div className="flex flex-col items-start justify-around h-10">
14
+ <div className="w-7/8 h-3.5 rounded-full bg-muted animate-pulse" />
15
+ <div className="w-5/6 h-3.5 rounded-full bg-muted animate-pulse" />
16
+ </div>
17
+ </div>
18
+ </div>
19
+ );
20
+
21
+ function DialogsListBase(
22
+ {
23
+ children,
24
+ pending,
25
+ pendingListLength = 5,
26
+ offsetToReach = 50,
27
+ placeholderVisible,
28
+ placeholderTitles = ['No dialogs yet.', 'Start a conversation!'],
29
+ onScrollStartReached,
30
+ onScrollEndReached,
31
+ ...props
32
+ },
33
+ ref
34
+ ) {
35
+ const vListRef = useRef(null);
36
+ const skeletonList = Array.from({
37
+ length: pendingListLength,
38
+ }).map((_, i) => <PendingItem key={`pending_dialog_item_${i}`} />);
39
+ const handleOnScroll = async (offset) => {
40
+ props.onScroll?.(offset);
41
+
42
+ if (!vListRef.current) return;
43
+
44
+ if (typeof onScrollStartReached === 'function' && offset < offsetToReach) {
45
+ onScrollStartReached();
46
+ }
47
+
48
+ if (
49
+ typeof onScrollEndReached === 'function' &&
50
+ vListRef.current.viewportSize + offset + offsetToReach > vListRef.current.scrollSize
51
+ ) {
52
+ onScrollEndReached();
53
+ }
54
+ };
55
+
56
+ useImperativeHandle(ref, () => vListRef.current || {}, []);
57
+
58
+ if (placeholderVisible) {
59
+ return <PlaceholderText titles={placeholderTitles} className="text-base text-muted" />;
60
+ }
61
+
62
+ return (
63
+ <VList ref={vListRef} onScroll={handleOnScroll} className="size-full overflow-y-scroll" {...props}>
64
+ {pending ? skeletonList : children}
65
+ </VList>
66
+ );
67
+ }
68
+
69
+ const DialogsList = forwardRef(DialogsListBase);
70
+
71
+ DialogsList.displayName = 'DialogsList';
72
+
73
+ export { DialogsList };
@@ -7,7 +7,7 @@ function DismissLayerBase(
7
7
  ) {
8
8
  const innerRef = useRef(null);
9
9
 
10
- useImperativeHandle(ref, () => innerRef.current, []);
10
+ useImperativeHandle(ref, () => innerRef.current || {}, []);
11
11
 
12
12
  const handleClickOrTouch = useCallback(
13
13
  (e) => {
@@ -73,7 +73,7 @@ function FilePickerInputBase(
73
73
  {...labelProps}
74
74
  htmlFor="file-uploader"
75
75
  className={cn(
76
- 'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-in-out cursor-pointer',
76
+ 'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-out cursor-pointer',
77
77
  labelProps?.className
78
78
  )}
79
79
  >
@@ -81,7 +81,7 @@ function FilePickerInputBase(
81
81
  <Paperclip
82
82
  {...iconProps}
83
83
  className={cn(
84
- 'text-foreground group-hover:scale-110 transition-all duration-200 ease-in-out',
84
+ 'text-foreground group-hover:scale-110 transition-all duration-200 ease-out',
85
85
  iconProps?.className
86
86
  )}
87
87
  />
@@ -1,4 +1,4 @@
1
- import { forwardRef, memo, useMemo } from 'react';
1
+ import { forwardRef, memo, useEffect, useMemo, useRef } from 'react';
2
2
  import Linkify from 'linkify-react';
3
3
  import { cn } from './utils';
4
4
 
@@ -6,8 +6,22 @@ const DEFAULT_LINKIFY_OPTIONS = {
6
6
  target: '_blank',
7
7
  rel: 'noopener noreferrer',
8
8
  };
9
+ const DEFAULT_SKELETON_LINES_CLASS_NAMES = ['w-full', 'w-3/4', 'w-1/2'];
9
10
 
10
- function LinkifyTextBase({ text, linkifyProps, ...props }, ref) {
11
+ function LinkifyTextBase(
12
+ {
13
+ text,
14
+ pending = false,
15
+ onReady = () => {},
16
+ linkifyProps,
17
+ skeletonContainerProps,
18
+ skeletonLineProps,
19
+ skeletonLinesClassNames = DEFAULT_SKELETON_LINES_CLASS_NAMES,
20
+ ...props
21
+ },
22
+ ref
23
+ ) {
24
+ const pendingRef = useRef(pending);
11
25
  const options = useMemo(
12
26
  () => ({
13
27
  ...DEFAULT_LINKIFY_OPTIONS,
@@ -17,6 +31,31 @@ function LinkifyTextBase({ text, linkifyProps, ...props }, ref) {
17
31
  [linkifyProps]
18
32
  );
19
33
 
34
+ useEffect(() => {
35
+ if (pendingRef.current && !pending) {
36
+ onReady();
37
+ }
38
+
39
+ pendingRef.current = pending;
40
+ }, [pending, onReady]);
41
+
42
+ if (pending) {
43
+ return (
44
+ <div
45
+ {...skeletonContainerProps}
46
+ className={cn('flex items-start flex-col size-full', skeletonContainerProps?.className)}
47
+ >
48
+ {skeletonLinesClassNames.map((width, index) => (
49
+ <span
50
+ {...skeletonLineProps}
51
+ key={`${width}_${index}`}
52
+ className={cn('bg-muted-foreground animate-pulse rounded-md my-1 h-4', skeletonLineProps?.className, width)}
53
+ ></span>
54
+ ))}
55
+ </div>
56
+ );
57
+ }
58
+
20
59
  return (
21
60
  <p ref={ref} {...props} className={cn('wrap-break-word text-base', props?.className)}>
22
61
  <Linkify options={options}>{text}</Linkify>
@@ -12,7 +12,11 @@ function PlaceholderTextBase({ title, titles = [], rowProps, ...props }, ref) {
12
12
  className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', props?.className)}
13
13
  >
14
14
  {rows.map((row, index) => (
15
- <div key={`placeholder-text-${index}`} {...rowProps} className={cn('text-center', rowProps?.className)}>
15
+ <div
16
+ key={`placeholder-text-${index}`}
17
+ {...rowProps}
18
+ className={cn('text-center text-muted-foreground', rowProps?.className)}
19
+ >
16
20
  {row}
17
21
  </div>
18
22
  ))}