@banbox/chat 1.0.17 → 1.0.18

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.17",
3
+ "version": "1.0.18",
4
4
  "description": "Banbox Chat UI components — reusable across all Banbox React/Next.js projects",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -18,14 +18,17 @@ interface ChatImagePreviewModalProps {
18
18
  /* =======================
19
19
  Slide direction variants
20
20
  ======================= */
21
+ // Percentage-based so the slide distance always matches the container width exactly.
22
+ // Both exit and enter run simultaneously (no mode="wait"), giving a natural
23
+ // cross-slide feel without the jerk caused by a sequential pause.
21
24
  const slideVariants = {
22
25
  enter: (dir: number) => ({
23
- x: dir > 0 ? 300 : -300,
26
+ x: dir > 0 ? "100%" : "-100%",
24
27
  opacity: 0,
25
28
  }),
26
29
  center: { x: 0, opacity: 1 },
27
30
  exit: (dir: number) => ({
28
- x: dir > 0 ? -300 : 300,
31
+ x: dir > 0 ? "-100%" : "100%",
29
32
  opacity: 0,
30
33
  }),
31
34
  };
@@ -80,7 +83,7 @@ const ChatImagePreviewModal: FC<ChatImagePreviewModalProps> = ({
80
83
  <AnimatePresence>
81
84
  {isOpen && total > 0 && (
82
85
  <motion.div
83
- className="fixed inset-0 z-999 flex items-center justify-center"
86
+ className="fixed inset-0 z-[10010] flex items-center justify-center"
84
87
  initial={{ opacity: 0, backgroundColor: "rgba(0,0,0,0)" }}
85
88
  animate={{ opacity: 1, backgroundColor: "rgba(0,0,0,0.55)" }}
86
89
  exit={{ opacity: 0, backgroundColor: "rgba(0,0,0,0)" }}
@@ -96,13 +99,13 @@ const ChatImagePreviewModal: FC<ChatImagePreviewModalProps> = ({
96
99
  transition={{ duration: 0.25, ease: "easeInOut" }}
97
100
  onClick={(e) => e.stopPropagation()}
98
101
  >
99
- {/* Fixed 850×850 frame */}
102
+ {/* Fixed 953×679 frame — image adapts dynamically inside */}
100
103
  <div
101
104
  className="relative flex items-center justify-center overflow-hidden rounded-[6px] bg-white"
102
- style={{ width: 850, height: 850 }}
105
+ style={{ width: 953, height: 679 }}
103
106
  >
104
- {/* Sliding image */}
105
- <AnimatePresence mode="wait" initial={false} custom={direction}>
107
+ {/* Sliding image — both exit and enter animate together (no mode="wait") */}
108
+ <AnimatePresence initial={false} custom={direction}>
106
109
  <motion.img
107
110
  key={current}
108
111
  custom={direction}
@@ -110,53 +113,58 @@ const ChatImagePreviewModal: FC<ChatImagePreviewModalProps> = ({
110
113
  initial="enter"
111
114
  animate="center"
112
115
  exit="exit"
113
- transition={{ duration: 0.3, ease: "easeInOut" }}
116
+ transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
114
117
  src={currentImage?.url}
115
118
  alt={currentImage?.altText ?? `Image ${current + 1}`}
116
- className="w-full rounded-[6px] object-contain"
119
+ className="absolute inset-0 w-full h-full rounded-[6px] object-contain"
117
120
  draggable={false}
118
121
  />
119
122
  </AnimatePresence>
120
123
 
121
- {/* Left arrow always visible, disabled when no prev */}
122
- <button
123
- type="button"
124
- onClick={goPrev}
125
- disabled={!hasPrev}
126
- className={`absolute left-0 top-1/2 -translate-y-1/2 flex h-[100px] items-center rounded-tr-[3px] rounded-br-[3px] p-[7px] backdrop-blur-[2px] shadow-[3px_0px_6px_0px_rgba(0,0,0,0.1)] transition-opacity ${hasPrev ? "cursor-pointer opacity-100" : "cursor-default opacity-30"}`}
127
- style={{ backgroundColor: "rgba(255,255,255,0.7)" }}
128
- aria-label="Previous image"
129
- >
130
- <svg width="42" height="42" viewBox="0 0 24 24" fill="none">
131
- <path
132
- d="M15 18L9 12L15 6"
133
- stroke="#2C2C2C"
134
- strokeWidth="2"
135
- strokeLinecap="round"
136
- strokeLinejoin="round"
137
- />
138
- </svg>
139
- </button>
124
+ {/* Arrowsonly shown when there are multiple images */}
125
+ {total > 1 && (
126
+ <>
127
+ {/* Left arrow */}
128
+ <button
129
+ type="button"
130
+ onClick={goPrev}
131
+ disabled={!hasPrev}
132
+ className={`absolute left-0 top-1/2 -translate-y-1/2 flex h-[100px] items-center rounded-tr-[3px] rounded-br-[3px] p-[7px] backdrop-blur-[2px] shadow-[3px_0px_6px_0px_rgba(0,0,0,0.1)] transition-opacity ${hasPrev ? "cursor-pointer opacity-100" : "cursor-default opacity-30"}`}
133
+ style={{ backgroundColor: "rgba(255,255,255,0.7)" }}
134
+ aria-label="Previous image"
135
+ >
136
+ <svg width="42" height="42" viewBox="0 0 24 24" fill="none">
137
+ <path
138
+ d="M15 18L9 12L15 6"
139
+ stroke="#2C2C2C"
140
+ strokeWidth="2"
141
+ strokeLinecap="round"
142
+ strokeLinejoin="round"
143
+ />
144
+ </svg>
145
+ </button>
140
146
 
141
- {/* Right arrow — always visible, disabled when no next */}
142
- <button
143
- type="button"
144
- onClick={goNext}
145
- disabled={!hasNext}
146
- className={`absolute right-0 top-1/2 -translate-y-1/2 flex h-[100px] items-center rounded-tl-[3px] rounded-bl-[3px] p-[7px] backdrop-blur-[2px] shadow-[-3px_0px_6px_0px_rgba(0,0,0,0.1)] transition-opacity ${hasNext ? "cursor-pointer opacity-100" : "cursor-default opacity-30"}`}
147
- style={{ backgroundColor: "rgba(255,255,255,0.7)" }}
148
- aria-label="Next image"
149
- >
150
- <svg width="42" height="42" viewBox="0 0 24 24" fill="none">
151
- <path
152
- d="M9 18L15 12L9 6"
153
- stroke="#2C2C2C"
154
- strokeWidth="2"
155
- strokeLinecap="round"
156
- strokeLinejoin="round"
157
- />
158
- </svg>
159
- </button>
147
+ {/* Right arrow */}
148
+ <button
149
+ type="button"
150
+ onClick={goNext}
151
+ disabled={!hasNext}
152
+ className={`absolute right-0 top-1/2 -translate-y-1/2 flex h-[100px] items-center rounded-tl-[3px] rounded-bl-[3px] p-[7px] backdrop-blur-[2px] shadow-[-3px_0px_6px_0px_rgba(0,0,0,0.1)] transition-opacity ${hasNext ? "cursor-pointer opacity-100" : "cursor-default opacity-30"}`}
153
+ style={{ backgroundColor: "rgba(255,255,255,0.7)" }}
154
+ aria-label="Next image"
155
+ >
156
+ <svg width="42" height="42" viewBox="0 0 24 24" fill="none">
157
+ <path
158
+ d="M9 18L15 12L9 6"
159
+ stroke="#2C2C2C"
160
+ strokeWidth="2"
161
+ strokeLinecap="round"
162
+ strokeLinejoin="round"
163
+ />
164
+ </svg>
165
+ </button>
166
+ </>
167
+ )}
160
168
  </div>
161
169
 
162
170
  {/* Close button — top-right outside the image */}
@@ -190,9 +190,12 @@ const SinglePopup: React.FC<SinglePopupProps> = ({ adapter, uiCallbacks, theme,
190
190
  exit={{ x: "110%" }}
191
191
  transition={{ type: "tween", duration: 0.3, ease: "easeOut" }}
192
192
  >
193
- {/* Inner white card */}
193
+ {/* Inner white card — `relative` is required so that ChatTranslateSettingsModal's
194
+ `absolute inset-0` overlay is contained here rather than escaping to an outer
195
+ positioned ancestor. Combined with `overflow-hidden` this clips the backdrop
196
+ to the rounded corners of the popup. */}
194
197
  <div
195
- className="flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
198
+ className="relative flex h-full w-full flex-col overflow-hidden rounded-[18px] bg-white"
196
199
  style={{ overscrollBehavior: "contain" }}
197
200
  >
198
201
  {/* Header — 64px */}
@@ -97,7 +97,7 @@ const ChatTranslateSettingsModal: React.FC<Props> = ({
97
97
  }}
98
98
  >
99
99
  {/* Semi-transparent backdrop over the chat panel */}
100
- <div className="absolute inset-0 bg-black/30 rounded-[inherit]" />
100
+ <div className="absolute inset-0 bg-black/30 rounded-[inherit] overflow-hidden" />
101
101
 
102
102
  {/* Modal card */}
103
103
  <div
@@ -107,7 +107,7 @@ const ChatTranslateSettingsModal: React.FC<Props> = ({
107
107
  onClick={(e) => e.stopPropagation()}
108
108
  className={clsx(
109
109
  "relative w-[460px] max-w-[95%]",
110
- "overflow-clip rounded-[6px] bg-white shadow-[0px_2px_12px_0px_rgba(59,51,51,0.1)]",
110
+ "rounded-[6px] bg-white shadow-[0px_2px_12px_0px_rgba(59,51,51,0.1)]",
111
111
  className,
112
112
  )}
113
113
  >
@@ -225,13 +225,23 @@ const ChatFooter: React.FC<Props> = ({
225
225
  const previewFilesToPayload = (files: PreviewFile[]) =>
226
226
  files.map((f) => ({ name: f.name, sizeMB: f.sizeMB, ext: f.ext, href: f.href, downloadName: f.downloadName }));
227
227
 
228
- const clearAttachments = () => {
228
+ // Used by the preview strip's ✕ / remove buttons — safe to revoke because
229
+ // the user is cancelling the attachment (it will never appear in a message).
230
+ const cancelAttachments = () => {
229
231
  imgPreviews.forEach((u) => u.startsWith("blob:") && URL.revokeObjectURL(u));
230
232
  filePreviews.forEach((f) => f.href?.startsWith("blob:") && URL.revokeObjectURL(f.href));
231
233
  setImgPreviews([]);
232
234
  setFilePreviews([]);
233
235
  };
234
236
 
237
+ // Used after a successful send — do NOT revoke blobs here.
238
+ // The sent message object already holds these blob URLs and ChatMessageItem
239
+ // needs them to render the image. Revoking immediately makes <img src> fail.
240
+ const dismissAttachments = () => {
241
+ setImgPreviews([]);
242
+ setFilePreviews([]);
243
+ };
244
+
235
245
  /* ─────── Send handlers — all delegate to the single onSend prop ─────── */
236
246
 
237
247
  const sendText = async () => {
@@ -248,7 +258,7 @@ const ChatFooter: React.FC<Props> = ({
248
258
  images: imgPreviews,
249
259
  replyTo,
250
260
  });
251
- clearAttachments();
261
+ dismissAttachments(); // ← do NOT revoke — blob URLs are now in the message
252
262
  } else {
253
263
  onSend({ type: "text", text: t, replyTo });
254
264
  }
@@ -268,7 +278,7 @@ const ChatFooter: React.FC<Props> = ({
268
278
  files: previewFilesToPayload(filePreviews),
269
279
  replyTo,
270
280
  });
271
- clearAttachments();
281
+ dismissAttachments(); // ← do NOT revoke — blob URLs are now in the message
272
282
  clearReply?.();
273
283
  onAfterSend?.();
274
284
  };
@@ -432,9 +442,9 @@ const ChatFooter: React.FC<Props> = ({
432
442
  </div>
433
443
 
434
444
  {micError && (
435
- <div className="mb-2 flex items-start gap-2 rounded-sm bg-[#f8f8f8] px-2 py-1.5 text-xs text-[#ff5301]">
436
- <span><ChatInfoIcon className="mt-0.5 h-3.5 w-3.5" /></span>
437
- <span>{micError}</span>
445
+ <div className="mb-2 flex items-center gap-2 rounded-sm bg-[#f8f8f8] px-2 py-1.5 text-xs text-[#ff5301]">
446
+ <span><ChatInfoIcon className="h-3.5 w-3.5 shrink-0" /></span>
447
+ <span className="flex-1">{micError}</span>
438
448
  <span className="pointer flex cursor-pointer items-center justify-center rounded-full p-1 hover:bg-black/10" onClick={() => setMicError("")}>
439
449
  <ChatXIcon className="h-3.5 w-3.5" />
440
450
  </span>