@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@banbox/chat",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Banbox Chat UI components — reusable across all Banbox React/Next.js projects",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -57,9 +57,9 @@
57
57
  "typescript"
58
58
  ],
59
59
  "peerDependencies": {
60
+ "framer-motion": ">=10",
60
61
  "react": ">=18",
61
- "react-dom": ">=18",
62
- "framer-motion": ">=10"
62
+ "react-dom": ">=18"
63
63
  },
64
64
  "peerDependenciesMeta": {
65
65
  "framer-motion": {
@@ -68,6 +68,7 @@
68
68
  },
69
69
  "dependencies": {
70
70
  "clsx": "^2.1.1",
71
+ "lottie-react": "^2.4.1",
71
72
  "tailwind-merge": "^3.6.0"
72
73
  },
73
74
  "devDependencies": {
@@ -144,36 +144,41 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
144
144
  };
145
145
 
146
146
  return (
147
- <div className="fixed bottom-4 right-[40px] z-50">
147
+ <div className="fixed bottom-4 right-4 z-[10002]">
148
148
  {/* Backdrop */}
149
149
  <motion.button
150
150
  aria-label="Close chat"
151
151
  onClick={close}
152
- className="fixed inset-0 bg-black/20"
152
+ className="fixed inset-0"
153
153
  initial={{ opacity: 0 }}
154
154
  animate={{ opacity: 1 }}
155
155
  exit={{ opacity: 0 }}
156
- transition={{ duration: 0.3 }}
156
+ transition={{ type: "tween", duration: 0.25 }}
157
157
  />
158
158
 
159
159
  {/* Popup wrapper */}
160
160
  <motion.div
161
161
  role="dialog"
162
162
  aria-modal="true"
163
- className="relative rounded-[16px]"
164
- style={{ width: 800, height: 650, boxShadow: "0px 2px 12px 0px rgba(59,51,51,0.1)" }}
165
- initial={{ x: "100%", opacity: 0 }}
166
- animate={{ x: 0, opacity: 1 }}
167
- exit={{ x: "100%", opacity: 0 }}
168
- transition={{ type: "tween", duration: 0.4, ease: "easeOut" }}
163
+ className="relative rounded-[20px] p-[3px]"
164
+ style={{
165
+ width: 800,
166
+ height: 650,
167
+ 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%)",
170
+ }}
171
+ initial={{ x: "110%" }}
172
+ animate={{ x: 0 }}
173
+ exit={{ x: "110%" }}
174
+ transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
169
175
  >
170
176
  <div
171
- className="relative h-full w-full overflow-hidden rounded-[16px]"
172
- style={{
173
- border: "2px solid transparent",
174
- background: "linear-gradient(white, white) padding-box, 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%) border-box",
175
- }}
177
+ className="relative h-full w-full overflow-hidden rounded-[18px] bg-white"
178
+ style={{ overscrollBehavior: "contain" }}
176
179
  >
180
+ <div className="pointer-events-none absolute inset-0 rounded-[14px] ring-1 ring-[#2F80ED]/40" />
181
+
177
182
  <div className="grid h-full min-h-0 grid-cols-[1fr_350px]">
178
183
  {/* LEFT — Message area */}
179
184
  <div className="flex h-full min-h-0 flex-col border-r border-[#9BBCCF]">
@@ -212,39 +217,39 @@ const InboxPopup: React.FC<InboxPopupProps> = ({ adapter, uiCallbacks }) => {
212
217
 
213
218
  <div className="flex-1 min-h-0">
214
219
  <div className="relative h-full min-h-0">
215
- {isLoading ? (
216
- <ChatSpinner className="h-full min-h-[200px]" />
217
- ) : (
218
- <ChatScroll className="h-full pb-10" bottomAlignWhenShort={false} scrollKey={scrollKey}>
219
- {messages.map((m, idx) => {
220
- const mine = m.author === "you";
221
- const isLast = idx === messages.length - 1;
222
- return (
223
- <ChatMessageItem
224
- key={m.id}
225
- id={m.id}
226
- mine={mine}
227
- time={m.time ?? ""}
228
- authorInitial={typeof m.author === "string" ? m.author : "U"}
229
- avatarBg={avatarBg}
230
- text={m.text ?? m.content}
231
- businessCard={m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]}
232
- addressCard={m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]}
233
- images={m.images}
234
- files={m.files}
235
- audio={m.audio}
236
- replyTo={m.replyTo}
237
- showStatus={isLast}
238
- status={activeThread?.status?.kind === "seen" ? "Seen" : "Delivered"}
239
- onReply={() => setReplyTo(toRef(m))}
240
- initialSrc={m.avatarSrc}
241
- />
242
- );
243
- })}
244
- </ChatScroll>
245
- )}
220
+ <ChatScroll
221
+ className="h-full pb-10"
222
+ bottomAlignWhenShort={false}
223
+ scrollKey={scrollKey}
224
+ >
225
+ {messages.map((m, idx) => {
226
+ const mine = m.author === "you";
227
+ const isLast = idx === messages.length - 1;
228
+ return (
229
+ <ChatMessageItem
230
+ key={m.id}
231
+ id={m.id}
232
+ mine={mine}
233
+ time={m.time ?? ""}
234
+ authorInitial={typeof m.author === "string" ? m.author : "U"}
235
+ avatarBg={avatarBg}
236
+ text={m.text ?? m.content}
237
+ businessCard={m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]}
238
+ addressCard={m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]}
239
+ images={m.images}
240
+ files={m.files}
241
+ audio={m.audio}
242
+ replyTo={m.replyTo}
243
+ showStatus={isLast}
244
+ status={activeThread?.status?.kind === "seen" ? "Seen" : "Delivered"}
245
+ onReply={() => setReplyTo(toRef(m))}
246
+ initialSrc={m.avatarSrc}
247
+ />
248
+ );
249
+ })}
250
+ </ChatScroll>
246
251
 
