@banbox/chat 1.0.3 → 1.0.4

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.
@@ -13,12 +13,11 @@ 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
18
 
19
- /* ─────────────────────────────────────────────────────────────
20
- Helpers
21
- ────────────────────────────────────────────────────────────── */
19
+ const GRADIENT_BORDER =
20
+ "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
21
 
23
22
  function coalesceThreadId(reference: Reference | undefined, threads: Thread[]): string {
24
23
  const referenceId = reference?.id;
@@ -45,21 +44,11 @@ function toRef(m: Message): MessageRef {
45
44
  };
46
45
  }
47
46
 
48
- /* ─────────────────────────────────────────────────────────────
49
- Props
50
- ────────────────────────────────────────────────────────────── */
51
-
52
47
  export type SinglePopupProps = {
53
- /** The unified data adapter */
54
48
  adapter: ChatAdapter;
55
- /** UI-level callbacks (toast, navigation) */
56
49
  uiCallbacks?: ChatUICallbacks;
57
50
  };
58
51
 
59
- /* ─────────────────────────────────────────────────────────────
60
- Component
61
- ────────────────────────────────────────────────────────────── */
62
-
63
52
  const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
64
53
  const { close, reference } = useChatUI();
65
54
 
@@ -86,14 +75,12 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
86
75
  const online = meta.online ?? activeThread?.online ?? true;
87
76
  const subtitle = meta.subtitle ?? "Customer";
88
77
 
89
- /* ─── Messages ─── */
90
78
  const [messages, setMessages] = React.useState<Message[]>(() =>
91
79
  activeId ? adapter.messages.list(activeId) : [],
92
80
  );
93
81
  const [scrollKey, setScrollKey] = React.useState<number>(Date.now());
94
82
  const [replyTo, setReplyTo] = React.useState<MessageRef | undefined>(undefined);
95
83
 
