@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/dist/index.cjs +61 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +61 -61
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/chat/ChatImagePreviewModal.tsx +55 -47
- package/src/chat/SinglePopup.tsx +5 -2
- package/src/modals/ChatTranslateSettingsModal.tsx +2 -2
- package/src/ui/ChatFooter.tsx +16 -6
package/package.json
CHANGED
|
@@ -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 ?
|
|
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 ? -
|
|
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-
|
|
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
|
|
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:
|
|
105
|
+
style={{ width: 953, height: 679 }}
|
|
103
106
|
>
|
|
104
|
-
{/* Sliding image */}
|
|
105
|
-
<AnimatePresence
|
|
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.
|
|
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
|
-
{/*
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
124
|
+
{/* Arrows — only 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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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 */}
|
package/src/chat/SinglePopup.tsx
CHANGED
|
@@ -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
|
-
"
|
|
110
|
+
"rounded-[6px] bg-white shadow-[0px_2px_12px_0px_rgba(59,51,51,0.1)]",
|
|
111
111
|
className,
|
|
112
112
|
)}
|
|
113
113
|
>
|
package/src/ui/ChatFooter.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
436
|
-
<span><ChatInfoIcon className="
|
|
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>
|