247
- <div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-center px-4 pb-2 pt-1 bg-white">
252
+ <div className="pointer-events-none absolute inset-x-0 bottom-0 flex items-center justify-start px-4 pb-2 pt-1 bg-white">
248
253
  <TypingIndicator className="pointer-events-auto" />
249
254
  </div>
250
255
  </div>
@@ -11,7 +11,6 @@ import ChatHeader from "../ui/chat/ChatHeader";
11
11
  import ChatIdentity from "../ui/chat/ChatIdentity";
12
12
  import ChatMessageItem from "../ui/chat/ChatMessageItem";
13
13
  import ChatScroll from "../ui/chat/ChatScroll";
14
- import ChatSpinner from "../ui/chat/ChatSpinner";
15
14
  import TypingIndicator from "../ui/chat/TypingIndicator";
16
15
 
17
16
  import type { Thread, Message, MessageRef, Reference } from "../types";
@@ -82,7 +81,7 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
82
81
  subtitle?: string;
83
82
  };
84
83
 
85
- const initial = meta.initial ?? activeThread?.avatarText ?? "A";
84
+ const initialSrc = activeThread?.avatarSrc ?? "/shop/ban-box.png";
86
85
  const title = meta.title ?? activeThread?.title ?? "Unknown";
87
86
  const online = meta.online ?? activeThread?.online ?? true;
88
87
  const subtitle = meta.subtitle ?? "Customer";
@@ -91,15 +90,8 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
91
90
  const [messages, setMessages] = React.useState<Message[]>(() =>
92
91
  activeId ? adapter.messages.list(activeId) : [],
93
92
  );
94
- const [scrollKey, setScrollKey] = React.useState<number>(0);
93
+ const [scrollKey, setScrollKey] = React.useState<number>(Date.now());
95
94
  const [replyTo, setReplyTo] = React.useState<MessageRef | undefined>(undefined);
96
- const [isLoading, setIsLoading] = React.useState(true);
97
-
98
- // Brief loading flash on initial open
99
- React.useEffect(() => {
100
- const t = setTimeout(() => setIsLoading(false), 300);
101
- return () => clearTimeout(t);
102
- }, []);
103
95
 
104
96
  // Subscribe to real-time updates