96
- // Subscribe to real-time updates
97
84
  React.useEffect(() => {
98
85
  if (!activeId || !adapter.messages.subscribe) return;
99
86
  const unsub = adapter.messages.subscribe(activeId, () => {
@@ -109,46 +96,54 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
109
96
  setReplyTo(undefined);
110
97
  }, [activeId, adapter]);
111
98
 
112
- const statusText = activeThread?.status?.kind === "seen" ? "Seen" : "Delivered";
113
-
114
- /* ─── Unused callbacks acknowledged ─── */
115
99
  void uiCallbacks;
116
100
 
117
101
  return (
118
- <div className="fixed bottom-4 right-4 z-[10002]">
102
+ <div style={{ position: "fixed", bottom: 16, right: 16, zIndex: 10002 }}>
119
103
  {/* Backdrop */}
120
104
  <motion.button
121
105
  aria-label="Close chat"
122
106
  onClick={close}
123
- className="fixed inset-0 cursor-auto!"
107
+ style={{ position: "fixed", inset: 0, background: "transparent", border: "none", cursor: "auto" }}
124
108
  initial={{ opacity: 0 }}
125
109
  animate={{ opacity: 1 }}
126
110
  exit={{ opacity: 0 }}
127
111
  transition={{ type: "tween", duration: 0.25 }}
128
112
  />
129
113
 
130
- {/* Outer gradient wrapper */}
114
+ {/* Outer gradient border */}
131
115
  <motion.div
132
116
  role="dialog"
133
117
  aria-modal="true"
134
- className="relative h-[650px] w-[450px] rounded-[20px] p-[2px]"
135
118
  style={{
119
+ position: "relative",
120
+ height: 650,
121
+ width: 450,
122
+ borderRadius: 20,
123
+ padding: 2,
136
124
  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%)",
125
+ background: GRADIENT_BORDER,
139
126
  }}
140
127
  initial={{ x: "110%" }}
141
128
  animate={{ x: 0 }}
142
129
  exit={{ x: "110%" }}
143
130
  transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
144
131
  >
145
- {/* Inner card */}
132
+ {/* Inner white card */}
146
133
  <div
147
- className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
148
- style={{ overscrollBehavior: "contain" }}
134
+ style={{
135
+ display: "flex",
136
+ flexDirection: "column",
137
+ height: "100%",
138
+ width: "100%",
139
+ overflow: "hidden",
140
+ borderRadius: 18,
141
+ backgroundColor: "#fff",
142
+ overscrollBehavior: "contain",
143
+ }}
149
144
  >
150
- {/* Header */}
151
- <div className="h-[64px] shrink-0">
145
+ {/* Header — 64px */}
146
+ <div style={{ height: 64, flexShrink: 0 }}>
152
147
  <ChatHeader
153
148
  left={
154
149
  <ChatIdentity
@@ -165,7 +160,19 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
165
160
  <button
166
161
  type="button"
167
162
  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"
163
+ style={{
164
+ display: "flex",
165
+ alignItems: "center",
166
+ justifyContent: "center",
167
+ height: 34,
168
+ width: 34,
169
+ borderRadius: "50%",
170
+ backgroundColor: "#fff",
171
+ color: "#000",
172
+ border: "none",
173
+ boxShadow: "0px 2px 4px 0px #A5A3AE4D",
174
+ cursor: "pointer",
175
+ }}
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 */}
177
- <div className="flex-1 min-h-0">
178
- <ChatScroll className="h-full" bottomAlignWhenShort={false} scrollKey={scrollKey}>
183
+ {/* Messages — flex-1 */}
184
+ <div style={{ flex: 1, minHeight: 0, position: "relative" }}>
185
+ <ChatScroll
186
+ bottomAlignWhenShort={false}
187
+ scrollKey={scrollKey}
188
+ style={{ height: "100%", overflowY: "auto" }}
189
+ >
179
190
  {messages.map((m, idx) => {
180
191
  const mine = m.author === "you";
181
192
  const isLast = idx === messages.length - 1;
@@ -187,29 +198,33 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
187
198
  time={m.time ?? ""}
188
199
  authorInitial={typeof m.author === "string" ? m.author : "U"}
189
200
  text={m.text ?? m.content}
190
- businessCard={m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]}
191
- addressCard={m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]}
201
+ businessCard={
202
+ m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]
203
+ }
204
+ addressCard={
205
+ m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]
206
+ }
192
207
  images={m.images}
193
208
  files={m.files}
194
209
  audio={m.audio}
195
210
  replyTo={m.replyTo}
196
211
  initialSrc={m.avatarSrc}
197
212
  showStatus={isLast}
198
- status={statusText}
213
+ status={activeThread?.status?.kind === "seen" ? "Seen" : "Delivered"}
199
214
  onReply={() => setReplyTo(toRef(m))}
200
215
  />
201
216
  );
202
217
  })}
203
218
 
204
- {/* Typing */}
205
- <div className="flex items-center justify-start">
219
+ {/* Typing indicator */}
220
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "flex-start" }}>
206
221
  <TypingIndicator />
207
222
  </div>
208
223
  </ChatScroll>
209
224
  </div>
210
225
 
211
226
  {/* Footer */}
212
- <div className="shrink-0">
227
+ <div style={{ flexShrink: 0 }}>
213
228
  <ChatFooter
214
229
  variant="single"
215
230
  replyTo={replyTo}
