@banbox/chat 1.0.1 → 1.0.2
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/dist/index.cjs +564 -576
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +43 -41
- package/dist/index.d.ts +43 -41
- package/dist/index.js +486 -499
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/chat/InboxPopup.tsx +51 -46
- package/src/chat/SinglePopup.tsx +47 -58
- package/src/lottie/banbox-chat-globe.json +1 -0
- package/src/ui/chat/AttachmentPreviewStrip.tsx +63 -112
- package/src/ui/chat/ChatComposerBar.tsx +22 -38
- package/src/ui/chat/ChatFooter.tsx +1 -1
- package/src/ui/chat/ChatIdentity.tsx +148 -145
- package/src/ui/chat/ChatListHeader.tsx +60 -83
- package/src/ui/chat/ChatMessageItem.tsx +193 -214
- package/src/ui/chat/ChatThreadItem.tsx +133 -140
- package/src/ui/chat/MessageHoverActions.tsx +136 -120
- package/src/ui/chat/TypingIndicator.tsx +23 -43
- package/src/ui/chat/drop-up/BusinessCardDropup.tsx +9 -1
- package/src/ui/chat/types.ts +42 -37
|
@@ -1,140 +1,133 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
"
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
</div>
|
|
135
|
-
</div>
|
|
136
|
-
</button>
|
|
137
|
-
);
|
|
138
|
-
};
|
|
139
|
-
|
|
140
|
-
export default ChatThreadItem;
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import clsx from "clsx";
|
|
4
|
+
import React from "react";
|
|
5
|
+
import { BlueBadgeIcon } from "../../icons";
|
|
6
|
+
|
|
7
|
+
export type ChatThreadStatus =
|
|
8
|
+
| { kind: "seen" }
|
|
9
|
+
| { kind: "delivered" }
|
|
10
|
+
| { kind: "new"; count: number };
|
|
11
|
+
|
|
12
|
+
type Props = {
|
|
13
|
+
active?: boolean;
|
|
14
|
+
pinned?: boolean;
|
|
15
|
+
online?: boolean;
|
|
16
|
+
verified?: boolean;
|
|
17
|
+
|
|
18
|
+
title: string;
|
|
19
|
+
preview: string; // last message snippet
|
|
20
|
+
time: string; // "29 Jul 2025 16:51"
|
|
21
|
+
status: ChatThreadStatus;
|
|
22
|
+
|
|
23
|
+
avatarText: string; // e.g. "A"
|
|
24
|
+
avatarSrc?: string; // e.g. "A"
|
|
25
|
+
_size?: number;
|
|
26
|
+
avatarBg?: string; // default soft peach
|
|
27
|
+
className?: string;
|
|
28
|
+
onClick?: () => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ChatThreadItem: React.FC<Props> = ({
|
|
32
|
+
active,
|
|
33
|
+
pinned,
|
|
34
|
+
online,
|
|
35
|
+
verified,
|
|
36
|
+
title,
|
|
37
|
+
preview,
|
|
38
|
+
time,
|
|
39
|
+
status,
|
|
40
|
+
avatarText,
|
|
41
|
+
avatarSrc,
|
|
42
|
+
_size = 46,
|
|
43
|
+
avatarBg = "#FFF1EC",
|
|
44
|
+
className,
|
|
45
|
+
onClick,
|
|
46
|
+
}) => {
|
|
47
|
+
const count = status.kind === "new" ? String(Math.max(0, status.count)).padStart(2, "0") : "";
|
|
48
|
+
|
|
49
|
+
const statusEl = (() => {
|
|
50
|
+
switch (status.kind) {
|
|
51
|
+
case "seen":
|
|
52
|
+
return <span className="text-[#0D5EA8]">Seen</span>;
|
|
53
|
+
|
|
54
|
+
case "delivered":
|
|
55
|
+
return <span className="text-[#B7B7B7]">Delivered</span>;
|
|
56
|
+
|
|
57
|
+
case "new":
|
|
58
|
+
return (
|
|
59
|
+
<span className="text-[#E63946]">
|
|
60
|
+
{count} New
|
|
61
|
+
</span>
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
default:
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<button
|
|
71
|
+
onClick={onClick}
|
|
72
|
+
className={clsx(
|
|
73
|
+
"relative w-full text-left px-5 py-2 hover:bg-[#f8f8f8] focus:outline-none h-[75px]",
|
|
74
|
+
active && "bg-[#f8f8f8]",
|
|
75
|
+
className,
|
|
76
|
+
)}
|
|
77
|
+
>
|
|
78
|
+
{/* Pinned corner triangle */}
|
|
79
|
+
{pinned && (
|
|
80
|
+
<span className="absolute right-0 top-0 h-0 w-0 border-l-16 border-t-16 border-l-transparent border-t-[#FFD2BD]" />
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
<div className="flex items-start gap-3 border-b border-[#f8f8f8] pb-2">
|
|
84
|
+
{/* Avatar + online */}
|
|
85
|
+
<div className="relative mt-[2px]">
|
|
86
|
+
{avatarSrc ? (
|
|
87
|
+
<div className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-2xl border border-[#f1f1f1] relative overflow-hidden">
|
|
88
|
+
<img
|
|
89
|
+
src={avatarSrc}
|
|
90
|
+
alt={title}
|
|
91
|
+
className="h-full w-full rounded-xs object-cover border border-[#f1f1f1]"
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
) : (
|
|
95
|
+
<div
|
|
96
|
+
className="grid h-9 w-9 place-items-center rounded-xs font-semibold text-[#2c2c2c] text-2xl border border-[#f1f1f1]"
|
|
97
|
+
style={{ backgroundColor: avatarBg }}
|
|
98
|
+
>
|
|
99
|
+
{avatarText}
|
|
100
|
+
</div>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<span
|
|
104
|
+
className={`absolute bottom-0 right-0 h-2.5 w-2.5 rounded-full ${
|
|
105
|
+
online ? "bg-[#328545]" : "bg-[#eb2127]"
|
|
106
|
+
} ring-1 ring-white`}
|
|
107
|
+
/>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{/* Text block */}
|
|
111
|
+
<div className="min-w-0 flex-1">
|
|
112
|
+
<div className="flex items-center gap-1 text-sm">
|
|
113
|
+
<span className="truncate font-medium">{title}</span>
|
|
114
|
+
{verified && (
|
|
115
|
+
<span>
|
|
116
|
+
<BlueBadgeIcon className="mt-[2px]" />
|
|
117
|
+
</span>
|
|
118
|
+
)}
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div className="truncate text-xs font-normal text-[#2c2c2c]">{preview}</div>
|
|
122
|
+
|
|
123
|
+
<div className="mt-0.5 flex items-center justify-between text-xs font-normal">
|
|
124
|
+
<div>{statusEl}</div>
|
|
125
|
+
<span className="text-[#636363]">{time}</span>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
</button>
|
|
130
|
+
);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export default ChatThreadItem;
|
|
@@ -1,120 +1,136 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import { NewLanguageIcon } from "../../icons";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
mine
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
activeButtons
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
className=
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
1
|
+
// components/ui/chat/MessageHoverActions.tsx
|
|
2
|
+
"use client";
|
|
3
|
+
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import React from "react";
|
|
6
|
+
import { MessageReplayIcon, NewLanguageIcon } from "../../icons";
|
|
7
|
+
|
|
8
|
+
type ItemButton = "replay" | "translate";
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
mine: boolean;
|
|
12
|
+
onReply?: () => void;
|
|
13
|
+
onTranslate?: () => void;
|
|
14
|
+
children: React.ReactNode;
|
|
15
|
+
className?: string;
|
|
16
|
+
alwaysVisible?: boolean;
|
|
17
|
+
|
|
18
|
+
/** Which buttons to show (omit => show all) */
|
|
19
|
+
isItemButton?: ItemButton[];
|
|
20
|
+
/** Which buttons are "active/on" for styling */
|
|
21
|
+
activeButtons?: ItemButton[];
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const MessageHoverActions: React.FC<Props> = ({
|
|
25
|
+
mine,
|
|
26
|
+
onReply,
|
|
27
|
+
onTranslate,
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
alwaysVisible = false,
|
|
31
|
+
isItemButton,
|
|
32
|
+
activeButtons,
|
|
33
|
+
}) => {
|
|
34
|
+
const sidePos = mine ? "right-full" : "left-full";
|
|
35
|
+
const railNudge = mine ? "-translate-x-1.5" : "translate-x-1.5";
|
|
36
|
+
|
|
37
|
+
const showReplay = !isItemButton || isItemButton.includes("replay");
|
|
38
|
+
const showTranslate = !isItemButton || isItemButton.includes("translate");
|
|
39
|
+
const hasAny = showReplay || showTranslate;
|
|
40
|
+
|
|
41
|
+
const isActive = (k: ItemButton) => !!activeButtons?.includes(k);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={clsx("relative inline-flex group/message", className)}>
|
|
45
|
+
{children}
|
|
46
|
+
|
|
47
|
+
{hasAny && (
|
|
48
|
+
<div
|
|
49
|
+
aria-hidden
|
|
50
|
+
className={clsx(
|
|
51
|
+
"absolute inset-y-0 w-2",
|
|
52
|
+
mine ? "right-full" : "left-full",
|
|
53
|
+
"pointer-events-auto",
|
|
54
|
+
)}
|
|
55
|
+
/>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
{hasAny && (
|
|
59
|
+
<div
|
|
60
|
+
className={clsx(
|
|
61
|
+
"absolute bottom-0 pointer-events-auto transition-opacity",
|
|
62
|
+
sidePos,
|
|
63
|
+
railNudge,
|
|
64
|
+
alwaysVisible ? "opacity-100" : "opacity-0 group-hover/message:opacity-100",
|
|
65
|
+
)}
|
|
66
|
+
>
|
|
67
|
+
<div className="flex gap-2 pb-[2px]">
|
|
68
|
+
{showReplay && (
|
|
69
|
+
<div className="relative group/replay">
|
|
70
|
+
{/* Tooltip */}
|
|
71
|
+
<div className="absolute bottom-full right-0 mb-0.5 pointer-events-none opacity-0 group-hover/replay:opacity-100 transition-opacity duration-150">
|
|
72
|
+
<div className="bg-[#636363] rounded-[6px] px-2 h-[28px] flex items-center whitespace-nowrap">
|
|
73
|
+
<span className="text-[12px] text-white font-normal tracking-[0.4px]">
|
|
74
|
+
Reply
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
{/* Caret — right-aligned */}
|
|
78
|
+
<div className="flex justify-end pr-1.5">
|
|
79
|
+
<div className="w-0 h-0 border-l-[5px] border-l-transparent border-r-[5px] border-r-transparent border-t-[5px] border-t-[#636363]" />
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<button
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={(e) => {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
onReply?.();
|
|
88
|
+
}}
|
|
89
|
+
className={clsx(
|
|
90
|
+
"inline-flex h-[22px] w-[22px] items-center justify-center rounded-sm bg-white text-[#2c2c2c] shadow-[0_1px_3px_rgba(0,0,0,0.08)] hover:bg-[#f8f8f8]",
|
|
91
|
+
isActive("replay") && "bg-[#636363] text-white",
|
|
92
|
+
)}
|
|
93
|
+
>
|
|
94
|
+
<MessageReplayIcon className="h-[14px] w-[14px]" />
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
{showTranslate && (
|
|
100
|
+
<div className="relative group/translate">
|
|
101
|
+
{/* Tooltip */}
|
|
102
|
+
<div className="absolute bottom-full right-0 mb-0.5 pointer-events-none opacity-0 group-hover/translate:opacity-100 transition-opacity duration-150">
|
|
103
|
+
<div className="bg-[#636363] rounded-[6px] px-2 h-[28px] flex items-center whitespace-nowrap">
|
|
104
|
+
<span className="text-[12px] text-white font-normal leading-4 tracking-[0.4px]">
|
|
105
|
+
{isActive("translate") ? "Back to Original" : "Translate"}
|
|
106
|
+
</span>
|
|
107
|
+
</div>
|
|
108
|
+
{/* Caret — right-aligned */}
|
|
109
|
+
<div className="flex justify-end pr-1.5">
|
|
110
|
+
<div className="w-0 h-0 border-l-[5px] border-l-transparent border-r-[5px] border-r-transparent border-t-[5px] border-t-[#636363]" />
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<button
|
|
115
|
+
type="button"
|
|
116
|
+
onClick={(e) => {
|
|
117
|
+
e.stopPropagation();
|
|
118
|
+
onTranslate?.();
|
|
119
|
+
}}
|
|
120
|
+
className={clsx(
|
|
121
|
+
"inline-flex h-[22px] w-[22px] items-center justify-center rounded-sm bg-white text-[#2c2c2c] shadow-[0_1px_3px_rgba(0,0,0,0.08)] hover:bg-[#f8f8f8]",
|
|
122
|
+
isActive("translate") && "!bg-[#636363] text-white",
|
|
123
|
+
)}
|
|
124
|
+
>
|
|
125
|
+
<NewLanguageIcon className="h-[14px] w-[14px]" />
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
)}
|
|
132
|
+
</div>
|
|
133
|
+
);
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export default MessageHoverActions;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import Lottie from "lottie-react";
|
|
3
4
|
import React from "react";
|
|
4
5
|
import { cn } from "../../utils/cn";
|
|
6
|
+
import dots from "../../lottie/typingdotanimation2.json";
|
|
5
7
|
|
|
6
8
|
/* =======================
|
|
7
9
|
Types
|
|
@@ -9,51 +11,24 @@ import { cn } from "../../utils/cn";
|
|
|
9
11
|
|
|
10
12
|
type Props = {
|
|
11
13
|
/** Pixel box for the animation area */
|
|
12
|
-
size?: number;
|
|
13
|
-
loop?: boolean;
|
|
14
|
-
autoplay?: boolean;
|
|
14
|
+
size?: number; // default 18 (inside badge)
|
|
15
|
+
loop?: boolean; // default true
|
|
16
|
+
autoplay?: boolean; // default true
|
|
15
17
|
className?: string;
|
|
16
18
|
ariaLabel?: string;
|
|
19
|
+
|
|
17
20
|
/** Avatar size in px */
|
|
18
|
-
avatarSize?: number;
|
|
21
|
+
avatarSize?: number; // default 40
|
|
19
22
|
};
|
|
20
23
|
|
|
21
|
-
/* =======================
|
|
22
|
-
CSS Typing Dots (no external dependency)
|
|
23
|
-
======================= */
|
|
24
|
-
|
|
25
|
-
const TypingDots: React.FC = () => (
|
|
26
|
-
<>
|
|
27
|
-
<style>{`
|
|
28
|
-
@keyframes banbox-typing-bounce {
|
|
29
|
-
0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
|
|
30
|
-
30% { transform: translateY(-5px); opacity: 1; }
|
|
31
|
-
}
|
|
32
|
-
.banbox-typing-dot {
|
|
33
|
-
width: 7px;
|
|
34
|
-
height: 7px;
|
|
35
|
-
border-radius: 50%;
|
|
36
|
-
background: #888;
|
|
37
|
-
display: inline-block;
|
|
38
|
-
animation: banbox-typing-bounce 1.2s infinite ease-in-out;
|
|
39
|
-
}
|
|
40
|
-
.banbox-typing-dot:nth-child(1) { animation-delay: 0s; }
|
|
41
|
-
.banbox-typing-dot:nth-child(2) { animation-delay: 0.2s; }
|
|
42
|
-
.banbox-typing-dot:nth-child(3) { animation-delay: 0.4s; }
|
|
43
|
-
`}</style>
|
|
44
|
-
<span style={{ display: "inline-flex", gap: "4px", alignItems: "center", height: "16px" }}>
|
|
45
|
-
<span className="banbox-typing-dot" />
|
|
46
|
-
<span className="banbox-typing-dot" />
|
|
47
|
-
<span className="banbox-typing-dot" />
|
|
48
|
-
</span>
|
|
49
|
-
</>
|
|
50
|
-
);
|
|
51
|
-
|
|
52
24
|
/* =======================
|
|
53
25
|
Component
|
|
54
26
|
======================= */
|
|
55
27
|
|
|
56
28
|
const TypingIndicator: React.FC<Props> = ({
|
|
29
|
+
size = 18,
|
|
30
|
+
loop = true,
|
|
31
|
+
autoplay = true,
|
|
57
32
|
className,
|
|
58
33
|
ariaLabel = "Typing…",
|
|
59
34
|
avatarSize = 40,
|
|
@@ -62,7 +37,7 @@ const TypingIndicator: React.FC<Props> = ({
|
|
|
62
37
|
|
|
63
38
|
return (
|
|
64
39
|
<div
|
|
65
|
-
className={cn("flex items-end gap-[6px]", className)}
|
|
40
|
+
className={cn("relative flex items-end gap-[6px]", className)}
|
|
66
41
|
role="status"
|
|
67
42
|
aria-label={ariaLabel}
|
|
68
43
|
>
|
|
@@ -77,15 +52,20 @@ const TypingIndicator: React.FC<Props> = ({
|
|
|
77
52
|
className="h-full w-full rounded-full object-cover"
|
|
78
53
|
/>
|
|
79
54
|
|
|
80
|
-
{isOnline
|
|
81
|
-
<span className="absolute bottom-
|
|
82
|
-
)
|
|
55
|
+
{isOnline && (
|
|
56
|
+
<span className="absolute bottom-[0px] right-[0px] h-[11.25px] w-[11.25px] rounded-full bg-[#328545] ring-1 ring-white" />
|
|
57
|
+
)}
|
|
83
58
|
</div>
|
|
84
59
|
|
|
85
|
-
{/*
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
|
|
60
|
+
{/* typing lottie at bottom-right */}
|
|
61
|
+
<span className="absolute bottom-[-13px] left-[30px]">
|
|
62
|
+
<Lottie
|
|
63
|
+
animationData={dots}
|
|
64
|
+
loop={loop}
|
|
65
|
+
autoplay={autoplay}
|
|
66
|
+
style={{ width: "80px", height: "38px" }}
|
|
67
|
+
/>
|
|
68
|
+
</span>
|
|
89
69
|
</div>
|
|
90
70
|
);
|
|
91
71
|
};
|