@connectycube/react-ui-kit 0.0.20 → 0.1.0
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.
- package/README.md +31 -4
- package/configs/dependencies.json +19 -4
- package/configs/imports.json +7 -2
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types/components/attachment.d.ts +5 -6
- package/dist/types/components/attachment.d.ts.map +1 -1
- package/dist/types/components/chat-bubble.d.ts +32 -0
- package/dist/types/components/chat-bubble.d.ts.map +1 -0
- package/dist/types/components/chat-input.d.ts +27 -0
- package/dist/types/components/chat-input.d.ts.map +1 -0
- package/dist/types/components/chat-list.d.ts +30 -0
- package/dist/types/components/chat-list.d.ts.map +1 -0
- package/dist/types/components/checkbox.d.ts +11 -0
- package/dist/types/components/checkbox.d.ts.map +1 -0
- package/dist/types/components/dialogs-list.d.ts +14 -0
- package/dist/types/components/dialogs-list.d.ts.map +1 -0
- package/dist/types/components/file-picker.d.ts +1 -1
- package/dist/types/components/file-picker.d.ts.map +1 -1
- package/dist/types/components/placeholder-text.d.ts.map +1 -1
- package/dist/types/components/quick-actions.d.ts +14 -0
- package/dist/types/components/quick-actions.d.ts.map +1 -0
- package/dist/types/components/status-call.d.ts +8 -0
- package/dist/types/components/status-call.d.ts.map +1 -0
- package/dist/types/index.d.ts +7 -2
- package/dist/types/index.d.ts.map +1 -1
- package/gen/components/attachment.jsx +6 -6
- package/gen/components/button.jsx +1 -1
- package/gen/components/{chat-message.jsx → chat-bubble.jsx} +69 -48
- package/gen/components/chat-input.jsx +152 -0
- package/gen/components/chat-list.jsx +151 -0
- package/gen/components/checkbox.jsx +30 -0
- package/gen/components/dialog-item.jsx +1 -1
- package/gen/components/dialogs-list.jsx +73 -0
- package/gen/components/dismiss-layer.jsx +1 -1
- package/gen/components/file-picker.jsx +2 -2
- package/gen/components/placeholder-text.jsx +5 -1
- package/gen/components/quick-actions.jsx +62 -0
- package/gen/components/search.jsx +1 -1
- package/gen/components/status-call.jsx +18 -0
- package/gen/components/stream-view.jsx +8 -8
- package/gen/index.js +14 -2
- package/package.json +11 -8
- package/src/components/attachment.tsx +16 -14
- package/src/components/button.tsx +1 -1
- package/src/components/chat-bubble.tsx +176 -0
- package/src/components/chat-input.tsx +172 -0
- package/src/components/chat-list.tsx +164 -0
- package/src/components/checkbox.tsx +40 -0
- package/src/components/connectycube-ui/chat-input.tsx +174 -0
- package/src/components/dialog-item.tsx +1 -1
- package/src/components/dialogs-list.tsx +84 -0
- package/src/components/dismiss-layer.tsx +1 -1
- package/src/components/file-picker.tsx +3 -3
- package/src/components/placeholder-text.tsx +5 -1
- package/src/components/quick-actions.tsx +74 -0
- package/src/components/search.tsx +1 -1
- package/src/components/status-call.tsx +23 -0
- package/src/components/stream-view.tsx +8 -8
- package/src/index.ts +17 -2
- package/dist/types/components/call-message.d.ts +0 -17
- package/dist/types/components/call-message.d.ts.map +0 -1
- package/dist/types/components/chat-message.d.ts +0 -30
- package/dist/types/components/chat-message.d.ts.map +0 -1
- package/gen/components/call-message.jsx +0 -62
- package/src/components/call-message.tsx +0 -75
- package/src/components/chat-message.tsx +0 -138
|
@@ -0,0 +1,174 @@
|
|
|
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, React.ComponentProps<'textarea'> {
|
|
15
|
+
key?: string;
|
|
16
|
+
children?: React.ReactNode;
|
|
17
|
+
pending?: boolean;
|
|
18
|
+
draft?: string;
|
|
19
|
+
onSend?: (value?: string) => void;
|
|
20
|
+
onDraft?: (value?: string) => void;
|
|
21
|
+
onTyping?: (typing?: boolean) => void;
|
|
22
|
+
onHeightGrow?: (data: { height: number; shift: number }) => void;
|
|
23
|
+
hideChatInputSend?: boolean;
|
|
24
|
+
chatInputSendProps?: ChatInputSendProps;
|
|
25
|
+
containerProps?: React.ComponentProps<'div'>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ChatInputSendBase(
|
|
29
|
+
{ onSend = () => {}, iconElement, iconProps, ...props }: ChatInputSendProps,
|
|
30
|
+
ref: React.ForwardedRef<HTMLLabelElement>
|
|
31
|
+
) {
|
|
32
|
+
return (
|
|
33
|
+
<Label
|
|
34
|
+
ref={ref}
|
|
35
|
+
{...props}
|
|
36
|
+
onClick={onSend}
|
|
37
|
+
className={cn('group rounded-full py-2 pl-2.5 pr-1.5 cursor-pointer bg-ring/90 hover:bg-ring', props?.className)}
|
|
38
|
+
>
|
|
39
|
+
{iconElement || (
|
|
40
|
+
<SendHorizontal
|
|
41
|
+
{...iconProps}
|
|
42
|
+
className={cn(
|
|
43
|
+
'size-7 text-background group-hover:scale-110 transition-all duration-200 ease-out',
|
|
44
|
+
iconProps?.className
|
|
45
|
+
)}
|
|
46
|
+
/>
|
|
47
|
+
)}
|
|
48
|
+
</Label>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const ChatInputSend = forwardRef<HTMLLabelElement, ChatInputSendProps>(ChatInputSendBase);
|
|
53
|
+
|
|
54
|
+
ChatInputSend.displayName = 'ChatInputSend';
|
|
55
|
+
|
|
56
|
+
function ChatInputBase(
|
|
57
|
+
{
|
|
58
|
+
key,
|
|
59
|
+
pending = false,
|
|
60
|
+
draft,
|
|
61
|
+
onSend = () => {},
|
|
62
|
+
onDraft = () => {},
|
|
63
|
+
onTyping = () => {},
|
|
64
|
+
onHeightGrow = () => {},
|
|
65
|
+
hideChatInputSend = false,
|
|
66
|
+
chatInputSendProps,
|
|
67
|
+
containerProps,
|
|
68
|
+
children,
|
|
69
|
+
...props
|
|
70
|
+
}: ChatInputProps,
|
|
71
|
+
ref: React.ForwardedRef<HTMLTextAreaElement>
|
|
72
|
+
) {
|
|
73
|
+
const [value, setValue] = useState<string>();
|
|
74
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
75
|
+
const textareaHeightRef = useRef<number>(0);
|
|
76
|
+
const typingRef = useRef<boolean>(false);
|
|
77
|
+
const typingTimeoutRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
78
|
+
const handleStopTyping = () => {
|
|
79
|
+
typingRef.current = false;
|
|
80
|
+
onTyping(false);
|
|
81
|
+
|
|
82
|
+
if (typingTimeoutRef.current) {
|
|
83
|
+
clearTimeout(typingTimeoutRef.current);
|
|
84
|
+
typingTimeoutRef.current = undefined;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const handleOnChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
|
88
|
+
const { data } = event.nativeEvent as InputEvent;
|
|
89
|
+
|
|
90
|
+
setValue(event.target.value);
|
|
91
|
+
|
|
92
|
+
if (!typingRef.current && typeof data === 'string' && data.length > 0) {
|
|
93
|
+
typingRef.current = true;
|
|
94
|
+
onTyping(true);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
clearTimeout(typingTimeoutRef.current);
|
|
98
|
+
typingTimeoutRef.current = setTimeout(handleStopTyping, 5000);
|
|
99
|
+
};
|
|
100
|
+
const handleOnSend = async () => {
|
|
101
|
+
handleStopTyping();
|
|
102
|
+
|
|
103
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
104
|
+
onSend(value.trim());
|
|
105
|
+
setValue('');
|
|
106
|
+
textareaRef.current?.focus();
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const handleOnHeightChange = (height: number, meta: TextareaHeightChangeMeta) => {
|
|
110
|
+
if (!height && !meta) return;
|
|
111
|
+
|
|
112
|
+
if (height !== textareaHeightRef.current) {
|
|
113
|
+
const shift = height > textareaHeightRef.current ? meta.rowHeight : -meta.rowHeight;
|
|
114
|
+
|
|
115
|
+
onHeightGrow({ height, shift });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
textareaHeightRef.current = height;
|
|
119
|
+
};
|
|
120
|
+
const handleOnKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
121
|
+
if (event.key === 'Enter' && event.shiftKey === false) {
|
|
122
|
+
event.preventDefault();
|
|
123
|
+
props.onKeyDown?.(event);
|
|
124
|
+
handleOnSend();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
const textarea = textareaRef.current;
|
|
130
|
+
|
|
131
|
+
handleStopTyping();
|
|
132
|
+
queueMicrotask(() => setValue(draft));
|
|
133
|
+
textarea?.focus();
|
|
134
|
+
|
|
135
|
+
return () => {
|
|
136
|
+
onDraft(textarea?.value);
|
|
137
|
+
};
|
|
138
|
+
}, [key]);
|
|
139
|
+
|
|
140
|
+
useImperativeHandle(ref, () => textareaRef.current!, [textareaRef]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div {...containerProps} className={cn('flex items-end gap-2', containerProps?.className)}>
|
|
144
|
+
<TextareaAutosize
|
|
145
|
+
key={key}
|
|
146
|
+
ref={textareaRef}
|
|
147
|
+
autoFocus
|
|
148
|
+
minRows={1}
|
|
149
|
+
maxRows={8}
|
|
150
|
+
{...props}
|
|
151
|
+
value={value}
|
|
152
|
+
onChange={handleOnChange}
|
|
153
|
+
onKeyDown={handleOnKeyDown}
|
|
154
|
+
onHeightChange={handleOnHeightChange}
|
|
155
|
+
className={cn(
|
|
156
|
+
'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',
|
|
157
|
+
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring',
|
|
158
|
+
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
159
|
+
'resize-none py-2.5',
|
|
160
|
+
pending && 'pointer-events-none bg-muted-foreground border-ring ring-ring/50 animate-pulse',
|
|
161
|
+
props?.className
|
|
162
|
+
)}
|
|
163
|
+
/>
|
|
164
|
+
{!hideChatInputSend && <ChatInputSend onSend={handleOnSend} {...chatInputSendProps} />}
|
|
165
|
+
{children}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const ChatInput = forwardRef<HTMLTextAreaElement, ChatInputProps>(ChatInputBase);
|
|
171
|
+
|
|
172
|
+
ChatInput.displayName = 'ChatInput';
|
|
173
|
+
|
|
174
|
+
export { ChatInput };
|
|
@@ -99,7 +99,7 @@ function DialogItemBase(
|
|
|
99
99
|
onClick={onSelect}
|
|
100
100
|
className={cn(
|
|
101
101
|
'flex items-start gap-2 px-2 flex-1 cursor-pointer',
|
|
102
|
-
'transition-colors duration-200 ease-
|
|
102
|
+
'transition-colors duration-200 ease-out',
|
|
103
103
|
`${selected ? 'border-l-[0.25em] pl-1 border-l-ring bg-ring/20' : 'hover:bg-ring/5'}`,
|
|
104
104
|
props?.className
|
|
105
105
|
)}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
|
3
|
+
import { VList, type VListHandle, type VListProps } from 'virtua';
|
|
4
|
+
import { PlaceholderText } from './placeholder-text';
|
|
5
|
+
|
|
6
|
+
interface DialogsListProps extends VListProps {
|
|
7
|
+
pending?: boolean;
|
|
8
|
+
pendingListLength?: number;
|
|
9
|
+
offsetToReach?: number;
|
|
10
|
+
placeholderVisible?: boolean;
|
|
11
|
+
placeholderTitles?: string[];
|
|
12
|
+
onScrollStartReached?: () => void;
|
|
13
|
+
onScrollEndReached?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PendingItem = () => (
|
|
17
|
+
<div className="flex flex-row gap-2 mx-2 border-b">
|
|
18
|
+
<div className="size-13 my-2 rounded-full bg-muted animate-pulse" />
|
|
19
|
+
<div className="flex-1 text-muted">
|
|
20
|
+
<div className="flex flex-row items-center justify-between h-6">
|
|
21
|
+
<div className="w-2/3 h-4 rounded-full bg-muted animate-pulse" />
|
|
22
|
+
<div className="w-1/6 h-3.5 rounded-full bg-muted animate-pulse" />
|
|
23
|
+
</div>
|
|
24
|
+
<div className="flex flex-col items-start justify-around h-10">
|
|
25
|
+
<div className="w-7/8 h-3.5 rounded-full bg-muted animate-pulse" />
|
|
26
|
+
<div className="w-5/6 h-3.5 rounded-full bg-muted animate-pulse" />
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
function DialogsListBase(
|
|
33
|
+
{
|
|
34
|
+
children,
|
|
35
|
+
pending,
|
|
36
|
+
pendingListLength = 5,
|
|
37
|
+
offsetToReach = 50,
|
|
38
|
+
placeholderVisible,
|
|
39
|
+
placeholderTitles = ['No dialogs yet.', 'Start a conversation!'],
|
|
40
|
+
onScrollStartReached,
|
|
41
|
+
onScrollEndReached,
|
|
42
|
+
...props
|
|
43
|
+
}: DialogsListProps,
|
|
44
|
+
ref: React.ForwardedRef<VListHandle>
|
|
45
|
+
) {
|
|
46
|
+
const vListRef = useRef<VListHandle>(null);
|
|
47
|
+
const skeletonList = Array.from({ length: pendingListLength }).map((_, i) => (
|
|
48
|
+
<PendingItem key={`pending_dialog_item_${i}`} />
|
|
49
|
+
));
|
|
50
|
+
const handleOnScroll = async (offset: number) => {
|
|
51
|
+
props.onScroll?.(offset);
|
|
52
|
+
|
|
53
|
+
if (!vListRef.current) return;
|
|
54
|
+
|
|
55
|
+
if (typeof onScrollStartReached === 'function' && offset < offsetToReach) {
|
|
56
|
+
onScrollStartReached();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (
|
|
60
|
+
typeof onScrollEndReached === 'function' &&
|
|
61
|
+
vListRef.current.viewportSize + offset + offsetToReach > vListRef.current.scrollSize
|
|
62
|
+
) {
|
|
63
|
+
onScrollEndReached();
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
useImperativeHandle(ref, () => vListRef.current || ({} as VListHandle), []);
|
|
68
|
+
|
|
69
|
+
if (placeholderVisible) {
|
|
70
|
+
return <PlaceholderText titles={placeholderTitles} className="text-base text-muted" />;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<VList ref={vListRef} onScroll={handleOnScroll} className="size-full overflow-y-scroll" {...props}>
|
|
75
|
+
{pending ? skeletonList : children}
|
|
76
|
+
</VList>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const DialogsList = forwardRef<VListHandle, DialogsListProps>(DialogsListBase);
|
|
81
|
+
|
|
82
|
+
DialogsList.displayName = 'DialogsList';
|
|
83
|
+
|
|
84
|
+
export { DialogsList, type DialogsListProps };
|
|
@@ -16,7 +16,7 @@ function DismissLayerBase(
|
|
|
16
16
|
) {
|
|
17
17
|
const innerRef = useRef<HTMLDivElement>(null);
|
|
18
18
|
|
|
19
|
-
useImperativeHandle(ref, () => innerRef.current
|
|
19
|
+
useImperativeHandle(ref, () => innerRef.current || ({} as HTMLDivElement), []);
|
|
20
20
|
|
|
21
21
|
const handleClickOrTouch = useCallback(
|
|
22
22
|
(e: React.MouseEvent | React.TouchEvent) => {
|
|
@@ -58,7 +58,7 @@ const handleFiles = (
|
|
|
58
58
|
};
|
|
59
59
|
|
|
60
60
|
interface FilePickerInputProps extends React.ComponentProps<'input'> {
|
|
61
|
-
onSelectFile
|
|
61
|
+
onSelectFile?: (files: File[]) => void;
|
|
62
62
|
onInvalidFile?: () => void;
|
|
63
63
|
iconElement?: React.ReactNode;
|
|
64
64
|
labelProps?: LabelProps;
|
|
@@ -97,7 +97,7 @@ function FilePickerInputBase(
|
|
|
97
97
|
{...labelProps}
|
|
98
98
|
htmlFor="file-uploader"
|
|
99
99
|
className={cn(
|
|
100
|
-
'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-
|
|
100
|
+
'group p-2 rounded-full hover:bg-ring/10 transition-all duration-200 ease-out cursor-pointer',
|
|
101
101
|
labelProps?.className
|
|
102
102
|
)}
|
|
103
103
|
>
|
|
@@ -105,7 +105,7 @@ function FilePickerInputBase(
|
|
|
105
105
|
<Paperclip
|
|
106
106
|
{...iconProps}
|
|
107
107
|
className={cn(
|
|
108
|
-
'text-foreground group-hover:scale-110 transition-all duration-200 ease-
|
|
108
|
+
'text-foreground group-hover:scale-110 transition-all duration-200 ease-out',
|
|
109
109
|
iconProps?.className
|
|
110
110
|
)}
|
|
111
111
|
/>
|
|
@@ -21,7 +21,11 @@ function PlaceholderTextBase(
|
|
|
21
21
|
className={cn('absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2', props?.className)}
|
|
22
22
|
>
|
|
23
23
|
{rows.map((row, index) => (
|
|
24
|
-
<div
|
|
24
|
+
<div
|
|
25
|
+
key={`placeholder-text-${index}`}
|
|
26
|
+
{...rowProps}
|
|
27
|
+
className={cn('text-center text-muted-foreground', rowProps?.className)}
|
|
28
|
+
>
|
|
25
29
|
{row}
|
|
26
30
|
</div>
|
|
27
31
|
))}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { forwardRef } from 'react';
|
|
3
|
+
import { cn } from './utils';
|
|
4
|
+
|
|
5
|
+
interface QuickActionsProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
title?: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
actions?: string[];
|
|
9
|
+
onAction?: (action: string) => void;
|
|
10
|
+
containerProps?: React.ComponentProps<'div'>;
|
|
11
|
+
titleProps?: React.ComponentProps<'h2'>;
|
|
12
|
+
descriptionProps?: React.ComponentProps<'span'>;
|
|
13
|
+
actionProps?: React.ComponentProps<'div'>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function QuickActionsBase(
|
|
17
|
+
{
|
|
18
|
+
title,
|
|
19
|
+
description,
|
|
20
|
+
actions = [],
|
|
21
|
+
onAction = () => {},
|
|
22
|
+
containerProps,
|
|
23
|
+
titleProps,
|
|
24
|
+
descriptionProps,
|
|
25
|
+
actionProps,
|
|
26
|
+
...props
|
|
27
|
+
}: QuickActionsProps,
|
|
28
|
+
ref: React.ForwardedRef<HTMLDivElement>
|
|
29
|
+
) {
|
|
30
|
+
if (!actions.length) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div
|
|
36
|
+
ref={ref}
|
|
37
|
+
{...containerProps}
|
|
38
|
+
className={cn('flex flex-col h-full overflow-y-auto py-2', containerProps?.className)}
|
|
39
|
+
>
|
|
40
|
+
<div className="grow" />
|
|
41
|
+
<div {...props} className={cn('grid gap-2 m-4', props?.className)}>
|
|
42
|
+
{title && (
|
|
43
|
+
<h2 {...titleProps} className={cn('font-medium text-foreground text-lg', titleProps?.className)}>
|
|
44
|
+
{title}
|
|
45
|
+
</h2>
|
|
46
|
+
)}
|
|
47
|
+
{description && (
|
|
48
|
+
<span {...descriptionProps} className={cn('text-sm text-muted-foreground mb-4', descriptionProps?.className)}>
|
|
49
|
+
{description}
|
|
50
|
+
</span>
|
|
51
|
+
)}
|
|
52
|
+
{actions.map((action, index) => (
|
|
53
|
+
<div
|
|
54
|
+
key={index}
|
|
55
|
+
{...actionProps}
|
|
56
|
+
className={cn(
|
|
57
|
+
'w-full border p-2 rounded-md wrap-break-word cursor-pointer overflow-hidden text-ellipsis bg-ring/5 hover:bg-ring/20 transition-colors duration-200 ease-out',
|
|
58
|
+
actionProps?.className
|
|
59
|
+
)}
|
|
60
|
+
onClick={() => onAction(action)}
|
|
61
|
+
>
|
|
62
|
+
{action}
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const QuickActions = forwardRef<HTMLDivElement, QuickActionsProps>(QuickActionsBase);
|
|
71
|
+
|
|
72
|
+
QuickActions.displayName = 'QuickActions';
|
|
73
|
+
|
|
74
|
+
export { QuickActions, type QuickActionsProps };
|
|
@@ -69,7 +69,7 @@ function SearchBase(
|
|
|
69
69
|
onClick={handleOnCancel}
|
|
70
70
|
{...cancelIconProps}
|
|
71
71
|
className={cn(
|
|
72
|
-
'absolute top-1/2 right-2 transform -translate-y-1/2 size-5 text-muted-foreground cursor-pointer hover:text-ring transition-all duration-300 ease-
|
|
72
|
+
'absolute top-1/2 right-2 transform -translate-y-1/2 size-5 text-muted-foreground cursor-pointer hover:text-ring transition-all duration-300 ease-out',
|
|
73
73
|
value.length > 0 ? 'opacity-100 scale-100' : 'opacity-0 scale-75 pointer-events-none',
|
|
74
74
|
cancelIconProps?.className
|
|
75
75
|
)}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Phone, PhoneIncoming, PhoneMissed, PhoneOutgoing, type LucideProps } from 'lucide-react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
|
|
4
|
+
interface StatusCallProps extends LucideProps {
|
|
5
|
+
fromMe?: boolean;
|
|
6
|
+
status?: 'reject' | 'notAnswer' | 'hungUp' | 'cancel' | undefined;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const StatusCall: React.FC<StatusCallProps> = ({ fromMe, status, ...props }) => {
|
|
10
|
+
const CallIcon =
|
|
11
|
+
status === 'hungUp' ? Phone : fromMe ? PhoneOutgoing : status === 'reject' ? PhoneIncoming : PhoneMissed;
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<CallIcon
|
|
15
|
+
{...props}
|
|
16
|
+
className={cn('size-4', status === 'hungUp' ? 'text-green-500' : 'text-red-500', props?.className)}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
StatusCall.displayName = 'StatusCall';
|
|
22
|
+
|
|
23
|
+
export { StatusCall, type StatusCallProps };
|
|
@@ -18,7 +18,7 @@ function StreamViewBase(
|
|
|
18
18
|
const defaultClassName = 'size-full object-contain';
|
|
19
19
|
const mirrorClassName = mirror ? 'scale-x-[-1]' : '';
|
|
20
20
|
|
|
21
|
-
useImperativeHandle(ref, () => innerRef.current
|
|
21
|
+
useImperativeHandle(ref, () => innerRef.current || ({} as HTMLVideoElement), []);
|
|
22
22
|
|
|
23
23
|
useEffect(() => {
|
|
24
24
|
if (innerRef.current && stream) {
|
|
@@ -146,13 +146,13 @@ function FullscreenStreamViewBase(
|
|
|
146
146
|
|
|
147
147
|
useImperativeHandle(
|
|
148
148
|
ref,
|
|
149
|
-
() =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
() => ({
|
|
150
|
+
...(innerRef.current || ({} as HTMLDivElement)),
|
|
151
|
+
isFullscreen,
|
|
152
|
+
isPictureInPicture,
|
|
153
|
+
toggleFullscreen,
|
|
154
|
+
togglePictureInPicture,
|
|
155
|
+
}),
|
|
156
156
|
[isFullscreen, isPictureInPicture, toggleFullscreen, togglePictureInPicture]
|
|
157
157
|
);
|
|
158
158
|
|
package/src/index.ts
CHANGED
|
@@ -23,12 +23,23 @@ export { Badge, type BadgeProps } from './components/badge';
|
|
|
23
23
|
|
|
24
24
|
export { Button, type ButtonProps } from './components/button';
|
|
25
25
|
|
|
26
|
-
export {
|
|
26
|
+
export {
|
|
27
|
+
ChatBubbleMessage,
|
|
28
|
+
ChatBubbleInfo,
|
|
29
|
+
type ChatBubbleMessageProps,
|
|
30
|
+
type ChatBubbleInfoProps,
|
|
31
|
+
} from './components/chat-bubble';
|
|
32
|
+
|
|
33
|
+
export { ChatInput, type ChatInputProps } from './components/chat-input';
|
|
34
|
+
|
|
35
|
+
export { ChatList, type ChatListProps, type ChatListHandle } from './components/chat-list';
|
|
27
36
|
|
|
28
|
-
export {
|
|
37
|
+
export { Checkbox, type CheckboxProps } from './components/checkbox';
|
|
29
38
|
|
|
30
39
|
export { DialogItem, type DialogItemProps } from './components/dialog-item';
|
|
31
40
|
|
|
41
|
+
export { DialogsList, type DialogsListProps } from './components/dialogs-list';
|
|
42
|
+
|
|
32
43
|
export { DismissLayer, type DismissLayerProps } from './components/dismiss-layer';
|
|
33
44
|
|
|
34
45
|
export {
|
|
@@ -58,10 +69,14 @@ export {
|
|
|
58
69
|
type PresenceBadgeProps,
|
|
59
70
|
} from './components/presence';
|
|
60
71
|
|
|
72
|
+
export { QuickActions, type QuickActionsProps } from './components/quick-actions';
|
|
73
|
+
|
|
61
74
|
export { Search, type SearchProps } from './components/search';
|
|
62
75
|
|
|
63
76
|
export { Spinner, type SpinnerProps } from './components/spinner';
|
|
64
77
|
|
|
78
|
+
export { StatusCall, type StatusCallProps } from './components/status-call';
|
|
79
|
+
|
|
65
80
|
export { StatusIndicator, type StatusName, type StatusIndicatorProps } from './components/status-indicator';
|
|
66
81
|
|
|
67
82
|
export { StatusSent, type StatusSentProps } from './components/status-sent';
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
|
-
import { type LucideProps } from 'lucide-react';
|
|
3
|
-
import { type FormattedDateProps } from './formatted-date';
|
|
4
|
-
interface CallMessageProps extends React.ComponentProps<'div'> {
|
|
5
|
-
signal?: 'reject' | 'notAnswer' | 'hungUp' | 'cancel' | undefined;
|
|
6
|
-
info?: string | undefined;
|
|
7
|
-
duration?: number | undefined;
|
|
8
|
-
fromMe: boolean;
|
|
9
|
-
isLast: boolean;
|
|
10
|
-
iconElement?: React.ReactNode;
|
|
11
|
-
iconProps?: LucideProps;
|
|
12
|
-
infoProps?: React.ComponentProps<'span'>;
|
|
13
|
-
formattedDateProps?: FormattedDateProps;
|
|
14
|
-
}
|
|
15
|
-
declare const CallMessage: React.NamedExoticComponent<Omit<CallMessageProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
16
|
-
export { CallMessage, type CallMessageProps };
|
|
17
|
-
//# sourceMappingURL=call-message.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"call-message.d.ts","sourceRoot":"","sources":["../../../src/components/call-message.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAE/B,OAAO,EAAoD,KAAK,WAAW,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAG1E,UAAU,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IAC5D,MAAM,CAAC,EAAE,QAAQ,GAAG,WAAW,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IAClE,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,WAAW,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC9B,SAAS,CAAC,EAAE,WAAW,CAAC;IACxB,SAAS,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IACzC,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;CACzC;AAsDD,QAAA,MAAM,WAAW,iGAAsE,CAAC;AAIxF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,CAAC"}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type React from 'react';
|
|
2
|
-
import { type AttachmentProps } from './attachment';
|
|
3
|
-
import { type AvatarProps } from './avatar';
|
|
4
|
-
import { type FormattedDateProps } from './formatted-date';
|
|
5
|
-
import { type LinkifyTextProps } from './linkify-text';
|
|
6
|
-
import { type LinkPreviewProps } from './link-preview';
|
|
7
|
-
import { type StatusSentProps } from './status-sent';
|
|
8
|
-
interface ChatMessageProps extends React.ComponentProps<'div'> {
|
|
9
|
-
isLast: boolean;
|
|
10
|
-
fromMe: boolean;
|
|
11
|
-
sameSenderAbove: boolean;
|
|
12
|
-
title?: string;
|
|
13
|
-
senderName?: string;
|
|
14
|
-
senderAvatar?: string;
|
|
15
|
-
attachmentElement?: React.ReactNode;
|
|
16
|
-
linkifyTextElement?: React.ReactNode;
|
|
17
|
-
linkPreviewElement?: React.ReactNode;
|
|
18
|
-
onView?: () => void;
|
|
19
|
-
avatarProps?: AvatarProps;
|
|
20
|
-
bubbleProps?: React.ComponentProps<'div'>;
|
|
21
|
-
titleProps?: React.ComponentProps<'span'>;
|
|
22
|
-
formattedDateProps?: FormattedDateProps;
|
|
23
|
-
statusSentProps?: StatusSentProps;
|
|
24
|
-
attachmentProps?: AttachmentProps;
|
|
25
|
-
linkifyTextProps?: LinkifyTextProps;
|
|
26
|
-
linkPreviewProps?: LinkPreviewProps;
|
|
27
|
-
}
|
|
28
|
-
declare const ChatMessage: React.NamedExoticComponent<Omit<ChatMessageProps, "ref"> & React.RefAttributes<HTMLDivElement>>;
|
|
29
|
-
export { ChatMessage, type ChatMessageProps };
|
|
30
|
-
//# sourceMappingURL=chat-message.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"chat-message.d.ts","sourceRoot":"","sources":["../../../src/components/chat-message.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAG/B,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,cAAc,CAAC;AAChE,OAAO,EAAU,KAAK,WAAW,EAAE,MAAM,UAAU,CAAC;AACpD,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAC1E,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAe,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACpE,OAAO,EAAc,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAGjE,UAAU,gBAAiB,SAAQ,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC;IAC5D,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,OAAO,CAAC;IAChB,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACpC,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACrC,kBAAkB,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACrC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,WAAW,CAAC,EAAE,WAAW,CAAC;IAC1B,WAAW,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC;IAC1C,UAAU,CAAC,EAAE,KAAK,CAAC,cAAc,CAAC,MAAM,CAAC,CAAC;IAC1C,kBAAkB,CAAC,EAAE,kBAAkB,CAAC;IACxC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,eAAe,CAAC,EAAE,eAAe,CAAC;IAClC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;IACpC,gBAAgB,CAAC,EAAE,gBAAgB,CAAC;CACrC;AAyGD,QAAA,MAAM,WAAW,iGAAsE,CAAC;AAExF,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,CAAC"}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { forwardRef, memo } from 'react';
|
|
2
|
-
import { Phone, PhoneIncoming, PhoneMissed, PhoneOutgoing } from 'lucide-react';
|
|
3
|
-
import { FormattedDate } from './formatted-date';
|
|
4
|
-
import { cn } from './utils';
|
|
5
|
-
|
|
6
|
-
function formatDuration(seconds) {
|
|
7
|
-
const h = Math.floor(seconds / 3600);
|
|
8
|
-
const m = Math.floor((seconds % 3600) / 60);
|
|
9
|
-
const s = seconds % 60;
|
|
10
|
-
const pad = (num) => String(num).padStart(2, '0');
|
|
11
|
-
|
|
12
|
-
return h > 0 ? `${h}:${pad(m)}:${pad(s)}` : `${pad(m)}:${pad(s)}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function CallMessageBase(
|
|
16
|
-
{
|
|
17
|
-
signal,
|
|
18
|
-
info,
|
|
19
|
-
duration = 0,
|
|
20
|
-
fromMe = false,
|
|
21
|
-
isLast = false,
|
|
22
|
-
iconElement,
|
|
23
|
-
iconProps,
|
|
24
|
-
infoProps,
|
|
25
|
-
formattedDateProps,
|
|
26
|
-
...props
|
|
27
|
-
},
|
|
28
|
-
ref
|
|
29
|
-
) {
|
|
30
|
-
const CallIcon =
|
|
31
|
-
signal === 'hungUp' ? Phone : fromMe ? (signal === 'reject' ? PhoneIncoming : PhoneOutgoing) : PhoneMissed;
|
|
32
|
-
|
|
33
|
-
return (
|
|
34
|
-
<div
|
|
35
|
-
ref={ref}
|
|
36
|
-
{...props}
|
|
37
|
-
className={cn(
|
|
38
|
-
'flex items-center justify-center gap-2 rounded-full w-fit bg-ring/20 mx-auto px-3 py-1.5',
|
|
39
|
-
isLast ? 'my-2' : 'mt-2',
|
|
40
|
-
props?.className
|
|
41
|
-
)}
|
|
42
|
-
>
|
|
43
|
-
{iconElement || (
|
|
44
|
-
<CallIcon
|
|
45
|
-
{...iconProps}
|
|
46
|
-
className={cn('size-4', signal === 'hungUp' ? 'text-green-500' : 'text-red-500', iconProps?.className)}
|
|
47
|
-
/>
|
|
48
|
-
)}
|
|
49
|
-
<span
|
|
50
|
-
{...infoProps}
|
|
51
|
-
className={cn('text-sm mb-px', infoProps?.className)}
|
|
52
|
-
>{`${info}${duration ? ` - ${formatDuration(duration)}` : ''}`}</span>
|
|
53
|
-
<FormattedDate distanceToNow {...formattedDateProps} />
|
|
54
|
-
</div>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const CallMessage = memo(forwardRef(CallMessageBase));
|
|
59
|
-
|
|
60
|
-
CallMessage.displayName = 'CallMessage';
|
|
61
|
-
|
|
62
|
-
export { CallMessage };
|