@@ -1,24 +1,32 @@
1
- "use client";
2
- import clsx from "clsx";
3
- import React from "react";
4
-
5
- type Props = {
6
- left: React.ReactNode; // header left content (avatar/title)
7
- right?: React.ReactNode; // header right content (icons/actions)
8
- below?: React.ReactNode; // optional bar below the header (e.g., Inquiry ID)
9
- className?: string;
10
- };
11
-
12
- export default function ChatHeader({ left, right, below, className }: Props) {
13
- return (
14
- <div>
15
- <div className={clsx("border-b border-[#e1e1e1] h-[64px]", className)}>
16
- <div className="flex items-start justify-between px-4 pt-2.5">
17
- <div className="flex items-start gap-3">{left}</div>
18
- {right}
19
- </div>
20
- </div>
21
- {below && <>{below}</>}
22
- </div>
23
- );
24
- }
1
+ "use client";
2
+ import React from "react";
3
+
4
+ type Props = {
5
+ left: React.ReactNode;
6
+ right?: React.ReactNode;
7
+ below?: React.ReactNode;
8
+ className?: string;
9
+ };
10
+
11
+ export default function ChatHeader({ left, right, below }: Props) {
12
+ return (
13
+ <div>
14
+ <div
15
+ style={{
16
+ borderBottom: "1px solid #e1e1e1",
17
+ height: 64,
18
+ display: "flex",
19
+ alignItems: "flex-start",
20
+ justifyContent: "space-between",
21
+ paddingLeft: 16,
22
+ paddingRight: 16,
23
+ paddingTop: 10,
24
+ }}
25
+ >
26
+ <div style={{ display: "flex", alignItems: "flex-start", gap: 12 }}>{left}</div>
27
+ {right}
28
+ </div>
29
+ {below && <>{below}</>}
30
+ </div>
31
+ );
32
+ }
@@ -1,156 +1,157 @@
1
- "use client";
2
-
3
- import clsx from "clsx";
4
- import type { Variants } from "framer-motion";
5
- import { AnimatePresence, motion } from "framer-motion";
6
- import React from "react";
7
- import { MessageIcon, ChatSearchIcon, ChatXIcon } from "../../icons";
8
-
9
- type Props = {
10
- className?: string;
11
- onClose?: () => void;
12
- onSearchChange?: (value: string) => void;
13
- };
14
-
15
- const ChatListHeader: React.FC<Props> = ({ className, onClose, onSearchChange }) => {
16
- const [searching, setSearching] = React.useState(false);
17
- const [q, setQ] = React.useState("");
18
- const inputRef = React.useRef<HTMLInputElement>(null);
19
-
20
- React.useEffect(() => {
21
- // AnimatePresence mode="wait" delays mounting; wait for enter animation
22
- const timer = searching
23
- ? setTimeout(() => {
24
- inputRef.current?.focus();
25
- }, 220)
26
- : undefined;
27
- return () => {
28
- clearTimeout(timer);
29
- };
30
- }, [searching]);
31
-
32
- React.useEffect(() => {
33
- if (!searching) {
34
- return;
35
- }
36
- const onKey = (e: KeyboardEvent) => {
37
- if (e.key === "Escape") {
38
- setSearching(false);
39
- setQ("");
40
- onSearchChange?.("");
41
- }
42
- };
43
- window.addEventListener("keydown", onKey);
44
- return () => {
45
- window.removeEventListener("keydown", onKey);
46
- };
47
- }, [searching, onSearchChange]);
48
-
49
- const _clearInside = () => {
50
- setQ("");
51
- onSearchChange?.("");
52
- inputRef.current?.focus();
53
- };
54
-
55
- // Use cubic-bezier tuples to satisfy the Transition type
56
- const variants: Variants = {
57
- inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
58
- outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
59
- inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
60
- outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
61
- };
62
-
63
- return (
64
- <div className={clsx("h-[64px] border-b border-[#ededed]", className)}>
65
- <div className="flex h-full items-center px-[20px]">
66
- <AnimatePresence initial={false} mode="wait">
67
- {!searching ? (
68
- // NORMAL (title) — appears from left when closing search
69
- <motion.div
70
- key="normal"
71
- className="flex w-full items-center justify-between"
72
- initial={{ opacity: 0, x: -24 }}
73
- animate="inFromLeft"
74
- exit="outToLeft"
75
- variants={variants}
76
- >
77
- <div className="flex items-center gap-3">
78
- <div className="text-[#2c2c2c] flex items-center gap-2">
79
- <MessageIcon className="w-6 h-6" />
80
- <span className="text-[22px] font-semibold">
81
- Messenger
82
- </span>
83
- </div>
84
- </div>
85
-
86
- <div className="flex items-center gap-2">
87
- <button
88
- title="Search"
89
- onClick={() => setSearching(true)}
90
- className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
91
- >
92
- <ChatSearchIcon className="w-5 h-5" />
93
- </button>
94
-
95
- <button
96
- title="Close"
97
- onClick={onClose}
98
- className="h-9 w-9 place-items-center rounded-full hover:bg-black/5 flex items-center justify-center"
99
- >
100
- <ChatXIcon className="w-6 h-6" />
101
- </button>
102
- </div>
103
- </motion.div>
104
- ) : (
105
- // SEARCH — enters from right, exits to right
106
- <motion.div
107
- key="search"
108
- className="flex w-full items-center gap-3"
109
- initial={{ opacity: 0, x: 24 }}
110
- animate="inFromRight"
111
- exit="outToRight"
112
- variants={variants}
113
- >
114
- <div className="relative flex-1">
115
- <div className="flex h-10 w-full items-center rounded-full border border-[#6A6A6A] bg-white px-[4px] gap-1.5">
116
- <div className="flex items-center ms-[12px] w-full">
117
- <span className="mr-2 grid h-6 w-6 shrink-0 place-items-center text-[#929292]">
118
- <ChatSearchIcon className="w-5 h-5" />
119
- </span>
120
- <span className="mr-2 h-6 w-px shrink-0 bg-[#e1e1e1]" />
121
- <input
122
- ref={inputRef}
123
- value={q}
124
- onChange={(e) => {
125
- setQ(e.target.value);
126
- onSearchChange?.(e.target.value);
127
- }}
128
- placeholder="Search"
129
- className="h-full w-full flex-1 bg-transparent text-[15px] outline-none placeholder:text-[#9C9C9C]"
130
- />
131
- </div>
132
-
133
- <div>
134
- <button
135
- title="Close search"
136
- onClick={() => {
137
- setSearching(false);
138
- setQ("");
139
- onSearchChange?.("");
140
- }}
141
- className="grid h-8 w-8 place-items-center rounded-full text-xl hover:bg-black/5"
142
- >
143
- <ChatXIcon className="w-5 h-5" />
144
- </button>
145
- </div>
146
- </div>
147
- </div>
148
- </motion.div>
149
- )}
150
- </AnimatePresence>
151
- </div>
152
- </div>
153
- );
154
- };
155
-
156
- export default ChatListHeader;
1
+ "use client";
2
+
3
+ import type { Variants } from "framer-motion";
4
+ import { AnimatePresence, motion } from "framer-motion";
5
+ import React from "react";
6
+ import { ChatSearchIcon, ChatXIcon, MessageIcon } from "../../icons";
7
+
8
+ type Props = {
9
+ className?: string;
10
+ onClose?: () => void;
11
+ onSearchChange?: (value: string) => void;
12
+ };
13
+
14
+ const ChatListHeader: React.FC<Props> = ({ onClose, onSearchChange }) => {
15
+ const [searching, setSearching] = React.useState(false);
16
+ const [q, setQ] = React.useState("");
17
+ const inputRef = React.useRef<HTMLInputElement>(null);
18
+
19
+ React.useEffect(() => {
20
+ const timer = searching
21
+ ? setTimeout(() => { inputRef.current?.focus(); }, 220)
22
+ : undefined;
23
+ return () => { clearTimeout(timer); };
24
+ }, [searching]);
25
+
26
+ React.useEffect(() => {
27
+ if (!searching) return;
28
+ const onKey = (e: KeyboardEvent) => {
29
+ if (e.key === "Escape") {
30
+ setSearching(false);
31
+ setQ("");
32
+ onSearchChange?.("");
33
+ }
34
+ };
35
+ window.addEventListener("keydown", onKey);
36
+ return () => { window.removeEventListener("keydown", onKey); };
37
+ }, [searching, onSearchChange]);
38
+
39
+ const variants: Variants = {
40
+ inFromRight: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
41
+ outToLeft: { opacity: 0, x: -24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
42
+ inFromLeft: { opacity: 1, x: 0, transition: { duration: 0.18, ease: [0.16, 1, 0.3, 1] } },
43
+ outToRight: { opacity: 0, x: 24, transition: { duration: 0.16, ease: [0.4, 0, 1, 1] } },
44
+ };
45
+
46
+ const btnStyle: React.CSSProperties = {
47
+ height: 36,
48
+ width: 36,
49
+ display: "flex",
50
+ alignItems: "center",
51
+ justifyContent: "center",
52
+ borderRadius: "50%",
53
+ border: "none",
54
+ background: "transparent",
55
+ cursor: "pointer",
56
+ };
57
+
58
+ return (
59
+ <div
60
+ style={{
61
+ height: 64,
62
+ borderBottom: "1px solid #ededed",
63
+ display: "flex",
64
+ alignItems: "center",
65
+ paddingLeft: 20,
66
+ paddingRight: 20,
67
+ }}
68
+ >
69
+ <AnimatePresence initial={false} mode="wait">
70
+ {!searching ? (
71
+ <motion.div
72
+ key="normal"
73
+ style={{ display: "flex", width: "100%", alignItems: "center", justifyContent: "space-between" }}
74
+ initial={{ opacity: 0, x: -24 }}
75
+ animate="inFromLeft"
76
+ exit="outToLeft"
77
+ variants={variants}
78
+ >
79
+ {/* Title */}
80
+ <div style={{ display: "flex", alignItems: "center", gap: 8, color: "#2c2c2c" }}>
81
+ <MessageIcon className="w-6 h-6" />
82
+ <span style={{ fontSize: 22, fontWeight: 600 }}>Messenger</span>
83
+ </div>
84
+
85
+ {/* Actions */}
86
+ <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
87
+ <button title="Search" onClick={() => setSearching(true)} style={btnStyle}>
88
+ <ChatSearchIcon className="w-5 h-5" />
89
+ </button>
90
+ <button title="Close" onClick={onClose} style={btnStyle}>
91
+ <ChatXIcon className="w-6 h-6" />
92
+ </button>
93
+ </div>
94
+ </motion.div>
95
+ ) : (
96
+ <motion.div
97
+ key="search"
98
+ style={{ display: "flex", width: "100%", alignItems: "center", gap: 12 }}
99
+ initial={{ opacity: 0, x: 24 }}
100
+ animate="inFromRight"
101
+ exit="outToRight"
102
+ variants={variants}
103
+ >
104
+ <div
105
+ style={{
106
+ display: "flex",
107
+ flex: 1,
108
+ height: 40,
109
+ alignItems: "center",
110
+ borderRadius: 9999,
111
+ border: "1px solid #6A6A6A",
112
+ backgroundColor: "#fff",
113
+ padding: "0 4px",
114
+ gap: 6,
115
+ }}
116
+ >
117
+ <div style={{ display: "flex", alignItems: "center", flex: 1, marginLeft: 12 }}>
118
+ <span style={{ marginRight: 8, color: "#929292", flexShrink: 0 }}>
119
+ <ChatSearchIcon className="w-5 h-5" />
120
+ </span>
121
+ <span style={{ marginRight: 8, height: 24, width: 1, flexShrink: 0, backgroundColor: "#e1e1e1" }} />
122
+ <input
123
+ ref={inputRef}
124
+ value={q}
125
+ onChange={(e) => {
126
+ setQ(e.target.value);
127
+ onSearchChange?.(e.target.value);
128
+ }}
129
+ placeholder="Search"
130
+ style={{
131
+ flex: 1,
132
+ height: "100%",
133
+ background: "transparent",
134
+ fontSize: 15,
135
+ outline: "none",
136
+ border: "none",
137
+ color: "#2c2c2c",
138
+ }}
139
+ />
140
+ </div>
141
+
142
+ <button
143
+ title="Close search"
144
+ onClick={() => { setSearching(false); setQ(""); onSearchChange?.(""); }}
145
+ style={{ ...btnStyle, height: 32, width: 32 }}
146
+ >
147
+ <ChatXIcon className="w-5 h-5" />
148
+ </button>
149
+ </div>
150
+ </motion.div>
151
+ )}
152
+ </AnimatePresence>
153
+ </div>
154
+ );
155
+ };
156
+
157
+ export default ChatListHeader;