@banbox/chat 1.0.3 → 1.0.5
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 +214 -192
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +2273 -0
- package/dist/index.d.cts +20 -14
- package/dist/index.d.ts +20 -14
- package/dist/index.js +212 -190
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
- package/src/chat/ChatRoot.tsx +15 -10
- package/src/chat/InboxPopup.tsx +97 -75
- package/src/chat/SinglePopup.tsx +40 -29
- package/src/index.ts +3 -0
- package/src/styles/index.css +231 -0
- package/src/ui/chat/ChatHeader.tsx +21 -24
- package/src/ui/chat/ChatListHeader.tsx +128 -156
- package/src/ui/chat/ChatScroll.tsx +52 -64
- package/src/ui/chat/TypingIndicator.tsx +10 -21
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@banbox/chat",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.5",
|
|
4
4
|
"description": "Banbox Chat UI components — reusable across all Banbox React/Next.js projects",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -15,12 +15,15 @@
|
|
|
15
15
|
"types": "./dist/index.d.cts",
|
|
16
16
|
"default": "./dist/index.cjs"
|
|
17
17
|
}
|
|
18
|
-
}
|
|
18
|
+
},
|
|
19
|
+
"./dist/index.css": "./dist/index.css"
|
|
19
20
|
},
|
|
20
21
|
"main": "./dist/index.cjs",
|
|
21
22
|
"module": "./dist/index.js",
|
|
22
23
|
"types": "./dist/index.d.ts",
|
|
23
|
-
"sideEffects":
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
24
27
|
"files": [
|
|
25
28
|
"dist",
|
|
26
29
|
"src",
|
|
@@ -75,8 +78,11 @@
|
|
|
75
78
|
"tailwind-merge": "^3.6.0"
|
|
76
79
|
},
|
|
77
80
|
"devDependencies": {
|
|
81
|
+
"@tailwindcss/postcss": "^4.3.0",
|
|
78
82
|
"@types/react": "^19.2.16",
|
|
79
83
|
"@types/react-dom": "^19.2.3",
|
|
84
|
+
"postcss": "^8.5.15",
|
|
85
|
+
"tailwindcss": "^4.3.0",
|
|
80
86
|
"tsup": "^8.5.0",
|
|
81
87
|
"typescript": "~6.0.3"
|
|
82
88
|
}
|
package/src/chat/ChatRoot.tsx
CHANGED
|
@@ -8,29 +8,33 @@ import { GalleryProvider } from "../contexts/GalleryProvider";
|
|
|
8
8
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
9
9
|
import InboxPopup from "./InboxPopup";
|
|
10
10
|
import SinglePopup from "./SinglePopup";
|
|
11
|
+
import type { ChatTheme } from "./InboxPopup";
|
|
11
12
|
|
|
12
13
|
export type ChatRootProps = {
|
|
13
14
|
/**
|
|
14
15
|
* The unified data adapter — provides all threads, messages, and send logic.
|
|
15
|
-
*
|
|
16
|
-
* Implement this in your host app:
|
|
17
|
-
* ```ts
|
|
18
|
-
* const adapter = createDemoChatAdapter(); // or createApiChatAdapter(...)
|
|
19
|
-
* ```
|
|
20
16
|
*/
|
|
21
17
|
adapter: ChatAdapter;
|
|
22
18
|
|
|
23
19
|
/**
|
|
24
20
|
* Optional UI callbacks — controls toast notifications, navigation,
|
|
25
21
|
* and the kebab (⋮) menu renderer.
|
|
26
|
-
*
|
|
27
|
-
* These delegate UI side-effects back to the host app so the package
|
|
28
|
-
* stays decoupled from the host's routing and notification systems.
|
|
29
22
|
*/
|
|
30
23
|
uiCallbacks?: ChatUICallbacks;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Visual theme:
|
|
27
|
+
* - "marketplace" (default) — orange primary (#ff5300)
|
|
28
|
+
* - "admin" — black primary (#1a1a1a)
|
|
29
|
+
* - custom object — { primary, primaryActive, surfaceLow }
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <ChatRoot adapter={adapter} theme="admin" />
|
|
33
|
+
*/
|
|
34
|
+
theme?: ChatTheme;
|
|
31
35
|
};
|
|
32
36
|
|
|
33
|
-
export default function ChatRoot({ adapter, uiCallbacks }: ChatRootProps) {
|
|
37
|
+
export default function ChatRoot({ adapter, uiCallbacks, theme }: ChatRootProps) {
|
|
34
38
|
const { isOpen, variant } = useChatUI();
|
|
35
39
|
|
|
36
40
|
// Lock page scroll whenever the chat is open
|
|
@@ -42,7 +46,6 @@ export default function ChatRoot({ adapter, uiCallbacks }: ChatRootProps) {
|
|
|
42
46
|
|
|
43
47
|
return createPortal(
|
|
44
48
|
// GalleryProvider is scoped to the chat only.
|
|
45
|
-
// It is completely separate from the host app's own gallery context.
|
|
46
49
|
<GalleryProvider>
|
|
47
50
|
<AnimatePresence mode="wait">
|
|
48
51
|
{isOpen && (
|
|
@@ -51,12 +54,14 @@ export default function ChatRoot({ adapter, uiCallbacks }: ChatRootProps) {
|
|
|
51
54
|
key="inbox"
|
|
52
55
|
adapter={adapter}
|
|
53
56
|
uiCallbacks={uiCallbacks}
|
|
57
|
+
theme={theme}
|
|
54
58
|
/>
|
|
55
59
|
) : (
|
|
56
60
|
<SinglePopup
|
|
57
61
|
key="single"
|
|
58
62
|
adapter={adapter}
|
|
59
63
|
uiCallbacks={uiCallbacks}
|
|
64
|
+
theme={theme}
|
|
60
65
|
/>
|
|
61
66
|
)
|
|
62
67
|
)}
|
package/src/chat/InboxPopup.tsx
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"use client";
|
|
3
3
|
|
|
4
4
|
import { motion } from "framer-motion";
|
|
5
|
-
import React, { useCallback, useEffect, useState } from "react";
|
|
5
|
+
import React, { useCallback, useEffect, useRef, useState } from "react";
|
|
6
6
|
|
|
7
|
-
import ChatConfirmModal from "../modals/chat/ChatConfirmModal";
|
|
8
7
|
import { useChatUI } from "../contexts/ChatUIContext";
|
|
9
8
|
import { useGallery } from "../contexts/GalleryContext";
|
|
9
|
+
import ChatConfirmModal from "../modals/chat/ChatConfirmModal";
|
|
10
10
|
import ChatFooter from "../ui/chat/ChatFooter";
|
|
11
11
|
import ChatHeader from "../ui/chat/ChatHeader";
|
|
12
12
|
import ChatIdentity from "../ui/chat/ChatIdentity";
|
|
@@ -17,37 +17,56 @@ import ChatScroll from "../ui/chat/ChatScroll";
|
|
|
17
17
|
import type { ChatThreadStatus } from "../ui/chat/ChatThreadItem";
|
|
18
18
|
import ChatThreadItem from "../ui/chat/ChatThreadItem";
|
|
19
19
|
import TypingIndicator from "../ui/chat/TypingIndicator";
|
|
20
|
-
import ChatSpinner from "../ui/chat/ChatSpinner";
|
|
21
20
|
import ChatImagePreviewModal from "./ChatImagePreviewModal";
|
|
22
21
|
|
|
23
|
-
import type { Thread, Message, MessageRef } from "../types";
|
|
24
22
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
23
|
+
import type { Message, MessageRef, Thread } from "../types";
|
|
24
|
+
|
|
25
|
+
/* ─── Types ─── */
|
|
26
|
+
export type ChatTheme =
|
|
27
|
+
| "marketplace"
|
|
28
|
+
| "admin"
|
|
29
|
+
| { primary?: string; primaryActive?: string; surfaceLow?: string };
|
|
25
30
|
|
|
26
|
-
/* =======================
|
|
27
|
-
Props
|
|
28
|
-
======================= */
|
|
29
31
|
export type InboxPopupProps = {
|
|
30
|
-
/** The unified data adapter — provides threads, messages, and send */
|
|
31
32
|
adapter: ChatAdapter;
|
|
32
|
-
/** UI-level callbacks (toast, navigation, kebab menu) */
|
|
33
33
|
uiCallbacks?: ChatUICallbacks;
|
|
34
|
+
/** Dynamic theme: "marketplace" (orange), "admin" (black), or custom object */
|
|
35
|
+
theme?: ChatTheme;
|
|
34
36
|
};
|
|
35
37
|
|
|
36
|
-
/*
|
|
37
|
-
Constants
|
|
38
|
-
======================= */
|
|
38
|
+
/* ─── Helpers ─── */
|
|
39
39
|
const avatarBgByInitial: Record<string, string> = {
|
|
40
40
|
K: "#FFE7DB", A: "#FFE5DA", F: "#E8F7FF", B: "#F0EDEB", b: "#F0EDEB",
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const GRADIENT_BORDER =
|
|
44
|
+
"linear-gradient(236.83deg, rgba(51,201,212,0.3) 0.4%, rgba(39,83,251,0.3) 30.28%, rgba(39,83,251,0.3) 50.2%, rgba(39,83,251,0.3) 65.14%, rgba(235,67,255,0.3) 100%)";
|
|
45
|
+
|
|
46
|
+
function getThemeAttr(theme?: ChatTheme): string {
|
|
47
|
+
if (!theme || theme === "marketplace") return "marketplace";
|
|
48
|
+
if (theme === "admin") return "admin";
|
|
49
|
+
return "custom";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
53
|
+
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
54
|
+
// Custom theme object — set CSS vars directly
|
|
55
|
+
const vars: Record<string, string> = {};
|
|
56
|
+
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
57
|
+
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
58
|
+
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
59
|
+
return vars as React.CSSProperties;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* ══════════════════════════════════════════════════
|
|
44
63
|
Component
|
|
45
|
-
|
|
46
|
-
const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
64
|
+
══════════════════════════════════════════════════ */
|
|
65
|
+
const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks, theme }) => {
|
|
47
66
|
const { close, selectThread, selectedThreadId, reference } = useChatUI();
|
|
48
67
|
const { isOpen: isGalleryOpen, closeGallery } = useGallery();
|
|
49
68
|
|
|
50
|
-
/* ───
|
|
69
|
+
/* ─── Threads ─── */
|
|
51
70
|
const [threads, setThreads] = useState<Thread[]>(() => adapter.threads.list(reference));
|
|
52
71
|
const refreshThreads = useCallback(
|
|
53
72
|
() => setThreads(adapter.threads.list(reference)),
|
|
@@ -55,7 +74,6 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
55
74
|
);
|
|
56
75
|
|
|
57
76
|
useEffect(() => {
|
|
58
|
-
// Immediate sync on mount / reference change
|
|
59
77
|
let rafId = 0;
|
|
60
78
|
rafId = requestAnimationFrame(refreshThreads);
|
|
61
79
|
const unsub = adapter.threads.subscribe(refreshThreads);
|
|
@@ -74,12 +92,10 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
74
92
|
activeId ? adapter.messages.list(activeId) : [],
|
|
75
93
|
);
|
|
76
94
|
|
|
77
|
-
// Refresh messages when active thread changes or rev bumps
|
|
78
95
|
useEffect(() => {
|
|
79
96
|
if (activeId) setMessages(adapter.messages.list(activeId));
|
|
80
97
|
}, [activeId, rev, adapter]);
|
|
81
98
|
|
|
82
|
-
// Subscribe to real-time message updates for the active thread
|
|
83
99
|
useEffect(() => {
|
|
84
100
|
if (!activeId || !adapter.messages.subscribe) return;
|
|
85
101
|
const unsub = adapter.messages.subscribe(activeId, () => {
|
|
@@ -88,7 +104,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
88
104
|
return unsub;
|
|
89
105
|
}, [activeId, adapter]);
|
|
90
106
|
|
|
91
|
-
/* ─── Derived
|
|
107
|
+
/* ─── Derived ─── */
|
|
92
108
|
const initial = activeThread?.avatarText ?? "U";
|
|
93
109
|
const title = activeThread?.title ?? "Unknown";
|
|
94
110
|
const subtitle = activeThread?.subTitle ?? "";
|
|
@@ -100,24 +116,17 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
100
116
|
const idButtonLabel = activeThread?.orderId ? "View Order" : activeThread?.inquiryId ? "View Inquiry" : undefined;
|
|
101
117
|
const idValue = activeThread?.orderId ?? activeThread?.inquiryId ?? undefined;
|
|
102
118
|
|
|
103
|
-
/* ─── Loading state ─── */
|
|
104
119
|
const [showDelete, setShowDelete] = useState(false);
|
|
105
|
-
const [isLoading, setIsLoading] = useState(false);
|
|
106
120
|
const scrollKey = `${activeId}-${messages.length}-${rev}`;
|
|
107
121
|
|
|
108
|
-
const prevActiveIdRef =
|
|
122
|
+
const prevActiveIdRef = useRef(activeId);
|
|
109
123
|
useEffect(() => {
|
|
110
124
|
if (prevActiveIdRef.current !== activeId) {
|
|
111
125
|
prevActiveIdRef.current = activeId;
|
|
112
|
-
setIsLoading(true);
|
|
113
|
-
const t = setTimeout(() => setIsLoading(false), 300);
|
|
114
|
-
// Mark thread as read when switching to it
|
|
115
126
|
if (activeId) adapter.threads.markRead?.(activeId);
|
|
116
|
-
return () => clearTimeout(t);
|
|
117
127
|
}
|
|
118
128
|
}, [activeId, adapter]);
|
|
119
129
|
|
|
120
|
-
/* ─── Reply helper ─── */
|
|
121
130
|
const toRef = (m: Message): MessageRef => ({
|
|
122
131
|
id: m.id,
|
|
123
132
|
author: typeof m.author === "string" ? m.author : "U",
|
|
@@ -128,7 +137,6 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
128
137
|
audio: m.audio,
|
|
129
138
|
});
|
|
130
139
|
|
|
131
|
-
/* ─── Delete ─── */
|
|
132
140
|
const handleConfirmDelete = () => {
|
|
133
141
|
if (!activeId) { setShowDelete(false); return; }
|
|
134
142
|
adapter.threads.delete(activeId);
|
|
@@ -136,52 +144,69 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
136
144
|
if (nextId) selectThread(nextId);
|
|
137
145
|
setReplyTo(undefined);
|
|
138
146
|
setShowDelete(false);
|
|
139
|
-
uiCallbacks?.showToast?.({
|
|
140
|
-
type: "success",
|
|
141
|
-
title: "Chat Deleted",
|
|
142
|
-
message: "The chat has been deleted successfully.",
|
|
143
|
-
});
|
|
147
|
+
uiCallbacks?.showToast?.({ type: "success", title: "Chat Deleted", message: "The chat has been deleted successfully." });
|
|
144
148
|
};
|
|
145
149
|
|
|
150
|
+
const filteredThreads = threads.filter((t) => {
|
|
151
|
+
if (!searchQuery.trim()) return true;
|
|
152
|
+
const q = searchQuery.toLowerCase();
|
|
153
|
+
return (
|
|
154
|
+
t.title.toLowerCase().includes(q) ||
|
|
155
|
+
t.last?.toLowerCase().includes(q) ||
|
|
156
|
+
t.orderId?.toLowerCase().includes(q) ||
|
|
157
|
+
t.inquiryId?.toLowerCase().includes(q)
|
|
158
|
+
);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/* ══════════════════════════════════════════════════
|
|
162
|
+
RENDER
|
|
163
|
+
══════════════════════════════════════════════════ */
|
|
146
164
|
return (
|
|
147
165
|
<div className="fixed bottom-4 right-4 z-[10002]">
|
|
148
166
|
{/* Backdrop */}
|
|
149
167
|
<motion.button
|
|
150
168
|
aria-label="Close chat"
|
|
151
169
|
onClick={close}
|
|
152
|
-
className="fixed inset-0"
|
|
170
|
+
className="fixed inset-0 cursor-auto!"
|
|
171
|
+
style={{ background: "transparent", border: "none" }}
|
|
153
172
|
initial={{ opacity: 0 }}
|
|
154
173
|
animate={{ opacity: 1 }}
|
|
155
174
|
exit={{ opacity: 0 }}
|
|
156
175
|
transition={{ type: "tween", duration: 0.25 }}
|
|
157
176
|
/>
|
|
158
177
|
|
|
159
|
-
{/*
|
|
178
|
+
{/* Outer gradient border + theme root */}
|
|
160
179
|
<motion.div
|
|
161
180
|
role="dialog"
|
|
162
181
|
aria-modal="true"
|
|
163
|
-
|
|
182
|
+
data-theme={getThemeAttr(theme)}
|
|
183
|
+
className="banbox-chat-root relative rounded-[20px] p-[3px]"
|
|
164
184
|
style={{
|
|
165
185
|
width: 800,
|
|
166
186
|
height: 650,
|
|
167
187
|
boxShadow: "0px 2px 12px 0px #3B33331A",
|
|
168
|
-
background:
|
|
169
|
-
|
|
188
|
+
background: GRADIENT_BORDER,
|
|
189
|
+
...getThemeVars(theme),
|
|
170
190
|
}}
|
|
171
191
|
initial={{ x: "110%" }}
|
|
172
192
|
animate={{ x: 0 }}
|
|
173
193
|
exit={{ x: "110%" }}
|
|
174
194
|
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
|
|
175
195
|
>
|
|
196
|
+
{/* Inner white card */}
|
|
176
197
|
<div
|
|
177
198
|
className="relative h-full w-full overflow-hidden rounded-[18px] bg-white"
|
|
178
199
|
style={{ overscrollBehavior: "contain" }}
|
|
179
200
|
>
|
|
180
201
|
<div className="pointer-events-none absolute inset-0 rounded-[14px] ring-1 ring-[#2F80ED]/40" />
|
|
181
202
|
|
|
182
|
-
|
|
183
|
-
|
|
203
|
+
{/* TWO-COLUMN GRID */}
|
|
204
|
+
<div className="grid h-full min-h-0 grid-cols-[1fr_310px]">
|
|
205
|
+
|
|
206
|
+
{/* ════ LEFT — chat ════ */}
|
|
184
207
|
<div className="flex h-full min-h-0 flex-col border-r border-[#9BBCCF]">
|
|
208
|
+
|
|
209
|
+
{/* Header */}
|
|
185
210
|
<div className="h-[64px] shrink-0">
|
|
186
211
|
<ChatHeader
|
|
187
212
|
left={
|
|
@@ -201,6 +226,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
201
226
|
/>
|
|
202
227
|
</div>
|
|
203
228
|
|
|
229
|
+
{/* Optional inquiry bar */}
|
|
204
230
|
{idValue && (
|
|
205
231
|
<div className="shrink-0">
|
|
206
232
|
<ChatInquiryBar
|
|
@@ -215,6 +241,7 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
215
241
|
</div>
|
|
216
242
|
)}
|
|
217
243
|
|
|
244
|
+
{/* Messages + typing */}
|
|
218
245
|
<div className="flex-1 min-h-0">
|
|
219
246
|
<div className="relative h-full min-h-0">
|
|
220
247
|
<ChatScroll
|
|
@@ -249,12 +276,14 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
249
276
|
})}
|
|
250
277
|
</ChatScroll>
|
|
251
278
|
|
|
279
|
+
{/* Typing indicator — pinned at bottom */}
|
|
252
280
|
<div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-start px-4 pb-2 pt-1 bg-white">
|
|
253
281
|
<TypingIndicator className="pointer-events-auto" />
|
|
254
282
|
</div>
|
|
255
283
|
</div>
|
|
256
284
|
</div>
|
|
257
285
|
|
|
286
|
+
{/* Footer */}
|
|
258
287
|
<div className="shrink-0">
|
|
259
288
|
<ChatFooter
|
|
260
289
|
key={activeId}
|
|
@@ -268,44 +297,37 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
268
297
|
</div>
|
|
269
298
|
</div>
|
|
270
299
|
|
|
271
|
-
{/* RIGHT —
|
|
272
|
-
<div className="h-full min-h-0">
|
|
273
|
-
<
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
t.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
preview={t.last ?? ""}
|
|
298
|
-
time={t.time ?? ""}
|
|
299
|
-
status={status}
|
|
300
|
-
avatarText={t.avatarText ?? ""}
|
|
301
|
-
avatarSrc={t.avatarSrc}
|
|
302
|
-
/>
|
|
303
|
-
);
|
|
304
|
-
})}
|
|
300
|
+
{/* ════ RIGHT — thread list ════ */}
|
|
301
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
302
|
+
<div className="shrink-0">
|
|
303
|
+
<ChatListHeader onClose={close} onSearchChange={(val) => setSearchQuery(val)} />
|
|
304
|
+
</div>
|
|
305
|
+
<div className="flex-1 min-h-0 overflow-y-auto custom-scroll">
|
|
306
|
+
{filteredThreads.map((t) => {
|
|
307
|
+
const status: ChatThreadStatus =
|
|
308
|
+
t.status ?? (t.unread && t.unread > 0 ? { kind: "new", count: t.unread } : { kind: "seen" });
|
|
309
|
+
return (
|
|
310
|
+
<ChatThreadItem
|
|
311
|
+
key={t.id}
|
|
312
|
+
onClick={() => { setReplyTo(undefined); selectThread(t.id); }}
|
|
313
|
+
active={t.id === activeId}
|
|
314
|
+
pinned={Boolean(t.pinned)}
|
|
315
|
+
online={t.online}
|
|
316
|
+
verified={Boolean(t.badge)}
|
|
317
|
+
title={t.title}
|
|
318
|
+
preview={t.last ?? ""}
|
|
319
|
+
time={t.time ?? ""}
|
|
320
|
+
status={status}
|
|
321
|
+
avatarText={t.avatarText ?? ""}
|
|
322
|
+
avatarSrc={t.avatarSrc}
|
|
323
|
+
/>
|
|
324
|
+
);
|
|
325
|
+
})}
|
|
305
326
|
</div>
|
|
306
327
|
</div>
|
|
307
328
|
</div>
|
|
308
329
|
|
|
330
|
+
{/* Modals */}
|
|
309
331
|
<ChatConfirmModal open={showDelete} onClose={() => setShowDelete(false)} onConfirm={handleConfirmDelete} />
|
|
310
332
|
<ChatImagePreviewModal isOpen={isGalleryOpen} onClose={closeGallery} />
|
|
311
333
|
</div>
|
package/src/chat/SinglePopup.tsx
CHANGED
|
@@ -13,12 +13,13 @@ import ChatMessageItem from "../ui/chat/ChatMessageItem";
|
|
|
13
13
|
import ChatScroll from "../ui/chat/ChatScroll";
|
|
14
14
|
import TypingIndicator from "../ui/chat/TypingIndicator";
|
|
15
15
|
|
|
16
|
-
import type { Thread, Message, MessageRef, Reference } from "../types";
|
|
17
16
|
import type { ChatAdapter, ChatUICallbacks } from "../adapter/types";
|
|
17
|
+
import type { Message, MessageRef, Reference, Thread } from "../types";
|
|
18
|
+
import type { ChatTheme } from "./InboxPopup";
|
|
18
19
|
|
|
19
|
-
/*
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/* ─── Helpers ─── */
|
|
21
|
+
const GRADIENT_BORDER =
|
|
22
|
+
"linear-gradient(236.83deg, rgba(51,201,212,0.3) 0.4%, rgba(39,83,251,0.3) 30.28%, rgba(39,83,251,0.3) 50.2%, rgba(39,83,251,0.3) 65.14%, rgba(235,67,255,0.3) 100%)";
|
|
22
23
|
|
|
23
24
|
function coalesceThreadId(reference: Reference | undefined, threads: Thread[]): string {
|
|
24
25
|
const referenceId = reference?.id;
|
|
@@ -45,22 +46,31 @@ function toRef(m: Message): MessageRef {
|
|
|
45
46
|
};
|
|
46
47
|
}
|
|
47
48
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
49
|
+
function getThemeAttr(theme?: ChatTheme): string {
|
|
50
|
+
if (!theme || theme === "marketplace") return "marketplace";
|
|
51
|
+
if (theme === "admin") return "admin";
|
|
52
|
+
return "custom";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getThemeVars(theme?: ChatTheme): React.CSSProperties {
|
|
56
|
+
if (!theme || theme === "marketplace" || theme === "admin") return {};
|
|
57
|
+
const vars: Record<string, string> = {};
|
|
58
|
+
if (theme.primary) vars["--color-banbox-primary"] = theme.primary;
|
|
59
|
+
if (theme.primaryActive) vars["--color-banbox-primary-active"] = theme.primaryActive;
|
|
60
|
+
if (theme.surfaceLow) vars["--color-banbox-surface-container-low"] = theme.surfaceLow;
|
|
61
|
+
return vars as React.CSSProperties;
|
|
62
|
+
}
|
|
51
63
|
|
|
52
64
|
export type SinglePopupProps = {
|
|
53
|
-
/** The unified data adapter */
|
|
54
65
|
adapter: ChatAdapter;
|
|
55
|
-
/** UI-level callbacks (toast, navigation) */
|
|
56
66
|
uiCallbacks?: ChatUICallbacks;
|
|
67
|
+
theme?: ChatTheme;
|
|
57
68
|
};
|
|
58
69
|
|
|
59
|
-
/*
|
|
70
|
+
/* ══════════════════════════════════════════════════
|
|
60
71
|
Component
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
72
|
+
══════════════════════════════════════════════════ */
|
|
73
|
+
const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme }) => {
|
|
64
74
|
const { close, reference } = useChatUI();
|
|
65
75
|
|
|
66
76
|
const threads = adapter.threads.list(reference);
|
|
@@ -86,14 +96,12 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
86
96
|
const online = meta.online ?? activeThread?.online ?? true;
|
|
87
97
|
const subtitle = meta.subtitle ?? "Customer";
|
|
88
98
|
|
|
89
|
-
/* ─── Messages ─── */
|
|
90
99
|
const [messages, setMessages] = React.useState<Message[]>(() =>
|
|
91
100
|
activeId ? adapter.messages.list(activeId) : [],
|
|
92
101
|
);
|
|
93
102
|
const [scrollKey, setScrollKey] = React.useState<number>(Date.now());
|
|
94
103
|
const [replyTo, setReplyTo] = React.useState<MessageRef | undefined>(undefined);
|
|
95
104
|
|
|
96
|
-
// Subscribe to real-time updates
|
|
97
105
|
React.useEffect(() => {
|
|
98
106
|
if (!activeId || !adapter.messages.subscribe) return;
|
|
99
107
|
const unsub = adapter.messages.subscribe(activeId, () => {
|
|
@@ -109,9 +117,6 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
109
117
|
setReplyTo(undefined);
|
|
110
118
|
}, [activeId, adapter]);
|
|
111
119
|
|
|
112
|
-
const statusText = activeThread?.status?.kind === "seen" ? "Seen" : "Delivered";
|
|
113
|
-
|
|
114
|
-
/* ─── Unused callbacks acknowledged ─── */
|
|
115
120
|
void uiCallbacks;
|
|
116
121
|
|
|
117
122
|
return (
|
|
@@ -121,33 +126,35 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
121
126
|
aria-label="Close chat"
|
|
122
127
|
onClick={close}
|
|
123
128
|
className="fixed inset-0 cursor-auto!"
|
|
129
|
+
style={{ background: "transparent", border: "none" }}
|
|
124
130
|
initial={{ opacity: 0 }}
|
|
125
131
|
animate={{ opacity: 1 }}
|
|
126
132
|
exit={{ opacity: 0 }}
|
|
127
133
|
transition={{ type: "tween", duration: 0.25 }}
|
|
128
134
|
/>
|
|
129
135
|
|
|
130
|
-
{/* Outer gradient
|
|
136
|
+
{/* Outer gradient border + theme root */}
|
|
131
137
|
<motion.div
|
|
132
138
|
role="dialog"
|
|
133
139
|
aria-modal="true"
|
|
134
|
-
|
|
140
|
+
data-theme={getThemeAttr(theme)}
|
|
141
|
+
className="banbox-chat-root relative h-[650px] w-[450px] rounded-[20px] p-[2px]"
|
|
135
142
|
style={{
|
|
136
143
|
boxShadow: "0px 2px 12px 0px #3B33331A",
|
|
137
|
-
background:
|
|
138
|
-
|
|
144
|
+
background: GRADIENT_BORDER,
|
|
145
|
+
...getThemeVars(theme),
|
|
139
146
|
}}
|
|
140
147
|
initial={{ x: "110%" }}
|
|
141
148
|
animate={{ x: 0 }}
|
|
142
149
|
exit={{ x: "110%" }}
|
|
143
150
|
transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
|
|
144
151
|
>
|
|
145
|
-
{/* Inner card */}
|
|
152
|
+
{/* Inner white card */}
|
|
146
153
|
<div
|
|
147
154
|
className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
|
|
148
155
|
style={{ overscrollBehavior: "contain" }}
|
|
149
156
|
>
|
|
150
|
-
{/* Header */}
|
|
157
|
+
{/* Header — 64px */}
|
|
151
158
|
<div className="h-[64px] shrink-0">
|
|
152
159
|
<ChatHeader
|
|
153
160
|
left={
|
|
@@ -165,7 +172,7 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
165
172
|
<button
|
|
166
173
|
type="button"
|
|
167
174
|
onClick={close}
|
|
168
|
-
className="flex h-[34px] w-[34px] items-center justify-center rounded-full bg-white text-black shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5 hover:text-[
|
|
175
|
+
className="flex h-[34px] w-[34px] items-center justify-center rounded-full bg-white text-black shadow-[0px_2px_4px_0px_#A5A3AE4D] hover:bg-black/5 hover:text-[var(--color-banbox-warning)] cursor-pointer border-none"
|
|
169
176
|
>
|
|
170
177
|
<ChatXIcon className="h-6 w-6" />
|
|
171
178
|
</button>
|
|
@@ -173,9 +180,13 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
173
180
|
/>
|
|
174
181
|
</div>
|
|
175
182
|
|
|
176
|
-
{/* Messages */}
|
|
183
|
+
{/* Messages — flex-1 */}
|
|
177
184
|
<div className="flex-1 min-h-0">
|
|
178
|
-
<ChatScroll
|
|
185
|
+
<ChatScroll
|
|
186
|
+
className="h-full"
|
|
187
|
+
bottomAlignWhenShort={false}
|
|
188
|
+
scrollKey={scrollKey}
|
|
189
|
+
>
|
|
179
190
|
{messages.map((m, idx) => {
|
|
180
191
|
const mine = m.author === "you";
|
|
181
192
|
const isLast = idx === messages.length - 1;
|
|
@@ -195,13 +206,13 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
|
|
|
195
206
|
replyTo={m.replyTo}
|
|
196
207
|
initialSrc={m.avatarSrc}
|
|
197
208
|
showStatus={isLast}
|
|
198
|
-
status={
|
|
209
|
+
status={activeThread?.status?.kind === "seen" ? "Seen" : "Delivered"}
|
|
199
210
|
onReply={() => setReplyTo(toRef(m))}
|
|
200
211
|
/>
|
|
201
212
|
);
|
|
202
213
|
})}
|
|
203
214
|
|
|
204
|
-
{/* Typing */}
|
|
215
|
+
{/* Typing indicator */}
|
|
205
216
|
<div className="flex items-center justify-start">
|
|
206
217
|
<TypingIndicator />
|
|
207
218
|
</div>
|
package/src/index.ts
CHANGED
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
export { default as ChatRoot } from "./chat/ChatRoot";
|
|
9
9
|
export type { ChatRootProps } from "./chat/ChatRoot";
|
|
10
10
|
|
|
11
|
+
// ── Theme type ────────────────────────────────────────────────────────────────
|
|
12
|
+
export type { ChatTheme } from "./chat/InboxPopup";
|
|
13
|
+
|
|
11
14
|
// ── Adapter interfaces (implement these in your host app) ─────────────────────
|
|
12
15
|
export type { ChatAdapter, ChatUICallbacks, KebabMenuOpts, ToastOpts } from "./adapter/types";
|
|
13
16
|
|