@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.
- package/configs/dependencies.json +21 -0
- package/configs/imports.json +7 -0
- package/dist/index.cjs +1 -36
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -35
- package/dist/index.js.map +1 -1
- package/dist/types/components/attachment.d.ts +7 -8
- package/dist/types/components/attachment.d.ts.map +1 -1
- package/dist/types/components/avatar.d.ts +1 -0
- package/dist/types/components/avatar.d.ts.map +1 -1
- package/dist/types/components/badge.d.ts +1 -1
- package/dist/types/components/button.d.ts +2 -2
- 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/dialog-item.d.ts.map +1 -1
- 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/linkify-text.d.ts +6 -1
- package/dist/types/components/linkify-text.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/components/switch.d.ts.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/gen/components/attachment.jsx +27 -25
- package/gen/components/avatar.jsx +14 -2
- package/gen/components/button.jsx +1 -1
- package/gen/components/chat-bubble.jsx +141 -0
- 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 +5 -2
- 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/linkify-text.jsx +41 -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/components/switch.jsx +0 -2
- package/gen/index.js +16 -0
- package/package.json +17 -13
- package/src/components/attachment.tsx +38 -37
- package/src/components/avatar.tsx +3 -1
- 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/attachment.tsx +269 -0
- package/src/components/connectycube-ui/chat-input.tsx +174 -0
- package/src/components/connectycube-ui/chat-message.tsx +138 -0
- package/src/components/connectycube-ui/link-preview.tsx +149 -0
- package/src/components/dialog-item.tsx +5 -2
- 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/linkify-text.tsx +44 -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/components/switch.tsx +0 -2
- package/src/index.ts +21 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { forwardRef } from 'react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
|
|
4
|
+
function QuickActionsBase(
|
|
5
|
+
{
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
actions = [],
|
|
9
|
+
onAction = () => {},
|
|
10
|
+
containerProps,
|
|
11
|
+
titleProps,
|
|
12
|
+
descriptionProps,
|
|
13
|
+
actionProps,
|
|
14
|
+
...props
|
|
15
|
+
},
|
|
16
|
+
ref
|
|
17
|
+
) {
|
|
18
|
+
if (!actions.length) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div
|
|
24
|
+
ref={ref}
|
|
25
|
+
{...containerProps}
|
|
26
|
+
className={cn('flex flex-col h-full overflow-y-auto py-2', containerProps?.className)}
|
|
27
|
+
>
|
|
28
|
+
<div className="grow" />
|
|
29
|
+
<div {...props} className={cn('grid gap-2 m-4', props?.className)}>
|
|
30
|
+
{title && (
|
|
31
|
+
<h2 {...titleProps} className={cn('font-medium text-foreground text-lg', titleProps?.className)}>
|
|
32
|
+
{title}
|
|
33
|
+
</h2>
|
|
34
|
+
)}
|
|
35
|
+
{description && (
|
|
36
|
+
<span {...descriptionProps} className={cn('text-sm text-muted-foreground mb-4', descriptionProps?.className)}>
|
|
37
|
+
{description}
|
|
38
|
+
</span>
|
|
39
|
+
)}
|
|
40
|
+
{actions.map((action, index) => (
|
|
41
|
+
<div
|
|
42
|
+
key={index}
|
|
43
|
+
{...actionProps}
|
|
44
|
+
className={cn(
|
|
45
|
+
'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',
|
|
46
|
+
actionProps?.className
|
|
47
|
+
)}
|
|
48
|
+
onClick={() => onAction(action)}
|
|
49
|
+
>
|
|
50
|
+
{action}
|
|
51
|
+
</div>
|
|
52
|
+
))}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const QuickActions = forwardRef(QuickActionsBase);
|
|
59
|
+
|
|
60
|
+
QuickActions.displayName = 'QuickActions';
|
|
61
|
+
|
|
62
|
+
export { QuickActions };
|
|
@@ -58,7 +58,7 @@ function SearchBase(
|
|
|
58
58
|
onClick={handleOnCancel}
|
|
59
59
|
{...cancelIconProps}
|
|
60
60
|
className={cn(
|
|
61
|
-
'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-
|
|
61
|
+
'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',
|
|
62
62
|
value.length > 0 ? 'opacity-100 scale-100' : 'opacity-0 scale-75 pointer-events-none',
|
|
63
63
|
cancelIconProps?.className
|
|
64
64
|
)}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Phone, PhoneIncoming, PhoneMissed, PhoneOutgoing } from 'lucide-react';
|
|
2
|
+
import { cn } from './utils';
|
|
3
|
+
|
|
4
|
+
const StatusCall = ({ fromMe, status, ...props }) => {
|
|
5
|
+
const CallIcon =
|
|
6
|
+
status === 'hungUp' ? Phone : fromMe ? PhoneOutgoing : status === 'reject' ? PhoneIncoming : PhoneMissed;
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<CallIcon
|
|
10
|
+
{...props}
|
|
11
|
+
className={cn('size-4', status === 'hungUp' ? 'text-green-500' : 'text-red-500', props?.className)}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
StatusCall.displayName = 'StatusCall';
|
|
17
|
+
|
|
18
|
+
export { StatusCall };
|
|
@@ -9,7 +9,7 @@ function StreamViewBase({ id, stream, mirror, className, muted, ...props }, ref)
|
|
|
9
9
|
const defaultClassName = 'size-full object-contain';
|
|
10
10
|
const mirrorClassName = mirror ? 'scale-x-[-1]' : '';
|
|
11
11
|
|
|
12
|
-
useImperativeHandle(ref, () => innerRef.current);
|
|
12
|
+
useImperativeHandle(ref, () => innerRef.current || {}, []);
|
|
13
13
|
|
|
14
14
|
useEffect(() => {
|
|
15
15
|
if (innerRef.current && stream) {
|
|
@@ -116,13 +116,13 @@ function FullscreenStreamViewBase(
|
|
|
116
116
|
|
|
117
117
|
useImperativeHandle(
|
|
118
118
|
ref,
|
|
119
|
-
() =>
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
119
|
+
() => ({
|
|
120
|
+
...(innerRef.current || {}),
|
|
121
|
+
isFullscreen,
|
|
122
|
+
isPictureInPicture,
|
|
123
|
+
toggleFullscreen,
|
|
124
|
+
togglePictureInPicture,
|
|
125
|
+
}),
|
|
126
126
|
[isFullscreen, isPictureInPicture, toggleFullscreen, togglePictureInPicture]
|
|
127
127
|
);
|
|
128
128
|
|
package/gen/index.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export { AlertDialog } from './components/alert-dialog';
|
|
2
|
+
|
|
1
3
|
export {
|
|
2
4
|
Attachment,
|
|
3
5
|
AttachmentLink,
|
|
@@ -14,8 +16,18 @@ export { Badge } from './components/badge';
|
|
|
14
16
|
|
|
15
17
|
export { Button } from './components/button';
|
|
16
18
|
|
|
19
|
+
export { ChatBubbleMessage, ChatBubbleInfo } from './components/chat-bubble';
|
|
20
|
+
|
|
21
|
+
export { ChatInput } from './components/chat-input';
|
|
22
|
+
|
|
23
|
+
export { ChatList } from './components/chat-list';
|
|
24
|
+
|
|
25
|
+
export { Checkbox } from './components/checkbox';
|
|
26
|
+
|
|
17
27
|
export { DialogItem } from './components/dialog-item';
|
|
18
28
|
|
|
29
|
+
export { DialogsList } from './components/dialogs-list';
|
|
30
|
+
|
|
19
31
|
export { DismissLayer } from './components/dismiss-layer';
|
|
20
32
|
|
|
21
33
|
export { FilePickerInput, FilePickerDropzone } from './components/file-picker';
|
|
@@ -34,10 +46,14 @@ export { PlaceholderText } from './components/placeholder-text';
|
|
|
34
46
|
|
|
35
47
|
export { Presence, PresenceBadge } from './components/presence';
|
|
36
48
|
|
|
49
|
+
export { QuickActions } from './components/quick-actions';
|
|
50
|
+
|
|
37
51
|
export { Search } from './components/search';
|
|
38
52
|
|
|
39
53
|
export { Spinner } from './components/spinner';
|
|
40
54
|
|
|
55
|
+
export { StatusCall } from './components/status-call';
|
|
56
|
+
|
|
41
57
|
export { StatusIndicator } from './components/status-indicator';
|
|
42
58
|
|
|
43
59
|
export { StatusSent } from './components/status-sent';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@connectycube/react-ui-kit",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.22",
|
|
4
4
|
"description": "Simple React UI Kit generator with TSX/JSX",
|
|
5
5
|
"homepage": "https://github.com/ConnectyCube/react-ui-kit#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -64,6 +64,7 @@
|
|
|
64
64
|
"dependencies": {
|
|
65
65
|
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
66
66
|
"@radix-ui/react-avatar": "^1.1.11",
|
|
67
|
+
"@radix-ui/react-checkbox": "^1.3.3",
|
|
67
68
|
"@radix-ui/react-label": "^2.1.8",
|
|
68
69
|
"@radix-ui/react-slot": "^1.2.4",
|
|
69
70
|
"@radix-ui/react-switch": "^1.2.6",
|
|
@@ -71,12 +72,12 @@
|
|
|
71
72
|
"class-variance-authority": "^0.7.1",
|
|
72
73
|
"clsx": "^2.1.1",
|
|
73
74
|
"date-fns": "^4.1.0",
|
|
74
|
-
"execa": "^9.6.0",
|
|
75
|
-
"fs-extra": "^11.3.2",
|
|
76
75
|
"linkify-react": "^4.3.2",
|
|
77
|
-
"lucide-react": "^0.
|
|
78
|
-
"
|
|
79
|
-
"
|
|
76
|
+
"lucide-react": "^0.562.0",
|
|
77
|
+
"react-intersection-observer": "^10.0.0",
|
|
78
|
+
"react-textarea-autosize": "^8.5.9",
|
|
79
|
+
"tailwind-merge": "^3.4.0",
|
|
80
|
+
"virtua": "^0.48.2"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|
|
82
83
|
"react": ">=18",
|
|
@@ -85,25 +86,28 @@
|
|
|
85
86
|
"devDependencies": {
|
|
86
87
|
"@babel/core": "^7.28.5",
|
|
87
88
|
"@babel/preset-typescript": "^7.28.5",
|
|
88
|
-
"@eslint/js": "^9.39.
|
|
89
|
+
"@eslint/js": "^9.39.2",
|
|
89
90
|
"@rollup/plugin-commonjs": "^29.0.0",
|
|
90
91
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
91
92
|
"@rollup/plugin-terser": "^0.4.4",
|
|
92
93
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
93
94
|
"@stylistic/eslint-plugin": "^5.6.1",
|
|
94
|
-
"@types/node": "^
|
|
95
|
+
"@types/node": "^25.0.3",
|
|
95
96
|
"@types/react": "^19.2.7",
|
|
96
|
-
"eslint": "^9.39.
|
|
97
|
+
"eslint": "^9.39.2",
|
|
97
98
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
98
|
-
"eslint-plugin-react-refresh": "^0.4.
|
|
99
|
+
"eslint-plugin-react-refresh": "^0.4.26",
|
|
100
|
+
"execa": "^9.6.1",
|
|
99
101
|
"fast-glob": "^3.3.3",
|
|
102
|
+
"fs-extra": "^11.3.3",
|
|
100
103
|
"globals": "^16.5.0",
|
|
101
|
-
"prettier": "^3.
|
|
102
|
-
"
|
|
104
|
+
"prettier": "^3.7.4",
|
|
105
|
+
"prompts": "^2.4.2",
|
|
106
|
+
"rollup": "^4.53.5",
|
|
103
107
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
104
108
|
"tslib": "^2.8.1",
|
|
105
109
|
"typescript": "^5.9.3",
|
|
106
|
-
"typescript-eslint": "^8.
|
|
110
|
+
"typescript-eslint": "^8.50.0"
|
|
107
111
|
},
|
|
108
112
|
"engines": {
|
|
109
113
|
"node": ">=18"
|
|
@@ -5,28 +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
|
-
|
|
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'>,
|
|
19
|
-
Omit<AttachmentProps, 'containerProps' & 'mimeType' & 'onReady'> {
|
|
20
|
-
children?: React.ReactNode;
|
|
21
|
-
}
|
|
17
|
+
interface AttachmentLinkProps extends React.ComponentProps<'a'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
|
|
22
18
|
|
|
23
19
|
interface AttachmentImageProps
|
|
24
|
-
extends React.ComponentProps<'img'>,
|
|
25
|
-
Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
|
|
20
|
+
extends React.ComponentProps<'img'>, Omit<AttachmentProps, 'containerProps' & 'mimeType'> {}
|
|
26
21
|
|
|
27
|
-
interface AttachmentAudioProps
|
|
28
|
-
extends React.ComponentProps<'audio'>,
|
|
29
|
-
Omit<AttachmentProps, 'linkProps' & 'mimeType' & 'onReady'> {}
|
|
22
|
+
interface AttachmentAudioProps extends React.ComponentProps<'audio'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {}
|
|
30
23
|
|
|
31
24
|
interface AttachmentVideoProps extends React.ComponentProps<'video'>, Omit<AttachmentProps, 'linkProps' & 'mimeType'> {
|
|
32
25
|
maxSize?: number;
|
|
@@ -43,7 +36,7 @@ interface AttachmentFailedProps extends LucideProps, Omit<AttachmentProps, 'link
|
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
function AttachmentLinkBase(
|
|
46
|
-
{ url,
|
|
39
|
+
{ url, pending = false, children, ...props }: AttachmentLinkProps,
|
|
47
40
|
ref: React.ForwardedRef<HTMLAnchorElement>
|
|
48
41
|
) {
|
|
49
42
|
return (
|
|
@@ -54,12 +47,12 @@ function AttachmentLinkBase(
|
|
|
54
47
|
{...props}
|
|
55
48
|
href={url}
|
|
56
49
|
className={cn(
|
|
57
|
-
'group relative min-h-
|
|
50
|
+
'group relative min-h-8 min-w-8 w-full flex items-center justify-center rounded-md overflow-hidden bg-ring/10 hover:bg-ring/20 transition-color duration-300 ease-out cursor-pointer',
|
|
58
51
|
props?.className
|
|
59
52
|
)}
|
|
60
53
|
>
|
|
61
54
|
{children}
|
|
62
|
-
<Spinner loading={
|
|
55
|
+
<Spinner loading={pending} layout="overlay" />
|
|
63
56
|
</a>
|
|
64
57
|
);
|
|
65
58
|
}
|
|
@@ -69,7 +62,7 @@ const AttachmentLink = forwardRef<HTMLAnchorElement, AttachmentLinkProps>(Attach
|
|
|
69
62
|
AttachmentLink.displayName = 'AttachmentLink';
|
|
70
63
|
|
|
71
64
|
function AttachmentAudioBase(
|
|
72
|
-
{ uid, url,
|
|
65
|
+
{ uid, url, pending = false, containerProps, ...props }: AttachmentAudioProps,
|
|
73
66
|
ref: React.ForwardedRef<HTMLAudioElement>
|
|
74
67
|
) {
|
|
75
68
|
const audioId = `attachment_audio_${uid || getRandomString()}`;
|
|
@@ -77,10 +70,10 @@ function AttachmentAudioBase(
|
|
|
77
70
|
return (
|
|
78
71
|
<div
|
|
79
72
|
{...containerProps}
|
|
80
|
-
className={cn('relative min-h-
|
|
73
|
+
className={cn('relative min-h-8 min-w-8 w-full rounded-md overflow-hidden', containerProps?.className)}
|
|
81
74
|
>
|
|
82
75
|
<audio ref={ref} src={url} id={audioId} controls {...props} />
|
|
83
|
-
<Spinner loading={
|
|
76
|
+
<Spinner loading={pending} layout="overlay" />
|
|
84
77
|
</div>
|
|
85
78
|
);
|
|
86
79
|
}
|
|
@@ -88,7 +81,7 @@ function AttachmentAudioBase(
|
|
|
88
81
|
const AttachmentAudio = forwardRef<HTMLAudioElement, AttachmentAudioProps>(AttachmentAudioBase);
|
|
89
82
|
|
|
90
83
|
function AttachmentVideoBase(
|
|
91
|
-
{ uid, url, maxSize = 360,
|
|
84
|
+
{ uid, url, maxSize = 360, pending = false, onReady = () => {}, containerProps, ...props }: AttachmentVideoProps,
|
|
92
85
|
ref: React.ForwardedRef<HTMLVideoElement>
|
|
93
86
|
) {
|
|
94
87
|
const videoId = `attachment_video_${uid || getRandomString()}`;
|
|
@@ -113,7 +106,7 @@ function AttachmentVideoBase(
|
|
|
113
106
|
}
|
|
114
107
|
};
|
|
115
108
|
|
|
116
|
-
useImperativeHandle(ref, () => playerRef.current
|
|
109
|
+
useImperativeHandle(ref, () => playerRef.current || ({} as HTMLVideoElement), []);
|
|
117
110
|
|
|
118
111
|
return (
|
|
119
112
|
<div
|
|
@@ -131,7 +124,7 @@ function AttachmentVideoBase(
|
|
|
131
124
|
onCanPlay={handleCanPlay}
|
|
132
125
|
className={cn('size-full', props?.className)}
|
|
133
126
|
/>
|
|
134
|
-
<Spinner loading={
|
|
127
|
+
<Spinner loading={pending} layout="overlay" />
|
|
135
128
|
</div>
|
|
136
129
|
);
|
|
137
130
|
}
|
|
@@ -141,7 +134,7 @@ const AttachmentVideo = forwardRef<HTMLVideoElement, AttachmentVideoProps>(Attac
|
|
|
141
134
|
AttachmentVideo.displayName = 'AttachmentVideo';
|
|
142
135
|
|
|
143
136
|
function AttachmentImageBase(
|
|
144
|
-
{ uid, url,
|
|
137
|
+
{ uid, url, pending = false, onReady = () => {}, linkProps, ...props }: AttachmentImageProps,
|
|
145
138
|
ref: React.ForwardedRef<HTMLImageElement>
|
|
146
139
|
) {
|
|
147
140
|
const imageId = `attachment_image_${uid || getRandomString()}`;
|
|
@@ -151,7 +144,7 @@ function AttachmentImageBase(
|
|
|
151
144
|
};
|
|
152
145
|
|
|
153
146
|
return (
|
|
154
|
-
<AttachmentLink href={url}
|
|
147
|
+
<AttachmentLink href={url} pending={pending} {...linkProps}>
|
|
155
148
|
<img
|
|
156
149
|
ref={ref}
|
|
157
150
|
src={url}
|
|
@@ -159,7 +152,7 @@ function AttachmentImageBase(
|
|
|
159
152
|
alt="attachment"
|
|
160
153
|
{...props}
|
|
161
154
|
className={cn(
|
|
162
|
-
'rounded-md object-cover min-h-
|
|
155
|
+
'rounded-md object-cover min-h-8 min-w-8 max-h-[360px] group-hover:scale-102 transition-transform duration-300 ease-out',
|
|
163
156
|
props?.className
|
|
164
157
|
)}
|
|
165
158
|
onLoad={handleLoad}
|
|
@@ -172,28 +165,28 @@ const AttachmentImage = forwardRef<HTMLImageElement, AttachmentImageProps>(Attac
|
|
|
172
165
|
|
|
173
166
|
AttachmentImage.displayName = 'AttachmentImage';
|
|
174
167
|
|
|
175
|
-
function AttachmentFile({ url, name,
|
|
168
|
+
function AttachmentFile({ url, name, pending = false, iconElement, linkProps, ...props }: AttachmentFileProps) {
|
|
176
169
|
const fileId = `attachment_file_${props.id || getRandomString()}`;
|
|
177
170
|
|
|
178
171
|
return (
|
|
179
172
|
<AttachmentLink
|
|
180
173
|
href={url}
|
|
181
|
-
|
|
174
|
+
pending={pending}
|
|
182
175
|
{...linkProps}
|
|
183
|
-
className={cn('flex-row gap-
|
|
176
|
+
className={cn('flex-row gap-1.5 p-2 hover:shadow', linkProps?.className)}
|
|
184
177
|
>
|
|
185
178
|
{iconElement || (
|
|
186
179
|
<File
|
|
187
180
|
id={fileId}
|
|
188
181
|
{...props}
|
|
189
182
|
className={cn(
|
|
190
|
-
'size-
|
|
183
|
+
'size-5 shrink-0 text-foreground/80 group-hover:text-foreground duration-300 ease-out',
|
|
191
184
|
props?.className
|
|
192
185
|
)}
|
|
193
186
|
/>
|
|
194
187
|
)}
|
|
195
188
|
{name && (
|
|
196
|
-
<span className="font-medium line-clamp-1 break-all text-foreground/
|
|
189
|
+
<span className="font-medium line-clamp-1 break-all text-foreground/80 group-hover:text-foreground duration-300 ease-out">
|
|
197
190
|
{name}
|
|
198
191
|
</span>
|
|
199
192
|
)}
|
|
@@ -205,7 +198,7 @@ AttachmentFile.displayName = 'AttachmentFile';
|
|
|
205
198
|
|
|
206
199
|
function AttachmentFailed({
|
|
207
200
|
name = 'Unknown file',
|
|
208
|
-
|
|
201
|
+
pending = false,
|
|
209
202
|
iconElement,
|
|
210
203
|
containerProps,
|
|
211
204
|
...props
|
|
@@ -216,7 +209,7 @@ function AttachmentFailed({
|
|
|
216
209
|
<div
|
|
217
210
|
{...containerProps}
|
|
218
211
|
className={cn(
|
|
219
|
-
'relative min-h-
|
|
212
|
+
'relative min-h-8 min-w-8 w-full flex flex-row items-center justify-center gap-2 px-2 bg-red-600/10 rounded-md overflow-hidden',
|
|
220
213
|
containerProps?.className
|
|
221
214
|
)}
|
|
222
215
|
>
|
|
@@ -224,14 +217,20 @@ function AttachmentFailed({
|
|
|
224
217
|
<FileXCorner id={failedId} {...props} className={cn('size-6 shrink-0 text-red-600', props?.className)} />
|
|
225
218
|
)}
|
|
226
219
|
<span className="font-medium line-clamp-1 break-all text-red-600">{name}</span>
|
|
227
|
-
<Spinner loading={
|
|
220
|
+
<Spinner loading={pending} layout="overlay" />
|
|
228
221
|
</div>
|
|
229
222
|
);
|
|
230
223
|
}
|
|
231
224
|
|
|
232
225
|
AttachmentFailed.displayName = 'AttachmentFailed';
|
|
233
226
|
|
|
234
|
-
function AttachmentBase({
|
|
227
|
+
function AttachmentBase({
|
|
228
|
+
mimeType,
|
|
229
|
+
onReady = () => {},
|
|
230
|
+
containerProps = {},
|
|
231
|
+
linkProps = {},
|
|
232
|
+
...props
|
|
233
|
+
}: AttachmentProps) {
|
|
235
234
|
const [type = ''] = mimeType?.split('/') || [];
|
|
236
235
|
|
|
237
236
|
if (!props.url) {
|
|
@@ -240,18 +239,20 @@ function AttachmentBase({ mimeType, ...props }: AttachmentProps) {
|
|
|
240
239
|
|
|
241
240
|
switch (type) {
|
|
242
241
|
case 'image':
|
|
243
|
-
return <AttachmentImage {...props} />;
|
|
242
|
+
return <AttachmentImage onReady={onReady} linkProps={linkProps} {...props} />;
|
|
244
243
|
case 'video':
|
|
245
|
-
return <AttachmentVideo {...props} />;
|
|
244
|
+
return <AttachmentVideo onReady={onReady} containerProps={containerProps} {...props} />;
|
|
246
245
|
case 'audio':
|
|
247
|
-
return <AttachmentAudio {...props} />;
|
|
246
|
+
return <AttachmentAudio containerProps={containerProps} {...props} />;
|
|
248
247
|
default:
|
|
249
|
-
return <AttachmentFile name={mimeType} {...props} />;
|
|
248
|
+
return <AttachmentFile name={mimeType} containerProps={containerProps} {...props} />;
|
|
250
249
|
}
|
|
251
250
|
}
|
|
252
251
|
|
|
253
252
|
const Attachment = memo(AttachmentBase);
|
|
254
253
|
|
|
254
|
+
Attachment.displayName = 'Attachment';
|
|
255
|
+
|
|
255
256
|
export {
|
|
256
257
|
Attachment,
|
|
257
258
|
AttachmentLink,
|
|
@@ -7,6 +7,7 @@ import { cn } from './utils';
|
|
|
7
7
|
interface AvatarProps extends AvatarPrimitive.AvatarProps {
|
|
8
8
|
src?: string | undefined;
|
|
9
9
|
name?: string | undefined;
|
|
10
|
+
fallbackIconElement?: React.ReactNode | undefined;
|
|
10
11
|
online?: boolean | undefined;
|
|
11
12
|
presence?: PresenceStatus;
|
|
12
13
|
onlineProps?: React.ComponentProps<'div'>;
|
|
@@ -26,6 +27,7 @@ function AvatarBase(
|
|
|
26
27
|
{
|
|
27
28
|
src,
|
|
28
29
|
name = 'NA',
|
|
30
|
+
fallbackIconElement,
|
|
29
31
|
online,
|
|
30
32
|
presence,
|
|
31
33
|
className,
|
|
@@ -50,7 +52,7 @@ function AvatarBase(
|
|
|
50
52
|
{...fallbackProps}
|
|
51
53
|
className={cn('bg-muted size-full rounded-full flex items-center justify-center', fallbackProps?.className)}
|
|
52
54
|
>
|
|
53
|
-
{initials}
|
|
55
|
+
{fallbackIconElement || initials}
|
|
54
56
|
</AvatarPrimitive.Fallback>
|
|
55
57
|
{online && (
|
|
56
58
|
<div
|
|
@@ -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-
|
|
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 };
|