105
97
  React.useEffect(() => {
@@ -123,16 +115,16 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
123
115
  void uiCallbacks;
124
116
 
125
117
  return (
126
- <div className="fixed bottom-4 right-[40px] z-50">
118
+ <div className="fixed bottom-4 right-4 z-[10002]">
127
119
  {/* Backdrop */}
128
120
  <motion.button
129
121
  aria-label="Close chat"
130
122
  onClick={close}
131
- className="fixed inset-0 bg-black/20 cursor-auto!"
123
+ className="fixed inset-0 cursor-auto!"
132
124
  initial={{ opacity: 0 }}
133
125
  animate={{ opacity: 1 }}
134
126
  exit={{ opacity: 0 }}
135
- transition={{ duration: 0.3 }}
127
+ transition={{ type: "tween", duration: 0.25 }}
136
128
  />
137
129
 
138
130
  {/* Outer gradient wrapper */}
@@ -142,23 +134,26 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
142
134
  className="relative h-[650px] w-[450px] rounded-[20px] p-[2px]"
143
135
  style={{
144
136
  boxShadow: "0px 2px 12px 0px #3B33331A",
145
- background: "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%)",
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%)",
146
139
  }}
147
- initial={{ x: "100%", opacity: 0 }}
148
- animate={{ x: 0, opacity: 1 }}
149
- exit={{ x: "100%", opacity: 0 }}
150
- transition={{ type: "tween", duration: 0.4, ease: "easeOut" }}
140
+ initial={{ x: "110%" }}
141
+ animate={{ x: 0 }}
142
+ exit={{ x: "110%" }}
143
+ transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
151
144
  >
152
145
  {/* Inner card */}
153
- <div className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white">
146
+ <div
147
+ className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
148
+ style={{ overscrollBehavior: "contain" }}
149
+ >
154
150
  {/* Header */}
155
151
  <div className="h-[64px] shrink-0">
156
152
  <ChatHeader
157
153
  left={
158
154
  <ChatIdentity
159
- variant="initial"
160
- initial={initial}
161
- bg="#FFE5DA"
155
+ variant="avatar"
156
+ src={initialSrc}
162
157
  online={online}
163
158
  title={title}
164
159
  subtitle={subtitle}
@@ -179,44 +174,38 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks }) => {
179
174
  </div>
180
175
 
181
176
  {/* Messages */}
182
- <div className="relative flex-1 min-h-0">
183
- {isLoading ? (
184
- <ChatSpinner className="h-full" />
185
- ) : (
186
- <ChatScroll className="h-full" bottomAlignWhenShort={false} scrollKey={scrollKey}>
187
- {messages.map((m, idx) => {
188
- const mine = m.author === "you";
189
- const isLast = idx === messages.length - 1;
190
- return (
191
- <ChatMessageItem
192
- key={m.id}
193
- id={m.id}
194
- mine={mine}
195
- time={m.time ?? ""}
196
- authorInitial={typeof m.author === "string" ? m.author : "U"}
197
- text={m.text ?? m.content}
198
- businessCard={m.businessCard as Parameters<typeof ChatMessageItem>[0]["businessCard"]}
199
- addressCard={m.addressCard as Parameters<typeof ChatMessageItem>[0]["addressCard"]}
200
- images={m.images}
201
- files={m.files}
202
- audio={m.audio}
203
- replyTo={m.replyTo}
204
- initialSrc={m.avatarSrc}
205
- showStatus={isLast}
206
- status={statusText}
207
- onReply={() => setReplyTo(toRef(m))}
208
- />
209
- );
210
- })}
211
- </ChatScroll>
212
- )}
213
-
214
- {/* Typing indicator */}
215
- <div className="pointer-events-none absolute bottom-0 left-0 w-full bg-white px-4 pb-2">
216
- <div className="pointer-events-auto flex items-center justify-start">
177
+ <div className="flex-1 min-h-0">
178
+ <ChatScroll className="h-full" bottomAlignWhenShort={false} scrollKey={scrollKey}>
179
+ {messages.map((m, idx) => {
180
+ const mine = m.author === "you";
181
+ const isLast = idx === messages.length - 1;
182
+ return (
183
+ <ChatMessageItem
184
+ key={m.id}
185
+ id={m.id}
186
+ mine={mine}
187
+ time={m.time ?? ""}
188
+ authorInitial={typeof m.author === "string" ? m.author : "U"}
189
+ 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"]}
192
+ images={m.images}
193
+ files={m.files}
194
+ audio={m.audio}
195
+ replyTo={m.replyTo}
196
+ initialSrc={m.avatarSrc}
197
+ showStatus={isLast}
198
+ status={statusText}
199
+ onReply={() => setReplyTo(toRef(m))}
200
+ />
201
+ );
202
+ })}
203
+
204
+ {/* Typing */}
205
+ <div className="flex items-center justify-start">
217
206
  <TypingIndicator />
218
207
  </div>
219
- </div>
208
+ </ChatScroll>
220
209
  </div>
221
210
 
222
211
  {/* Footer */}