@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banbox/chat",
3
- "version": "1.0.3",
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": false,
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
  }
@@ -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
  )}
@@ -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
- /* ─── Thread list ─── */
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 UI values ─── */
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 = React.useRef(activeId);
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
- {/* Popup wrapper */}
178
+ {/* Outer gradient border + theme root */}
160
179
  <motion.div
161
180
  role="dialog"
162
181
  aria-modal="true"
163
- className="relative rounded-[20px] p-[3px]"
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
- "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%)",
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
- <div className="grid h-full min-h-0 grid-cols-[1fr_350px]">
183
- {/* LEFT Message area */}
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 — Thread list */}
272
- <div className="h-full min-h-0">
273
- <ChatListHeader onClose={close} onSearchChange={(val) => setSearchQuery(val)} />
274
- <div className="h-full overflow-y-auto custom-scroll">
275
- {threads
276
- .filter((t) => {
277
- if (!searchQuery.trim()) return true;
278
- const q = searchQuery.toLowerCase();
279
- return (
280
- t.title.toLowerCase().includes(q) ||
281
- t.last?.toLowerCase().includes(q) ||
282
- t.orderId?.toLowerCase().includes(q) ||
283
- t.inquiryId?.toLowerCase().includes(q)
284
- );
285
- })
286
- .map((t) => {
287
- const status: ChatThreadStatus = t.status ?? (t.unread && t.unread > 0 ? { kind: "new", count: t.unread } : { kind: "seen" });
288
- return (
289
- <ChatThreadItem
290
- key={t.id}
291
- onClick={() => { setReplyTo(undefined); selectThread(t.id); }}
292
- active={t.id === activeId}
293
- pinned={Boolean(t.pinned)}
294
- online={t.online}
295
- verified={Boolean(t.badge)}
296
- title={t.title}
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>
@@ -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
- Helpers
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
- Props
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 wrapper */}
136
+ {/* Outer gradient border + theme root */}
131
137
  <motion.div
132
138
  role="dialog"
133
139
  aria-modal="true"
134
- className="relative h-[650px] w-[450px] rounded-[20px] p-[2px]"
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
- "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%)",
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-[#ff5301] cursor-pointer"
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 className="h-full" bottomAlignWhenShort={false} scrollKey={scrollKey}>
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={statusText}
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