@baseportal/chat-widget 0.1.0

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.
@@ -0,0 +1,2253 @@
1
+ // src/api/client.ts
2
+ var ApiClient = class {
3
+ constructor(channelToken, apiUrl) {
4
+ this.channelToken = channelToken;
5
+ this.baseUrl = `${apiUrl}/public/chat`;
6
+ }
7
+ setVisitorIdentity(email, hash) {
8
+ this.visitorEmail = email;
9
+ this.visitorHash = hash;
10
+ }
11
+ clearVisitorIdentity() {
12
+ this.visitorEmail = void 0;
13
+ this.visitorHash = void 0;
14
+ }
15
+ headers() {
16
+ const h2 = {
17
+ "Content-Type": "application/json",
18
+ "x-channel-token": this.channelToken
19
+ };
20
+ if (this.visitorEmail) h2["x-visitor-email"] = this.visitorEmail;
21
+ if (this.visitorHash) h2["x-visitor-hash"] = this.visitorHash;
22
+ return h2;
23
+ }
24
+ async request(method, path, body) {
25
+ const res = await fetch(`${this.baseUrl}${path}`, {
26
+ method,
27
+ headers: this.headers(),
28
+ body: body ? JSON.stringify(body) : void 0
29
+ });
30
+ if (!res.ok) {
31
+ const text = await res.text().catch(() => "");
32
+ throw new Error(`[BaseportalChat] API error ${res.status}: ${text}`);
33
+ }
34
+ return res.json();
35
+ }
36
+ async getChannelInfo() {
37
+ return this.request("GET", "/channel-info");
38
+ }
39
+ async initConversation(data) {
40
+ return this.request("POST", "/conversations", {
41
+ ...data,
42
+ channelToken: this.channelToken
43
+ });
44
+ }
45
+ async getMessages(conversationId, params) {
46
+ const qs = new URLSearchParams();
47
+ if (params?.limit) qs.set("limit", String(params.limit));
48
+ if (params?.page) qs.set("page", String(params.page));
49
+ const query = qs.toString() ? `?${qs.toString()}` : "";
50
+ return this.request("GET", `/conversations/${conversationId}/messages${query}`);
51
+ }
52
+ async uploadFile(conversationId, file) {
53
+ const formData = new FormData();
54
+ formData.append("file", file);
55
+ const headers = {
56
+ "x-channel-token": this.channelToken
57
+ };
58
+ if (this.visitorEmail) headers["x-visitor-email"] = this.visitorEmail;
59
+ if (this.visitorHash) headers["x-visitor-hash"] = this.visitorHash;
60
+ const res = await fetch(`${this.baseUrl}/conversations/${conversationId}/upload`, {
61
+ method: "POST",
62
+ headers,
63
+ body: formData
64
+ });
65
+ if (!res.ok) {
66
+ const text = await res.text().catch(() => "");
67
+ throw new Error(`[BaseportalChat] Upload error ${res.status}: ${text}`);
68
+ }
69
+ return res.json();
70
+ }
71
+ async sendMessage(conversationId, data) {
72
+ return this.request(
73
+ "POST",
74
+ `/conversations/${conversationId}/messages`,
75
+ data
76
+ );
77
+ }
78
+ async getVisitorConversations() {
79
+ return this.request("GET", "/conversations");
80
+ }
81
+ async reopenConversation(conversationId) {
82
+ return this.request("POST", `/conversations/${conversationId}/reopen`);
83
+ }
84
+ async getAblyToken(conversationId) {
85
+ return this.request("POST", "/ably-token", { conversationId });
86
+ }
87
+ };
88
+
89
+ // src/realtime/ably-client.ts
90
+ import * as Ably from "ably";
91
+ var RealtimeClient = class {
92
+ constructor(apiClient) {
93
+ this.client = null;
94
+ this.channel = null;
95
+ this.conversationId = null;
96
+ this.handlers = null;
97
+ this.apiClient = apiClient;
98
+ }
99
+ async subscribe(conversationId, handlers) {
100
+ this.unsubscribe();
101
+ this.conversationId = conversationId;
102
+ this.handlers = handlers;
103
+ try {
104
+ const tokenRequest = await this.apiClient.getAblyToken(conversationId);
105
+ this.client = new Ably.Realtime({
106
+ authCallback: (_data, callback) => {
107
+ callback(null, tokenRequest);
108
+ },
109
+ clientId: `visitor-${conversationId}`
110
+ });
111
+ const channelName = `conversation-${conversationId}`;
112
+ this.channel = this.client.channels.get(channelName);
113
+ this.channel.subscribe((msg) => {
114
+ if (!msg.data) return;
115
+ try {
116
+ const data = typeof msg.data === "string" ? JSON.parse(msg.data) : msg.data;
117
+ if (data.text === "conversation_status_updated" && data.metadata) {
118
+ handlers.onConversationStatusUpdate(data.metadata);
119
+ } else if (data.text === "created_or_updated_message" && data.metadata) {
120
+ handlers.onMessage(data.metadata);
121
+ }
122
+ } catch (e) {
123
+ console.error("[BaseportalChat] Error parsing realtime message:", e);
124
+ }
125
+ });
126
+ } catch (e) {
127
+ console.error("[BaseportalChat] Error connecting to realtime:", e);
128
+ }
129
+ }
130
+ unsubscribe() {
131
+ if (this.channel) {
132
+ this.channel.unsubscribe();
133
+ this.channel = null;
134
+ }
135
+ if (this.client) {
136
+ this.client.close();
137
+ this.client = null;
138
+ }
139
+ this.conversationId = null;
140
+ this.handlers = null;
141
+ }
142
+ isConnected() {
143
+ return this.client?.connection.state === "connected";
144
+ }
145
+ };
146
+
147
+ // src/ui/i18n.ts
148
+ var pt = {
149
+ prechat: {
150
+ title: "Iniciar conversa",
151
+ description: "Preencha os dados abaixo para iniciar o atendimento.",
152
+ name: "Nome",
153
+ namePlaceholder: "Seu nome",
154
+ email: "E-mail",
155
+ emailPlaceholder: "seu@email.com",
156
+ start: "Iniciar conversa",
157
+ loading: "Iniciando...",
158
+ privacyPrefix: "Ao enviar, voc\xEA concorda com nossa",
159
+ privacyLink: "Pol\xEDtica de Privacidade"
160
+ },
161
+ chat: {
162
+ placeholder: "Digite uma mensagem...",
163
+ closed: "Esta conversa foi encerrada.",
164
+ reopen: "Reabrir conversa",
165
+ attachFile: "Anexar arquivo",
166
+ uploading: "Enviando...",
167
+ fileTooLarge: "Arquivo muito grande (m\xE1x. 25MB)",
168
+ download: "Baixar"
169
+ },
170
+ conversations: {
171
+ title: "Atendimento",
172
+ newConversation: "Nova conversa",
173
+ empty: "Nenhuma conversa encontrada.",
174
+ open: "Aberta",
175
+ closed: "Fechada",
176
+ noMessages: "Nenhuma mensagem ainda"
177
+ }
178
+ };
179
+ var en = {
180
+ prechat: {
181
+ title: "Start a conversation",
182
+ description: "Fill in the details below to start chatting.",
183
+ name: "Name",
184
+ namePlaceholder: "Your name",
185
+ email: "Email",
186
+ emailPlaceholder: "you@email.com",
187
+ start: "Start conversation",
188
+ loading: "Starting...",
189
+ privacyPrefix: "By sending, you agree to our",
190
+ privacyLink: "Privacy Policy"
191
+ },
192
+ chat: {
193
+ placeholder: "Type a message...",
194
+ closed: "This conversation has been closed.",
195
+ reopen: "Reopen conversation",
196
+ attachFile: "Attach file",
197
+ uploading: "Uploading...",
198
+ fileTooLarge: "File too large (max 25MB)",
199
+ download: "Download"
200
+ },
201
+ conversations: {
202
+ title: "Support",
203
+ newConversation: "New conversation",
204
+ empty: "No conversations found.",
205
+ open: "Open",
206
+ closed: "Closed",
207
+ noMessages: "No messages yet"
208
+ }
209
+ };
210
+ var es = {
211
+ prechat: {
212
+ title: "Iniciar conversaci\xF3n",
213
+ description: "Complete los datos a continuaci\xF3n para iniciar la atenci\xF3n.",
214
+ name: "Nombre",
215
+ namePlaceholder: "Tu nombre",
216
+ email: "Correo electr\xF3nico",
217
+ emailPlaceholder: "tu@email.com",
218
+ start: "Iniciar conversaci\xF3n",
219
+ loading: "Iniciando...",
220
+ privacyPrefix: "Al enviar, aceptas nuestra",
221
+ privacyLink: "Pol\xEDtica de Privacidad"
222
+ },
223
+ chat: {
224
+ placeholder: "Escribe un mensaje...",
225
+ closed: "Esta conversaci\xF3n ha sido cerrada.",
226
+ reopen: "Reabrir conversaci\xF3n",
227
+ attachFile: "Adjuntar archivo",
228
+ uploading: "Subiendo...",
229
+ fileTooLarge: "Archivo demasiado grande (m\xE1x. 25MB)",
230
+ download: "Descargar"
231
+ },
232
+ conversations: {
233
+ title: "Atenci\xF3n",
234
+ newConversation: "Nueva conversaci\xF3n",
235
+ empty: "No se encontraron conversaciones.",
236
+ open: "Abierta",
237
+ closed: "Cerrada",
238
+ noMessages: "Sin mensajes a\xFAn"
239
+ }
240
+ };
241
+ var locales = { pt, en, es };
242
+ function getTranslations(locale) {
243
+ return locales[locale] || locales["pt"];
244
+ }
245
+
246
+ // src/ui/mount.ts
247
+ import { render, h } from "preact";
248
+
249
+ // src/ui/App.tsx
250
+ import { useEffect as useEffect4, useState as useState4 } from "preact/hooks";
251
+
252
+ // src/ui/icons.tsx
253
+ import { jsx, jsxs } from "preact/jsx-runtime";
254
+ var IconChat = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("path", { d: "M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" }) });
255
+ var IconX = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
256
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
257
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
258
+ ] });
259
+ var IconArrowLeft = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
260
+ /* @__PURE__ */ jsx("line", { x1: "19", y1: "12", x2: "5", y2: "12" }),
261
+ /* @__PURE__ */ jsx("polyline", { points: "12 19 5 12 12 5" })
262
+ ] });
263
+ var IconSend = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
264
+ /* @__PURE__ */ jsx("line", { x1: "22", y1: "2", x2: "11", y2: "13" }),
265
+ /* @__PURE__ */ jsx("polygon", { points: "22 2 15 22 11 13 2 9 22 2" })
266
+ ] });
267
+ var IconPlus = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
268
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "5", x2: "12", y2: "19" }),
269
+ /* @__PURE__ */ jsx("line", { x1: "5", y1: "12", x2: "19", y2: "12" })
270
+ ] });
271
+ var IconPaperclip = () => /* @__PURE__ */ jsx("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: /* @__PURE__ */ jsx("path", { d: "M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" }) });
272
+ var IconDownload = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
273
+ /* @__PURE__ */ jsx("path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }),
274
+ /* @__PURE__ */ jsx("polyline", { points: "7 10 12 15 17 10" }),
275
+ /* @__PURE__ */ jsx("line", { x1: "12", y1: "15", x2: "12", y2: "3" })
276
+ ] });
277
+ var IconFile = () => /* @__PURE__ */ jsxs("svg", { viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", "stroke-width": "2", "stroke-linecap": "round", "stroke-linejoin": "round", children: [
278
+ /* @__PURE__ */ jsx("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
279
+ /* @__PURE__ */ jsx("polyline", { points: "14 2 14 8 20 8" })
280
+ ] });
281
+
282
+ // src/ui/components/ChatBubble.tsx
283
+ import { jsx as jsx2, jsxs as jsxs2 } from "preact/jsx-runtime";
284
+ function ChatBubble({
285
+ isOpen,
286
+ position,
287
+ unreadCount,
288
+ onClick
289
+ }) {
290
+ const posClass = position === "bottom-left" ? "bp-bubble--left" : "bp-bubble--right";
291
+ return /* @__PURE__ */ jsxs2(
292
+ "button",
293
+ {
294
+ class: `bp-bubble ${posClass}`,
295
+ onClick,
296
+ "aria-label": isOpen ? "Close chat" : "Open chat",
297
+ children: [
298
+ isOpen ? /* @__PURE__ */ jsx2(IconX, {}) : /* @__PURE__ */ jsx2(IconChat, {}),
299
+ !isOpen && unreadCount > 0 && /* @__PURE__ */ jsx2("span", { class: "bp-bubble__badge", children: unreadCount > 99 ? "99+" : unreadCount })
300
+ ]
301
+ }
302
+ );
303
+ }
304
+
305
+ // src/ui/components/ChatWindow.tsx
306
+ import { useCallback as useCallback3, useEffect as useEffect3, useState as useState3 } from "preact/hooks";
307
+
308
+ // src/ui/components/ConversationList.tsx
309
+ import { jsx as jsx3, jsxs as jsxs3 } from "preact/jsx-runtime";
310
+ function ConversationList({
311
+ conversations,
312
+ channelInfo,
313
+ loading,
314
+ onSelect,
315
+ onNew,
316
+ t
317
+ }) {
318
+ const hasOpen = conversations.some((c) => c.open);
319
+ const canReopen = channelInfo.config.allowReopenConversation;
320
+ if (loading) {
321
+ return /* @__PURE__ */ jsx3("div", { class: "bp-loading", children: /* @__PURE__ */ jsx3("div", { class: "bp-spinner" }) });
322
+ }
323
+ return /* @__PURE__ */ jsxs3("div", { class: "bp-convlist", children: [
324
+ !hasOpen && /* @__PURE__ */ jsx3("div", { class: "bp-convlist__new", children: /* @__PURE__ */ jsxs3("button", { class: "bp-convlist__new-btn", onClick: onNew, children: [
325
+ /* @__PURE__ */ jsx3(IconPlus, {}),
326
+ t.conversations.newConversation
327
+ ] }) }),
328
+ /* @__PURE__ */ jsx3("div", { class: "bp-convlist__items", children: conversations.length === 0 ? /* @__PURE__ */ jsx3("div", { class: "bp-convlist__empty", children: t.conversations.empty }) : conversations.map((conv) => {
329
+ const isClickable = conv.open || canReopen;
330
+ return /* @__PURE__ */ jsxs3(
331
+ "button",
332
+ {
333
+ class: "bp-convlist__item",
334
+ onClick: () => isClickable && onSelect(conv),
335
+ disabled: !isClickable,
336
+ children: [
337
+ /* @__PURE__ */ jsxs3("div", { class: "bp-convlist__item-top", children: [
338
+ /* @__PURE__ */ jsx3("span", { class: "bp-convlist__item-title", children: channelInfo.name }),
339
+ /* @__PURE__ */ jsx3(
340
+ "span",
341
+ {
342
+ class: `bp-convlist__item-status ${conv.open ? "bp-convlist__item-status--open" : "bp-convlist__item-status--closed"}`,
343
+ children: conv.open ? t.conversations.open : t.conversations.closed
344
+ }
345
+ )
346
+ ] }),
347
+ /* @__PURE__ */ jsx3("span", { class: "bp-convlist__item-preview", children: conv.lastMessage?.content || t.conversations.noMessages })
348
+ ]
349
+ },
350
+ conv.id
351
+ );
352
+ }) })
353
+ ] });
354
+ }
355
+
356
+ // src/ui/components/MessageInput.tsx
357
+ import { useCallback, useRef } from "preact/hooks";
358
+ import { jsx as jsx4, jsxs as jsxs4 } from "preact/jsx-runtime";
359
+ function MessageInput({
360
+ value,
361
+ onChange,
362
+ onSend,
363
+ onFileSelect,
364
+ onFileRemove,
365
+ attachedFile,
366
+ uploading,
367
+ disabled,
368
+ placeholder,
369
+ t
370
+ }) {
371
+ const textareaRef = useRef(null);
372
+ const fileInputRef = useRef(null);
373
+ const handleKeyDown = useCallback(
374
+ (e) => {
375
+ if (e.key === "Enter" && !e.shiftKey) {
376
+ e.preventDefault();
377
+ onSend();
378
+ }
379
+ },
380
+ [onSend]
381
+ );
382
+ const handleInput = useCallback(
383
+ (e) => {
384
+ const target = e.target;
385
+ onChange(target.value);
386
+ target.style.height = "auto";
387
+ target.style.height = `${Math.min(target.scrollHeight, 100)}px`;
388
+ },
389
+ [onChange]
390
+ );
391
+ const handleAttachClick = useCallback(() => {
392
+ fileInputRef.current?.click();
393
+ }, []);
394
+ const handleFileChange = useCallback(
395
+ (e) => {
396
+ const input = e.target;
397
+ const file = input.files?.[0];
398
+ if (file) {
399
+ onFileSelect(file);
400
+ }
401
+ input.value = "";
402
+ },
403
+ [onFileSelect]
404
+ );
405
+ const isImage = attachedFile?.file.type.startsWith("image/");
406
+ const canSend = (value.trim() || attachedFile) && !disabled && !uploading;
407
+ return /* @__PURE__ */ jsxs4("div", { class: "bp-composer", children: [
408
+ attachedFile && /* @__PURE__ */ jsxs4("div", { class: "bp-composer__preview", children: [
409
+ isImage && attachedFile.preview ? /* @__PURE__ */ jsx4(
410
+ "img",
411
+ {
412
+ src: attachedFile.preview,
413
+ alt: attachedFile.file.name,
414
+ class: "bp-composer__preview-thumb"
415
+ }
416
+ ) : /* @__PURE__ */ jsx4("div", { class: "bp-composer__preview-icon", children: /* @__PURE__ */ jsx4(IconFile, {}) }),
417
+ /* @__PURE__ */ jsxs4("div", { class: "bp-composer__preview-info", children: [
418
+ /* @__PURE__ */ jsx4("div", { class: "bp-composer__preview-name", children: attachedFile.file.name }),
419
+ /* @__PURE__ */ jsx4("div", { class: "bp-composer__preview-status", children: uploading ? t.chat.uploading : formatSize(attachedFile.file.size) })
420
+ ] }),
421
+ !uploading && /* @__PURE__ */ jsx4(
422
+ "button",
423
+ {
424
+ class: "bp-composer__preview-remove",
425
+ onClick: onFileRemove,
426
+ "aria-label": "Remove file",
427
+ children: /* @__PURE__ */ jsx4(IconX, {})
428
+ }
429
+ )
430
+ ] }),
431
+ /* @__PURE__ */ jsxs4("div", { class: "bp-composer__row", children: [
432
+ /* @__PURE__ */ jsx4(
433
+ "button",
434
+ {
435
+ class: "bp-composer__attach",
436
+ onClick: handleAttachClick,
437
+ disabled: disabled || uploading || !!attachedFile,
438
+ "aria-label": t.chat.attachFile,
439
+ children: /* @__PURE__ */ jsx4(IconPaperclip, {})
440
+ }
441
+ ),
442
+ /* @__PURE__ */ jsx4(
443
+ "textarea",
444
+ {
445
+ ref: textareaRef,
446
+ class: "bp-composer__field",
447
+ value,
448
+ onInput: handleInput,
449
+ onKeyDown: handleKeyDown,
450
+ placeholder: placeholder || t.chat.placeholder,
451
+ disabled,
452
+ rows: 1
453
+ }
454
+ ),
455
+ /* @__PURE__ */ jsx4(
456
+ "button",
457
+ {
458
+ class: "bp-composer__send",
459
+ onClick: onSend,
460
+ disabled: !canSend,
461
+ "aria-label": "Send message",
462
+ children: /* @__PURE__ */ jsx4(IconSend, {})
463
+ }
464
+ )
465
+ ] }),
466
+ /* @__PURE__ */ jsx4(
467
+ "input",
468
+ {
469
+ type: "file",
470
+ ref: fileInputRef,
471
+ onChange: handleFileChange,
472
+ style: { display: "none" },
473
+ accept: "image/*,video/mp4,audio/*,.pdf,.doc,.docx,.xls,.xlsx,.txt"
474
+ }
475
+ )
476
+ ] });
477
+ }
478
+ function formatSize(bytes) {
479
+ if (bytes < 1024) return `${bytes} B`;
480
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)} KB`;
481
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
482
+ }
483
+
484
+ // src/ui/components/MessageList.tsx
485
+ import { useEffect as useEffect2, useRef as useRef2, useState } from "preact/hooks";
486
+
487
+ // src/ui/components/ImageLightbox.tsx
488
+ import { useCallback as useCallback2, useEffect } from "preact/hooks";
489
+ import { jsx as jsx5, jsxs as jsxs5 } from "preact/jsx-runtime";
490
+ function ImageLightbox({ src, alt, onClose }) {
491
+ const handleKeyDown = useCallback2(
492
+ (e) => {
493
+ if (e.key === "Escape") onClose();
494
+ },
495
+ [onClose]
496
+ );
497
+ useEffect(() => {
498
+ document.addEventListener("keydown", handleKeyDown);
499
+ return () => document.removeEventListener("keydown", handleKeyDown);
500
+ }, [handleKeyDown]);
501
+ return /* @__PURE__ */ jsxs5("div", { class: "bp-lightbox", onClick: onClose, children: [
502
+ /* @__PURE__ */ jsx5("button", { class: "bp-lightbox__close", onClick: onClose, children: /* @__PURE__ */ jsx5(IconX, {}) }),
503
+ /* @__PURE__ */ jsx5(
504
+ "img",
505
+ {
506
+ src,
507
+ alt: alt || "",
508
+ class: "bp-lightbox__img",
509
+ onClick: (e) => e.stopPropagation()
510
+ }
511
+ )
512
+ ] });
513
+ }
514
+
515
+ // src/ui/components/MessageMedia.tsx
516
+ import { jsx as jsx6, jsxs as jsxs6 } from "preact/jsx-runtime";
517
+ function MessageMedia({ media, onImageClick, t }) {
518
+ const mimeType = (media.mimeType || "").toLowerCase();
519
+ if (mimeType.startsWith("image/") || media.kind === "image") {
520
+ const thumbSrc = media.streamUrlData?.small || media.url;
521
+ const fullSrc = media.streamUrlData?.large || media.url;
522
+ return /* @__PURE__ */ jsx6(
523
+ "img",
524
+ {
525
+ src: thumbSrc,
526
+ alt: media.name,
527
+ class: "bp-media-img",
528
+ onClick: () => onImageClick(fullSrc)
529
+ }
530
+ );
531
+ }
532
+ if (mimeType.startsWith("video/")) {
533
+ return /* @__PURE__ */ jsx6("video", { controls: true, class: "bp-media-video", preload: "metadata", children: /* @__PURE__ */ jsx6("source", { src: media.url, type: mimeType }) });
534
+ }
535
+ return /* @__PURE__ */ jsxs6(
536
+ "a",
537
+ {
538
+ href: media.url,
539
+ target: "_blank",
540
+ rel: "noopener noreferrer",
541
+ class: "bp-media-file",
542
+ download: media.name,
543
+ children: [
544
+ /* @__PURE__ */ jsx6("div", { class: "bp-media-file__icon", children: /* @__PURE__ */ jsx6(IconFile, {}) }),
545
+ /* @__PURE__ */ jsx6("span", { class: "bp-media-file__name", children: media.name }),
546
+ /* @__PURE__ */ jsx6("span", { class: "bp-media-file__download", children: /* @__PURE__ */ jsx6(IconDownload, {}) })
547
+ ]
548
+ }
549
+ );
550
+ }
551
+
552
+ // src/ui/components/MessageList.tsx
553
+ import { jsx as jsx7, jsxs as jsxs7 } from "preact/jsx-runtime";
554
+ function MessageList({ messages, loading, t }) {
555
+ const endRef = useRef2(null);
556
+ const [lightboxSrc, setLightboxSrc] = useState(null);
557
+ useEffect2(() => {
558
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
559
+ }, [messages]);
560
+ if (loading) {
561
+ return /* @__PURE__ */ jsx7("div", { class: "bp-loading", children: /* @__PURE__ */ jsx7("div", { class: "bp-spinner" }) });
562
+ }
563
+ return /* @__PURE__ */ jsxs7("div", { class: "bp-messages", children: [
564
+ messages.map((msg) => /* @__PURE__ */ jsx7(
565
+ MessageBubble,
566
+ {
567
+ message: msg,
568
+ onImageClick: setLightboxSrc,
569
+ t
570
+ },
571
+ msg.id
572
+ )),
573
+ /* @__PURE__ */ jsx7("div", { ref: endRef }),
574
+ lightboxSrc && /* @__PURE__ */ jsx7(
575
+ ImageLightbox,
576
+ {
577
+ src: lightboxSrc,
578
+ onClose: () => setLightboxSrc(null)
579
+ }
580
+ )
581
+ ] });
582
+ }
583
+ function MessageBubble({
584
+ message,
585
+ onImageClick,
586
+ t
587
+ }) {
588
+ const isClient = message.role === "client";
589
+ const cls = isClient ? "bp-msg bp-msg--client" : "bp-msg bp-msg--agent";
590
+ const time = new Date(message.createdAt).toLocaleTimeString([], {
591
+ hour: "2-digit",
592
+ minute: "2-digit"
593
+ });
594
+ return /* @__PURE__ */ jsxs7("div", { class: cls, children: [
595
+ !isClient && /* @__PURE__ */ jsx7("div", { class: "bp-msg__avatar", children: message.user?.avatar?.url ? /* @__PURE__ */ jsx7(
596
+ "img",
597
+ {
598
+ src: message.user.avatar.url,
599
+ alt: message.user.firstName || "Agent"
600
+ }
601
+ ) : (message.user?.firstName?.[0] || "A").toUpperCase() }),
602
+ /* @__PURE__ */ jsxs7("div", { class: "bp-msg__body", children: [
603
+ message.media && /* @__PURE__ */ jsx7(
604
+ MessageMedia,
605
+ {
606
+ media: message.media,
607
+ onImageClick,
608
+ t
609
+ }
610
+ ),
611
+ message.content && /* @__PURE__ */ jsx7("div", { class: "bp-msg__content", children: message.content }),
612
+ /* @__PURE__ */ jsx7("div", { class: "bp-msg__time", children: time })
613
+ ] })
614
+ ] });
615
+ }
616
+
617
+ // src/ui/components/PreChatForm.tsx
618
+ import { useState as useState2 } from "preact/hooks";
619
+ import { jsx as jsx8, jsxs as jsxs8 } from "preact/jsx-runtime";
620
+ function PreChatForm({
621
+ channelInfo,
622
+ onSubmit,
623
+ loading,
624
+ t
625
+ }) {
626
+ const [name, setName] = useState2("");
627
+ const [email, setEmail] = useState2("");
628
+ const { requireName, requireEmail, privacyPolicyUrl } = channelInfo.config;
629
+ const handleSubmit = (e) => {
630
+ e.preventDefault();
631
+ onSubmit({
632
+ name: name.trim() || void 0,
633
+ email: email.trim() || void 0
634
+ });
635
+ };
636
+ const isValid = (!requireName || name.trim()) && (!requireEmail || email.trim());
637
+ return /* @__PURE__ */ jsxs8("form", { class: "bp-prechat", onSubmit: handleSubmit, children: [
638
+ /* @__PURE__ */ jsx8("div", { class: "bp-prechat__title", children: t.prechat.title }),
639
+ /* @__PURE__ */ jsx8("div", { class: "bp-prechat__desc", children: t.prechat.description }),
640
+ requireName && /* @__PURE__ */ jsxs8("div", { class: "bp-prechat__field", children: [
641
+ /* @__PURE__ */ jsx8("label", { class: "bp-prechat__label", children: t.prechat.name }),
642
+ /* @__PURE__ */ jsx8(
643
+ "input",
644
+ {
645
+ class: "bp-prechat__input",
646
+ type: "text",
647
+ value: name,
648
+ onInput: (e) => setName(e.target.value),
649
+ placeholder: t.prechat.namePlaceholder,
650
+ required: true
651
+ }
652
+ )
653
+ ] }),
654
+ requireEmail && /* @__PURE__ */ jsxs8("div", { class: "bp-prechat__field", children: [
655
+ /* @__PURE__ */ jsx8("label", { class: "bp-prechat__label", children: t.prechat.email }),
656
+ /* @__PURE__ */ jsx8(
657
+ "input",
658
+ {
659
+ class: "bp-prechat__input",
660
+ type: "email",
661
+ value: email,
662
+ onInput: (e) => setEmail(e.target.value),
663
+ placeholder: t.prechat.emailPlaceholder,
664
+ required: true
665
+ }
666
+ )
667
+ ] }),
668
+ /* @__PURE__ */ jsx8(
669
+ "button",
670
+ {
671
+ class: "bp-prechat__submit",
672
+ type: "submit",
673
+ disabled: !isValid || loading,
674
+ children: loading ? t.prechat.loading : t.prechat.start
675
+ }
676
+ ),
677
+ privacyPolicyUrl && /* @__PURE__ */ jsxs8("div", { class: "bp-prechat__privacy", children: [
678
+ t.prechat.privacyPrefix,
679
+ " ",
680
+ /* @__PURE__ */ jsx8("a", { href: privacyPolicyUrl, target: "_blank", rel: "noopener noreferrer", children: t.prechat.privacyLink })
681
+ ] })
682
+ ] });
683
+ }
684
+
685
+ // src/ui/components/ChatWindow.tsx
686
+ import { Fragment, jsx as jsx9, jsxs as jsxs9 } from "preact/jsx-runtime";
687
+ function ChatWindow({
688
+ channelInfo,
689
+ apiClient,
690
+ realtimeClient,
691
+ storage,
692
+ events,
693
+ visitor,
694
+ isAuthenticated,
695
+ position,
696
+ onClose,
697
+ t,
698
+ initialConversationId
699
+ }) {
700
+ const [view, setView] = useState3("chat");
701
+ const [conversation, setConversation] = useState3(null);
702
+ const [conversations, setConversations] = useState3([]);
703
+ const [messages, setMessages] = useState3([]);
704
+ const [inputValue, setInputValue] = useState3("");
705
+ const [loading, setLoading] = useState3(true);
706
+ const [sending, setSending] = useState3(false);
707
+ const [attachedFile, setAttachedFile] = useState3(null);
708
+ const [uploadedFileId, setUploadedFileId] = useState3(null);
709
+ const [uploading, setUploading] = useState3(false);
710
+ const isOpen = conversation?.open !== false;
711
+ const allowViewHistory = channelInfo.config.allowViewHistory && isAuthenticated;
712
+ useEffect3(() => {
713
+ const init = async () => {
714
+ setLoading(true);
715
+ try {
716
+ if (allowViewHistory) {
717
+ const convs = await apiClient.getVisitorConversations();
718
+ setConversations(convs);
719
+ if (initialConversationId) {
720
+ const conv = convs.find((c) => c.id === initialConversationId);
721
+ if (conv) {
722
+ await openConversation(conv);
723
+ return;
724
+ }
725
+ }
726
+ if (convs.length > 0) {
727
+ setView("conversations");
728
+ } else {
729
+ if (needsPreChat()) {
730
+ setView("prechat");
731
+ } else {
732
+ await startNewConversation();
733
+ }
734
+ }
735
+ } else {
736
+ const storedId = initialConversationId || storage.getConversationId();
737
+ if (storedId) {
738
+ try {
739
+ const msgs = await apiClient.getMessages(storedId, { limit: 50 });
740
+ setMessages(Array.isArray(msgs) ? msgs.reverse() : []);
741
+ setConversation({ id: storedId, open: true });
742
+ setView("chat");
743
+ connectRealtime(storedId);
744
+ } catch {
745
+ storage.clear();
746
+ if (needsPreChat()) {
747
+ setView("prechat");
748
+ } else {
749
+ await startNewConversation();
750
+ }
751
+ }
752
+ } else {
753
+ if (needsPreChat()) {
754
+ setView("prechat");
755
+ } else {
756
+ await startNewConversation();
757
+ }
758
+ }
759
+ }
760
+ } catch (e) {
761
+ console.error("[BaseportalChat] Error initializing:", e);
762
+ } finally {
763
+ setLoading(false);
764
+ }
765
+ };
766
+ init();
767
+ return () => {
768
+ realtimeClient.unsubscribe();
769
+ };
770
+ }, []);
771
+ const needsPreChat = useCallback3(() => {
772
+ if (visitor?.name && visitor?.email) return false;
773
+ return channelInfo.config.requireName || channelInfo.config.requireEmail;
774
+ }, [channelInfo, visitor]);
775
+ const connectRealtime = useCallback3(
776
+ (convId) => {
777
+ realtimeClient.subscribe(convId, {
778
+ onMessage: (msg) => {
779
+ setMessages((prev) => {
780
+ if (prev.some((m) => m.id === msg.id)) {
781
+ return prev.map(
782
+ (m) => m.id === msg.id ? { ...m, ...msg } : m
783
+ );
784
+ }
785
+ const withoutTemp = prev.filter(
786
+ (m) => !String(m.id).startsWith("temp-") || m.content !== msg.content
787
+ );
788
+ return [...withoutTemp, msg];
789
+ });
790
+ events.emit("message:received", msg);
791
+ },
792
+ onConversationStatusUpdate: (conv) => {
793
+ setConversation(
794
+ (prev) => prev ? { ...prev, open: conv.open } : prev
795
+ );
796
+ if (!conv.open) {
797
+ events.emit("conversation:closed", conv);
798
+ }
799
+ }
800
+ });
801
+ },
802
+ [realtimeClient, events]
803
+ );
804
+ const openConversation = useCallback3(
805
+ async (conv) => {
806
+ setLoading(true);
807
+ try {
808
+ const msgs = await apiClient.getMessages(conv.id, { limit: 50 });
809
+ setMessages(Array.isArray(msgs) ? msgs.reverse() : []);
810
+ setConversation(conv);
811
+ setView("chat");
812
+ storage.setConversationId(conv.id);
813
+ connectRealtime(conv.id);
814
+ } catch (e) {
815
+ console.error("[BaseportalChat] Error opening conversation:", e);
816
+ } finally {
817
+ setLoading(false);
818
+ }
819
+ },
820
+ [apiClient, storage, connectRealtime]
821
+ );
822
+ const startNewConversation = useCallback3(async () => {
823
+ setLoading(true);
824
+ try {
825
+ const result = await apiClient.initConversation({
826
+ name: visitor?.name,
827
+ email: visitor?.email
828
+ });
829
+ setConversation(result);
830
+ setMessages(result.messages || []);
831
+ setView("chat");
832
+ storage.setConversationId(result.id);
833
+ connectRealtime(result.id);
834
+ events.emit("conversation:started", result);
835
+ } catch (e) {
836
+ console.error("[BaseportalChat] Error starting conversation:", e);
837
+ } finally {
838
+ setLoading(false);
839
+ }
840
+ }, [apiClient, visitor, storage, connectRealtime, events]);
841
+ const handlePreChatSubmit = useCallback3(
842
+ async (data) => {
843
+ storage.setVisitor({ ...visitor, ...data });
844
+ const result = await apiClient.initConversation(data);
845
+ setConversation(result);
846
+ setMessages(result.messages || []);
847
+ setView("chat");
848
+ storage.setConversationId(result.id);
849
+ connectRealtime(result.id);
850
+ events.emit("conversation:started", result);
851
+ },
852
+ [apiClient, visitor, storage, connectRealtime, events]
853
+ );
854
+ const handleFileSelect = useCallback3(
855
+ async (file) => {
856
+ if (!conversation) return;
857
+ const MAX_SIZE = 25 * 1024 * 1024;
858
+ if (file.size > MAX_SIZE) {
859
+ console.warn("[BaseportalChat] File too large");
860
+ return;
861
+ }
862
+ const preview = file.type.startsWith("image/") ? URL.createObjectURL(file) : void 0;
863
+ setAttachedFile({ file, preview });
864
+ setUploading(true);
865
+ try {
866
+ const uploaded = await apiClient.uploadFile(conversation.id, file);
867
+ setUploadedFileId(uploaded.id);
868
+ } catch (e) {
869
+ console.error("[BaseportalChat] Error uploading file:", e);
870
+ setAttachedFile(null);
871
+ if (preview) URL.revokeObjectURL(preview);
872
+ } finally {
873
+ setUploading(false);
874
+ }
875
+ },
876
+ [apiClient, conversation]
877
+ );
878
+ const handleFileRemove = useCallback3(() => {
879
+ if (attachedFile?.preview) URL.revokeObjectURL(attachedFile.preview);
880
+ setAttachedFile(null);
881
+ setUploadedFileId(null);
882
+ }, [attachedFile]);
883
+ const handleSend = useCallback3(async () => {
884
+ const content = inputValue.trim();
885
+ if (!content && !uploadedFileId || !conversation || sending) return;
886
+ const tempId = `temp-${Date.now()}`;
887
+ const optimistic = {
888
+ id: tempId,
889
+ content,
890
+ role: "client",
891
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
892
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
893
+ };
894
+ const mediaId = uploadedFileId || void 0;
895
+ setInputValue("");
896
+ setAttachedFile(null);
897
+ setUploadedFileId(null);
898
+ setSending(true);
899
+ setMessages((prev) => [...prev, optimistic]);
900
+ try {
901
+ const msg = await apiClient.sendMessage(conversation.id, {
902
+ content: content || void 0,
903
+ mediaId
904
+ });
905
+ setMessages((prev) => prev.map((m) => m.id === tempId ? msg : m));
906
+ events.emit("message:sent", msg);
907
+ } catch (e) {
908
+ console.error("[BaseportalChat] Error sending message:", e);
909
+ setMessages((prev) => prev.filter((m) => m.id !== tempId));
910
+ setInputValue(content);
911
+ } finally {
912
+ setSending(false);
913
+ }
914
+ }, [inputValue, uploadedFileId, conversation, sending, apiClient, events]);
915
+ const handleReopen = useCallback3(async () => {
916
+ if (!conversation) return;
917
+ try {
918
+ const updated = await apiClient.reopenConversation(conversation.id);
919
+ setConversation((prev) => prev ? { ...prev, open: updated.open ?? true } : prev);
920
+ } catch (e) {
921
+ console.error("[BaseportalChat] Error reopening conversation:", e);
922
+ }
923
+ }, [conversation, apiClient]);
924
+ const handleBack = useCallback3(() => {
925
+ if (allowViewHistory && view === "chat") {
926
+ realtimeClient.unsubscribe();
927
+ setView("conversations");
928
+ setConversation(null);
929
+ setMessages([]);
930
+ apiClient.getVisitorConversations().then(setConversations).catch(() => {
931
+ });
932
+ } else {
933
+ onClose();
934
+ }
935
+ }, [allowViewHistory, view, realtimeClient, apiClient, onClose]);
936
+ const posClass = position === "bottom-left" ? "bp-window--left" : "bp-window--right";
937
+ const headerTitle = view === "conversations" ? t.conversations.title : channelInfo.name;
938
+ const showBack = allowViewHistory && view === "chat" || view === "prechat";
939
+ return /* @__PURE__ */ jsxs9("div", { class: `bp-window ${posClass}`, children: [
940
+ /* @__PURE__ */ jsxs9("div", { class: "bp-header", children: [
941
+ /* @__PURE__ */ jsxs9("div", { class: "bp-header__title", children: [
942
+ showBack && /* @__PURE__ */ jsx9("button", { class: "bp-header__back", onClick: handleBack, children: /* @__PURE__ */ jsx9(IconArrowLeft, {}) }),
943
+ headerTitle
944
+ ] }),
945
+ /* @__PURE__ */ jsx9("button", { class: "bp-header__close", onClick: onClose, children: /* @__PURE__ */ jsx9(IconX, {}) })
946
+ ] }),
947
+ loading && view !== "chat" ? /* @__PURE__ */ jsx9("div", { class: "bp-loading", children: /* @__PURE__ */ jsx9("div", { class: "bp-spinner" }) }) : /* @__PURE__ */ jsxs9(Fragment, { children: [
948
+ view === "prechat" && /* @__PURE__ */ jsx9(
949
+ PreChatForm,
950
+ {
951
+ channelInfo,
952
+ onSubmit: handlePreChatSubmit,
953
+ loading,
954
+ t
955
+ }
956
+ ),
957
+ view === "conversations" && /* @__PURE__ */ jsx9(
958
+ ConversationList,
959
+ {
960
+ conversations,
961
+ channelInfo,
962
+ loading,
963
+ onSelect: openConversation,
964
+ onNew: needsPreChat() ? () => setView("prechat") : startNewConversation,
965
+ t
966
+ }
967
+ ),
968
+ view === "chat" && /* @__PURE__ */ jsxs9(Fragment, { children: [
969
+ /* @__PURE__ */ jsx9(MessageList, { messages, loading, t }),
970
+ isOpen ? /* @__PURE__ */ jsx9(
971
+ MessageInput,
972
+ {
973
+ value: inputValue,
974
+ onChange: setInputValue,
975
+ onSend: handleSend,
976
+ onFileSelect: handleFileSelect,
977
+ onFileRemove: handleFileRemove,
978
+ attachedFile,
979
+ uploading,
980
+ disabled: sending || loading,
981
+ placeholder: t.chat.placeholder,
982
+ t
983
+ }
984
+ ) : /* @__PURE__ */ jsxs9("div", { class: "bp-closed-banner", children: [
985
+ /* @__PURE__ */ jsx9("span", { class: "bp-closed-banner__text", children: t.chat.closed }),
986
+ channelInfo.config.allowReopenConversation && /* @__PURE__ */ jsx9("button", { class: "bp-closed-banner__reopen", onClick: handleReopen, children: t.chat.reopen })
987
+ ] }),
988
+ channelInfo.config.privacyPolicyUrl && /* @__PURE__ */ jsx9("div", { class: "bp-privacy-footer", children: /* @__PURE__ */ jsx9(
989
+ "a",
990
+ {
991
+ href: channelInfo.config.privacyPolicyUrl,
992
+ target: "_blank",
993
+ rel: "noopener noreferrer",
994
+ children: t.prechat.privacyLink
995
+ }
996
+ ) })
997
+ ] })
998
+ ] })
999
+ ] });
1000
+ }
1001
+
1002
+ // src/ui/App.tsx
1003
+ import { Fragment as Fragment2, jsx as jsx10, jsxs as jsxs10 } from "preact/jsx-runtime";
1004
+ function App({
1005
+ channelInfo,
1006
+ apiClient,
1007
+ realtimeClient,
1008
+ storage,
1009
+ events,
1010
+ visitor,
1011
+ isAuthenticated,
1012
+ position,
1013
+ hidden,
1014
+ t,
1015
+ isOpenRef,
1016
+ setIsOpen
1017
+ }) {
1018
+ const [isOpen, setIsOpenState] = useState4(isOpenRef.current);
1019
+ const [isHidden, setIsHidden] = useState4(hidden);
1020
+ const [unreadCount] = useState4(0);
1021
+ useEffect4(() => {
1022
+ const onOpen = () => {
1023
+ setIsOpenState(true);
1024
+ isOpenRef.current = true;
1025
+ setIsOpen(true);
1026
+ };
1027
+ const onClose = () => {
1028
+ setIsOpenState(false);
1029
+ isOpenRef.current = false;
1030
+ setIsOpen(false);
1031
+ };
1032
+ const onShow = () => setIsHidden(false);
1033
+ const onHide = () => {
1034
+ setIsHidden(true);
1035
+ setIsOpenState(false);
1036
+ isOpenRef.current = false;
1037
+ setIsOpen(false);
1038
+ };
1039
+ events.on("_open", onOpen);
1040
+ events.on("_close", onClose);
1041
+ events.on("show", onShow);
1042
+ events.on("hide", onHide);
1043
+ return () => {
1044
+ events.off("_open", onOpen);
1045
+ events.off("_close", onClose);
1046
+ events.off("show", onShow);
1047
+ events.off("hide", onHide);
1048
+ };
1049
+ }, [events, isOpenRef, setIsOpen]);
1050
+ const handleToggle = () => {
1051
+ const next = !isOpen;
1052
+ setIsOpenState(next);
1053
+ isOpenRef.current = next;
1054
+ setIsOpen(next);
1055
+ events.emit(next ? "open" : "close");
1056
+ };
1057
+ const handleClose = () => {
1058
+ setIsOpenState(false);
1059
+ isOpenRef.current = false;
1060
+ setIsOpen(false);
1061
+ events.emit("close");
1062
+ };
1063
+ return /* @__PURE__ */ jsxs10(Fragment2, { children: [
1064
+ !isHidden && /* @__PURE__ */ jsx10(
1065
+ ChatBubble,
1066
+ {
1067
+ isOpen,
1068
+ position,
1069
+ unreadCount,
1070
+ onClick: handleToggle
1071
+ }
1072
+ ),
1073
+ isOpen && /* @__PURE__ */ jsx10(
1074
+ ChatWindow,
1075
+ {
1076
+ channelInfo,
1077
+ apiClient,
1078
+ realtimeClient,
1079
+ storage,
1080
+ events,
1081
+ visitor,
1082
+ isAuthenticated,
1083
+ position,
1084
+ onClose: handleClose,
1085
+ t
1086
+ }
1087
+ )
1088
+ ] });
1089
+ }
1090
+
1091
+ // src/ui/styles/widget-css.ts
1092
+ var widget_css_default = `#baseportal-chat-widget {
1093
+ display: block;
1094
+ position: static;
1095
+ width: 0;
1096
+ height: 0;
1097
+ overflow: visible;
1098
+ padding: 0;
1099
+ margin: 0;
1100
+ border: none;
1101
+
1102
+ --bp-primary: #6366f1;
1103
+ --bp-primary-contrast: #ffffff;
1104
+ --bp-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
1105
+ 'Helvetica Neue', Arial, sans-serif;
1106
+ --bp-radius: 16px;
1107
+ --bp-radius-sm: 8px;
1108
+ --bp-bubble-size: 60px;
1109
+ --bp-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
1110
+ --bp-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.1);
1111
+ --bp-gray-50: #f9fafb;
1112
+ --bp-gray-100: #f3f4f6;
1113
+ --bp-gray-200: #e5e7eb;
1114
+ --bp-gray-300: #d1d5db;
1115
+ --bp-gray-500: #6b7280;
1116
+ --bp-gray-700: #374151;
1117
+ --bp-gray-900: #111827;
1118
+ --bp-transition: 0.2s ease;
1119
+ }
1120
+
1121
+ #baseportal-chat-widget *,
1122
+ #baseportal-chat-widget *::before,
1123
+ #baseportal-chat-widget *::after {
1124
+ box-sizing: border-box;
1125
+ }
1126
+
1127
+ /* ===== Chat Bubble (FAB) ===== */
1128
+ .bp-bubble {
1129
+ pointer-events: auto;
1130
+ position: fixed;
1131
+ bottom: 24px;
1132
+ z-index: 2147483646;
1133
+ width: var(--bp-bubble-size);
1134
+ height: var(--bp-bubble-size);
1135
+ border-radius: 50%;
1136
+ background: var(--bp-primary);
1137
+ color: var(--bp-primary-contrast);
1138
+ border: none;
1139
+ cursor: pointer;
1140
+ display: flex;
1141
+ align-items: center;
1142
+ justify-content: center;
1143
+ box-shadow: var(--bp-shadow);
1144
+ transition: transform var(--bp-transition), box-shadow var(--bp-transition);
1145
+ font-family: var(--bp-font-family);
1146
+ font-size: 14px;
1147
+ line-height: 1.5;
1148
+ margin: 0;
1149
+ padding: 0;
1150
+ }
1151
+
1152
+ .bp-bubble:hover {
1153
+ transform: scale(1.08);
1154
+ box-shadow: 0 6px 32px rgba(0, 0, 0, 0.2);
1155
+ }
1156
+
1157
+ .bp-bubble--right {
1158
+ right: 24px;
1159
+ }
1160
+
1161
+ .bp-bubble--left {
1162
+ left: 24px;
1163
+ }
1164
+
1165
+ .bp-bubble svg {
1166
+ width: 28px;
1167
+ height: 28px;
1168
+ fill: currentColor;
1169
+ }
1170
+
1171
+ .bp-bubble__badge {
1172
+ position: absolute;
1173
+ top: -2px;
1174
+ right: -2px;
1175
+ min-width: 20px;
1176
+ height: 20px;
1177
+ border-radius: 10px;
1178
+ background: #ef4444;
1179
+ color: #fff;
1180
+ font-size: 11px;
1181
+ font-weight: 600;
1182
+ display: flex;
1183
+ align-items: center;
1184
+ justify-content: center;
1185
+ padding: 0 5px;
1186
+ }
1187
+
1188
+ /* ===== Chat Window ===== */
1189
+ .bp-window {
1190
+ pointer-events: auto;
1191
+ position: fixed;
1192
+ bottom: 96px;
1193
+ z-index: 2147483647;
1194
+ width: 380px;
1195
+ height: 520px;
1196
+ border-radius: var(--bp-radius);
1197
+ background: #fff;
1198
+ box-shadow: var(--bp-shadow);
1199
+ display: flex;
1200
+ flex-direction: column;
1201
+ overflow: hidden;
1202
+ animation: bp-slide-up 0.25s ease-out;
1203
+ font-family: var(--bp-font-family);
1204
+ font-size: 14px;
1205
+ line-height: 1.5;
1206
+ color: var(--bp-gray-900);
1207
+ }
1208
+
1209
+ .bp-window--right {
1210
+ right: 24px;
1211
+ }
1212
+
1213
+ .bp-window--left {
1214
+ left: 24px;
1215
+ }
1216
+
1217
+ @keyframes bp-slide-up {
1218
+ from {
1219
+ opacity: 0;
1220
+ transform: translateY(16px);
1221
+ }
1222
+ to {
1223
+ opacity: 1;
1224
+ transform: translateY(0);
1225
+ }
1226
+ }
1227
+
1228
+ /* Mobile fullscreen */
1229
+ @media (max-width: 480px) {
1230
+ .bp-window {
1231
+ top: 0;
1232
+ left: 0;
1233
+ right: 0;
1234
+ bottom: 0;
1235
+ width: 100%;
1236
+ height: 100%;
1237
+ border-radius: 0;
1238
+ }
1239
+ }
1240
+
1241
+ /* ===== Header ===== */
1242
+ .bp-header {
1243
+ padding: 16px;
1244
+ background: var(--bp-primary);
1245
+ color: var(--bp-primary-contrast);
1246
+ display: flex;
1247
+ align-items: center;
1248
+ justify-content: space-between;
1249
+ flex-shrink: 0;
1250
+ }
1251
+
1252
+ .bp-header__title {
1253
+ font-size: 16px;
1254
+ font-weight: 600;
1255
+ color: inherit;
1256
+ display: flex;
1257
+ align-items: center;
1258
+ gap: 8px;
1259
+ }
1260
+
1261
+ .bp-header__back,
1262
+ .bp-header__close {
1263
+ background: none;
1264
+ border: none;
1265
+ color: inherit;
1266
+ cursor: pointer;
1267
+ padding: 4px;
1268
+ border-radius: 4px;
1269
+ display: flex;
1270
+ align-items: center;
1271
+ justify-content: center;
1272
+ transition: background var(--bp-transition);
1273
+ }
1274
+
1275
+ .bp-header__back:hover,
1276
+ .bp-header__close:hover {
1277
+ background: rgba(255, 255, 255, 0.15);
1278
+ }
1279
+
1280
+ .bp-header__back svg,
1281
+ .bp-header__close svg {
1282
+ width: 18px;
1283
+ height: 18px;
1284
+ fill: currentColor;
1285
+ }
1286
+
1287
+ /* ===== Message List ===== */
1288
+ .bp-messages {
1289
+ flex: 1;
1290
+ overflow-y: auto;
1291
+ padding: 16px;
1292
+ display: flex;
1293
+ flex-direction: column;
1294
+ gap: 8px;
1295
+ background: var(--bp-gray-50);
1296
+ }
1297
+
1298
+ .bp-messages::-webkit-scrollbar {
1299
+ width: 4px;
1300
+ }
1301
+
1302
+ .bp-messages::-webkit-scrollbar-thumb {
1303
+ background: var(--bp-gray-300);
1304
+ border-radius: 2px;
1305
+ }
1306
+
1307
+ /* ===== Message Bubble ===== */
1308
+ .bp-msg {
1309
+ display: flex;
1310
+ max-width: 80%;
1311
+ }
1312
+
1313
+ .bp-msg--client {
1314
+ align-self: flex-end;
1315
+ flex-direction: row-reverse;
1316
+ }
1317
+
1318
+ .bp-msg--agent {
1319
+ align-self: flex-start;
1320
+ }
1321
+
1322
+ .bp-msg__avatar {
1323
+ width: 28px;
1324
+ height: 28px;
1325
+ border-radius: 50%;
1326
+ background: var(--bp-primary);
1327
+ color: var(--bp-primary-contrast);
1328
+ font-size: 12px;
1329
+ font-weight: 600;
1330
+ display: flex;
1331
+ align-items: center;
1332
+ justify-content: center;
1333
+ flex-shrink: 0;
1334
+ margin-top: auto;
1335
+ }
1336
+
1337
+ .bp-msg__avatar img {
1338
+ width: 100%;
1339
+ height: 100%;
1340
+ border-radius: 50%;
1341
+ object-fit: cover;
1342
+ }
1343
+
1344
+ .bp-msg__body {
1345
+ margin: 0 8px;
1346
+ }
1347
+
1348
+ .bp-msg__content {
1349
+ padding: 10px 14px;
1350
+ border-radius: 16px;
1351
+ white-space: pre-wrap;
1352
+ word-break: break-word;
1353
+ font-size: 14px;
1354
+ line-height: 1.4;
1355
+ }
1356
+
1357
+ .bp-msg--client .bp-msg__content {
1358
+ background: var(--bp-primary);
1359
+ color: var(--bp-primary-contrast);
1360
+ border-bottom-right-radius: 4px;
1361
+ }
1362
+
1363
+ .bp-msg--agent .bp-msg__content {
1364
+ background: var(--bp-gray-100);
1365
+ color: var(--bp-gray-900);
1366
+ border-bottom-left-radius: 4px;
1367
+ }
1368
+
1369
+ .bp-msg__time {
1370
+ font-size: 11px;
1371
+ color: var(--bp-gray-500);
1372
+ margin-top: 2px;
1373
+ padding: 0 4px;
1374
+ }
1375
+
1376
+ .bp-msg--client .bp-msg__time {
1377
+ text-align: right;
1378
+ }
1379
+
1380
+ /* ===== Composer (Message Input) ===== */
1381
+ .bp-composer {
1382
+ margin: 8px 12px 12px;
1383
+ border: 1px solid var(--bp-gray-200);
1384
+ border-radius: 12px;
1385
+ background: #fff;
1386
+ flex-shrink: 0;
1387
+ overflow: hidden;
1388
+ transition: border-color var(--bp-transition);
1389
+ }
1390
+
1391
+ .bp-composer:focus-within {
1392
+ border-color: var(--bp-primary);
1393
+ }
1394
+
1395
+ .bp-composer__preview {
1396
+ padding: 8px 12px;
1397
+ border-bottom: 1px solid var(--bp-gray-100);
1398
+ display: flex;
1399
+ align-items: center;
1400
+ gap: 8px;
1401
+ background: var(--bp-gray-50);
1402
+ }
1403
+
1404
+ .bp-composer__preview-thumb {
1405
+ width: 40px;
1406
+ height: 40px;
1407
+ border-radius: 6px;
1408
+ object-fit: cover;
1409
+ flex-shrink: 0;
1410
+ }
1411
+
1412
+ .bp-composer__preview-icon {
1413
+ width: 36px;
1414
+ height: 36px;
1415
+ border-radius: 6px;
1416
+ background: var(--bp-gray-100);
1417
+ display: flex;
1418
+ align-items: center;
1419
+ justify-content: center;
1420
+ flex-shrink: 0;
1421
+ color: var(--bp-gray-500);
1422
+ }
1423
+
1424
+ .bp-composer__preview-icon svg {
1425
+ width: 18px;
1426
+ height: 18px;
1427
+ }
1428
+
1429
+ .bp-composer__preview-info {
1430
+ flex: 1;
1431
+ min-width: 0;
1432
+ }
1433
+
1434
+ .bp-composer__preview-name {
1435
+ font-size: 12px;
1436
+ font-weight: 500;
1437
+ color: var(--bp-gray-700);
1438
+ overflow: hidden;
1439
+ text-overflow: ellipsis;
1440
+ white-space: nowrap;
1441
+ }
1442
+
1443
+ .bp-composer__preview-status {
1444
+ font-size: 11px;
1445
+ color: var(--bp-gray-500);
1446
+ }
1447
+
1448
+ .bp-composer__preview-remove {
1449
+ background: none;
1450
+ border: none;
1451
+ cursor: pointer;
1452
+ padding: 2px;
1453
+ color: var(--bp-gray-500);
1454
+ display: flex;
1455
+ align-items: center;
1456
+ justify-content: center;
1457
+ flex-shrink: 0;
1458
+ }
1459
+
1460
+ .bp-composer__preview-remove svg {
1461
+ width: 14px;
1462
+ height: 14px;
1463
+ }
1464
+
1465
+ .bp-composer__preview-remove:hover {
1466
+ color: var(--bp-gray-700);
1467
+ }
1468
+
1469
+ .bp-composer__row {
1470
+ display: flex;
1471
+ align-items: flex-end;
1472
+ padding: 4px;
1473
+ }
1474
+
1475
+ .bp-composer__attach {
1476
+ width: 32px;
1477
+ height: 32px;
1478
+ background: none;
1479
+ border: none;
1480
+ cursor: pointer;
1481
+ color: var(--bp-gray-500);
1482
+ display: flex;
1483
+ align-items: center;
1484
+ justify-content: center;
1485
+ flex-shrink: 0;
1486
+ border-radius: 6px;
1487
+ transition: color var(--bp-transition), background var(--bp-transition);
1488
+ }
1489
+
1490
+ .bp-composer__attach:hover {
1491
+ color: var(--bp-primary);
1492
+ background: var(--bp-gray-50);
1493
+ }
1494
+
1495
+ .bp-composer__attach:disabled {
1496
+ opacity: 0.4;
1497
+ cursor: not-allowed;
1498
+ }
1499
+
1500
+ .bp-composer__attach svg {
1501
+ width: 18px;
1502
+ height: 18px;
1503
+ }
1504
+
1505
+ .bp-composer__field {
1506
+ flex: 1;
1507
+ border: none;
1508
+ padding: 6px 4px;
1509
+ font-size: 14px;
1510
+ font-family: inherit;
1511
+ line-height: 1.4;
1512
+ resize: none;
1513
+ outline: none;
1514
+ max-height: 100px;
1515
+ overflow-y: auto;
1516
+ color: var(--bp-gray-900);
1517
+ background: transparent;
1518
+ }
1519
+
1520
+ .bp-composer__field::placeholder {
1521
+ color: var(--bp-gray-500);
1522
+ }
1523
+
1524
+ .bp-composer__field:disabled {
1525
+ cursor: not-allowed;
1526
+ }
1527
+
1528
+ .bp-composer__send {
1529
+ width: 32px;
1530
+ height: 32px;
1531
+ border-radius: 50%;
1532
+ background: var(--bp-primary);
1533
+ color: var(--bp-primary-contrast);
1534
+ border: none;
1535
+ cursor: pointer;
1536
+ display: flex;
1537
+ align-items: center;
1538
+ justify-content: center;
1539
+ flex-shrink: 0;
1540
+ transition: opacity var(--bp-transition);
1541
+ }
1542
+
1543
+ .bp-composer__send:disabled {
1544
+ opacity: 0.4;
1545
+ cursor: not-allowed;
1546
+ }
1547
+
1548
+ .bp-composer__send svg {
1549
+ width: 16px;
1550
+ height: 16px;
1551
+ }
1552
+
1553
+ /* ===== Message Media ===== */
1554
+ .bp-media-img {
1555
+ max-width: 100%;
1556
+ max-height: 240px;
1557
+ border-radius: 8px;
1558
+ cursor: pointer;
1559
+ display: block;
1560
+ object-fit: contain;
1561
+ }
1562
+
1563
+ .bp-media-video {
1564
+ max-width: 100%;
1565
+ max-height: 240px;
1566
+ border-radius: 8px;
1567
+ display: block;
1568
+ }
1569
+
1570
+ .bp-media-file {
1571
+ display: flex;
1572
+ align-items: center;
1573
+ gap: 8px;
1574
+ padding: 8px 12px;
1575
+ background: var(--bp-gray-50);
1576
+ border-radius: 8px;
1577
+ text-decoration: none;
1578
+ color: var(--bp-gray-700);
1579
+ transition: background var(--bp-transition);
1580
+ }
1581
+
1582
+ .bp-media-file:hover {
1583
+ background: var(--bp-gray-100);
1584
+ }
1585
+
1586
+ .bp-media-file__icon {
1587
+ width: 32px;
1588
+ height: 32px;
1589
+ border-radius: 6px;
1590
+ background: var(--bp-gray-200);
1591
+ display: flex;
1592
+ align-items: center;
1593
+ justify-content: center;
1594
+ flex-shrink: 0;
1595
+ color: var(--bp-gray-500);
1596
+ }
1597
+
1598
+ .bp-media-file__icon svg {
1599
+ width: 16px;
1600
+ height: 16px;
1601
+ }
1602
+
1603
+ .bp-media-file__name {
1604
+ flex: 1;
1605
+ font-size: 13px;
1606
+ font-weight: 500;
1607
+ overflow: hidden;
1608
+ text-overflow: ellipsis;
1609
+ white-space: nowrap;
1610
+ min-width: 0;
1611
+ }
1612
+
1613
+ .bp-media-file__download svg {
1614
+ width: 16px;
1615
+ height: 16px;
1616
+ color: var(--bp-primary);
1617
+ }
1618
+
1619
+ /* ===== Image Lightbox ===== */
1620
+ .bp-lightbox {
1621
+ position: fixed;
1622
+ inset: 0;
1623
+ z-index: 2147483647;
1624
+ background: rgba(0, 0, 0, 0.85);
1625
+ display: flex;
1626
+ align-items: center;
1627
+ justify-content: center;
1628
+ cursor: pointer;
1629
+ }
1630
+
1631
+ .bp-lightbox__img {
1632
+ max-width: 90vw;
1633
+ max-height: 90vh;
1634
+ object-fit: contain;
1635
+ border-radius: 4px;
1636
+ cursor: default;
1637
+ }
1638
+
1639
+ .bp-lightbox__close {
1640
+ position: absolute;
1641
+ top: 16px;
1642
+ right: 16px;
1643
+ width: 36px;
1644
+ height: 36px;
1645
+ border-radius: 50%;
1646
+ background: rgba(255, 255, 255, 0.15);
1647
+ color: #fff;
1648
+ border: none;
1649
+ cursor: pointer;
1650
+ display: flex;
1651
+ align-items: center;
1652
+ justify-content: center;
1653
+ transition: background var(--bp-transition);
1654
+ }
1655
+
1656
+ .bp-lightbox__close:hover {
1657
+ background: rgba(255, 255, 255, 0.3);
1658
+ }
1659
+
1660
+ .bp-lightbox__close svg {
1661
+ width: 20px;
1662
+ height: 20px;
1663
+ }
1664
+
1665
+ /* ===== Pre-Chat Form ===== */
1666
+ .bp-prechat {
1667
+ flex: 1;
1668
+ padding: 24px 16px;
1669
+ display: flex;
1670
+ flex-direction: column;
1671
+ gap: 16px;
1672
+ overflow-y: auto;
1673
+ }
1674
+
1675
+ .bp-prechat__title {
1676
+ font-size: 16px;
1677
+ font-weight: 600;
1678
+ color: var(--bp-gray-900);
1679
+ }
1680
+
1681
+ .bp-prechat__desc {
1682
+ font-size: 13px;
1683
+ color: var(--bp-gray-500);
1684
+ }
1685
+
1686
+ .bp-prechat__field {
1687
+ display: flex;
1688
+ flex-direction: column;
1689
+ gap: 4px;
1690
+ }
1691
+
1692
+ .bp-prechat__label {
1693
+ font-size: 13px;
1694
+ font-weight: 500;
1695
+ color: var(--bp-gray-700);
1696
+ }
1697
+
1698
+ .bp-prechat__input {
1699
+ border: 1px solid var(--bp-gray-200);
1700
+ border-radius: var(--bp-radius-sm);
1701
+ padding: 10px 12px;
1702
+ font-size: 14px;
1703
+ font-family: inherit;
1704
+ outline: none;
1705
+ transition: border-color var(--bp-transition);
1706
+ color: var(--bp-gray-900);
1707
+ background: #fff;
1708
+ }
1709
+
1710
+ .bp-prechat__input:focus {
1711
+ border-color: var(--bp-primary);
1712
+ }
1713
+
1714
+ .bp-prechat__submit {
1715
+ padding: 10px 20px;
1716
+ border-radius: var(--bp-radius-sm);
1717
+ background: var(--bp-primary);
1718
+ color: var(--bp-primary-contrast);
1719
+ border: none;
1720
+ font-size: 14px;
1721
+ font-weight: 500;
1722
+ font-family: inherit;
1723
+ cursor: pointer;
1724
+ transition: opacity var(--bp-transition);
1725
+ }
1726
+
1727
+ .bp-prechat__submit:disabled {
1728
+ opacity: 0.6;
1729
+ cursor: not-allowed;
1730
+ }
1731
+
1732
+ .bp-prechat__privacy {
1733
+ font-size: 12px;
1734
+ color: var(--bp-gray-500);
1735
+ text-align: center;
1736
+ margin-top: auto;
1737
+ }
1738
+
1739
+ .bp-prechat__privacy a {
1740
+ color: var(--bp-primary);
1741
+ text-decoration: underline;
1742
+ }
1743
+
1744
+ /* ===== Conversation List ===== */
1745
+ .bp-convlist {
1746
+ flex: 1;
1747
+ overflow-y: auto;
1748
+ display: flex;
1749
+ flex-direction: column;
1750
+ }
1751
+
1752
+ .bp-convlist__new {
1753
+ padding: 12px 16px;
1754
+ border-bottom: 1px solid var(--bp-gray-200);
1755
+ }
1756
+
1757
+ .bp-convlist__new-btn {
1758
+ width: 100%;
1759
+ padding: 10px 16px;
1760
+ border-radius: var(--bp-radius-sm);
1761
+ background: rgba(99, 102, 241, 0.08);
1762
+ border: none;
1763
+ cursor: pointer;
1764
+ display: flex;
1765
+ align-items: center;
1766
+ gap: 8px;
1767
+ font-size: 14px;
1768
+ font-weight: 500;
1769
+ font-family: inherit;
1770
+ color: var(--bp-primary);
1771
+ transition: background var(--bp-transition);
1772
+ }
1773
+
1774
+ .bp-convlist__new-btn svg {
1775
+ width: 16px;
1776
+ height: 16px;
1777
+ flex-shrink: 0;
1778
+ }
1779
+
1780
+ .bp-convlist__new-btn:hover {
1781
+ background: rgba(99, 102, 241, 0.15);
1782
+ }
1783
+
1784
+ .bp-convlist__items {
1785
+ flex: 1;
1786
+ overflow-y: auto;
1787
+ padding: 8px;
1788
+ }
1789
+
1790
+ .bp-convlist__item {
1791
+ width: 100%;
1792
+ padding: 12px;
1793
+ border-radius: var(--bp-radius-sm);
1794
+ background: none;
1795
+ border: none;
1796
+ cursor: pointer;
1797
+ display: flex;
1798
+ flex-direction: column;
1799
+ gap: 4px;
1800
+ text-align: left;
1801
+ font-family: inherit;
1802
+ transition: background var(--bp-transition);
1803
+ color: var(--bp-gray-900);
1804
+ }
1805
+
1806
+ .bp-convlist__item:hover {
1807
+ background: var(--bp-gray-50);
1808
+ }
1809
+
1810
+ .bp-convlist__item:disabled {
1811
+ opacity: 0.5;
1812
+ cursor: not-allowed;
1813
+ }
1814
+
1815
+ .bp-convlist__item-top {
1816
+ display: flex;
1817
+ align-items: center;
1818
+ justify-content: space-between;
1819
+ gap: 8px;
1820
+ }
1821
+
1822
+ .bp-convlist__item-title {
1823
+ font-size: 14px;
1824
+ font-weight: 500;
1825
+ overflow: hidden;
1826
+ text-overflow: ellipsis;
1827
+ white-space: nowrap;
1828
+ flex: 1;
1829
+ }
1830
+
1831
+ .bp-convlist__item-status {
1832
+ font-size: 11px;
1833
+ font-weight: 500;
1834
+ padding: 2px 8px;
1835
+ border-radius: 10px;
1836
+ flex-shrink: 0;
1837
+ }
1838
+
1839
+ .bp-convlist__item-status--open {
1840
+ background: #dcfce7;
1841
+ color: #166534;
1842
+ }
1843
+
1844
+ .bp-convlist__item-status--closed {
1845
+ background: var(--bp-gray-100);
1846
+ color: var(--bp-gray-500);
1847
+ }
1848
+
1849
+ .bp-convlist__item-preview {
1850
+ font-size: 13px;
1851
+ color: var(--bp-gray-500);
1852
+ overflow: hidden;
1853
+ text-overflow: ellipsis;
1854
+ white-space: nowrap;
1855
+ }
1856
+
1857
+ .bp-convlist__empty {
1858
+ padding: 32px 16px;
1859
+ text-align: center;
1860
+ color: var(--bp-gray-500);
1861
+ font-size: 14px;
1862
+ }
1863
+
1864
+ /* ===== Closed conversation banner ===== */
1865
+ .bp-closed-banner {
1866
+ padding: 14px 16px;
1867
+ border-top: 1px solid var(--bp-gray-200);
1868
+ background: var(--bp-gray-50);
1869
+ flex-shrink: 0;
1870
+ display: flex;
1871
+ align-items: center;
1872
+ justify-content: center;
1873
+ gap: 6px;
1874
+ flex-wrap: wrap;
1875
+ }
1876
+
1877
+ .bp-closed-banner__text {
1878
+ font-size: 13px;
1879
+ color: var(--bp-gray-500);
1880
+ }
1881
+
1882
+ .bp-closed-banner__reopen {
1883
+ background: none;
1884
+ border: none;
1885
+ padding: 0;
1886
+ font-size: 13px;
1887
+ font-weight: 500;
1888
+ font-family: inherit;
1889
+ color: var(--bp-primary);
1890
+ cursor: pointer;
1891
+ text-decoration: underline;
1892
+ text-underline-offset: 2px;
1893
+ }
1894
+
1895
+ .bp-closed-banner__reopen:hover {
1896
+ opacity: 0.8;
1897
+ }
1898
+
1899
+ /* ===== Privacy footer ===== */
1900
+ .bp-privacy-footer {
1901
+ padding: 4px 16px 8px;
1902
+ text-align: center;
1903
+ font-size: 11px;
1904
+ color: var(--bp-gray-500);
1905
+ flex-shrink: 0;
1906
+ }
1907
+
1908
+ .bp-privacy-footer a {
1909
+ color: var(--bp-primary);
1910
+ text-decoration: underline;
1911
+ }
1912
+
1913
+ /* ===== Loading ===== */
1914
+ .bp-loading {
1915
+ flex: 1;
1916
+ display: flex;
1917
+ align-items: center;
1918
+ justify-content: center;
1919
+ }
1920
+
1921
+ .bp-spinner {
1922
+ width: 32px;
1923
+ height: 32px;
1924
+ border: 3px solid var(--bp-gray-200);
1925
+ border-top-color: var(--bp-primary);
1926
+ border-radius: 50%;
1927
+ animation: bp-spin 0.6s linear infinite;
1928
+ }
1929
+
1930
+ @keyframes bp-spin {
1931
+ to {
1932
+ transform: rotate(360deg);
1933
+ }
1934
+ }
1935
+
1936
+ /* ===== Hidden ===== */
1937
+ .bp-hidden {
1938
+ display: none !important;
1939
+ }
1940
+ `;
1941
+
1942
+ // src/ui/mount.ts
1943
+ var styleElement = null;
1944
+ var hostElement = null;
1945
+ function mount(options) {
1946
+ if (!styleElement) {
1947
+ styleElement = document.createElement("style");
1948
+ styleElement.id = "baseportal-chat-styles";
1949
+ styleElement.textContent = widget_css_default;
1950
+ document.head.appendChild(styleElement);
1951
+ }
1952
+ hostElement = document.createElement("div");
1953
+ hostElement.id = "baseportal-chat-widget";
1954
+ const target = options.container || document.body;
1955
+ target.appendChild(hostElement);
1956
+ const primaryColor = options.channelInfo.theme?.primaryColor || "#6366f1";
1957
+ const contrastColor = getContrastColor(primaryColor);
1958
+ hostElement.style.setProperty("--bp-primary", primaryColor);
1959
+ hostElement.style.setProperty("--bp-primary-contrast", contrastColor);
1960
+ render(
1961
+ h(App, {
1962
+ channelInfo: options.channelInfo,
1963
+ apiClient: options.apiClient,
1964
+ realtimeClient: options.realtimeClient,
1965
+ storage: options.storage,
1966
+ events: options.events,
1967
+ visitor: options.visitor,
1968
+ isAuthenticated: options.isAuthenticated,
1969
+ position: options.position,
1970
+ hidden: options.hidden,
1971
+ t: options.t,
1972
+ isOpenRef: options.isOpenRef,
1973
+ setIsOpen: options.setIsOpen
1974
+ }),
1975
+ hostElement
1976
+ );
1977
+ }
1978
+ function unmount() {
1979
+ if (hostElement) {
1980
+ render(null, hostElement);
1981
+ hostElement.remove();
1982
+ hostElement = null;
1983
+ }
1984
+ if (styleElement) {
1985
+ styleElement.remove();
1986
+ styleElement = null;
1987
+ }
1988
+ }
1989
+ function updateTheme(primaryColor) {
1990
+ if (!hostElement) return;
1991
+ hostElement.style.setProperty("--bp-primary", primaryColor);
1992
+ hostElement.style.setProperty(
1993
+ "--bp-primary-contrast",
1994
+ getContrastColor(primaryColor)
1995
+ );
1996
+ }
1997
+ function getContrastColor(hex) {
1998
+ const r = parseInt(hex.slice(1, 3), 16);
1999
+ const g = parseInt(hex.slice(3, 5), 16);
2000
+ const b = parseInt(hex.slice(5, 7), 16);
2001
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
2002
+ return luminance > 0.5 ? "#000000" : "#ffffff";
2003
+ }
2004
+
2005
+ // src/utils/events.ts
2006
+ var EventEmitter = class {
2007
+ constructor() {
2008
+ this.listeners = /* @__PURE__ */ new Map();
2009
+ }
2010
+ on(event, callback) {
2011
+ if (!this.listeners.has(event)) {
2012
+ this.listeners.set(event, /* @__PURE__ */ new Set());
2013
+ }
2014
+ this.listeners.get(event).add(callback);
2015
+ }
2016
+ off(event, callback) {
2017
+ this.listeners.get(event)?.delete(callback);
2018
+ }
2019
+ emit(event, ...args) {
2020
+ this.listeners.get(event)?.forEach((cb) => {
2021
+ try {
2022
+ cb(...args);
2023
+ } catch (e) {
2024
+ console.error(`[BaseportalChat] Error in ${event} handler:`, e);
2025
+ }
2026
+ });
2027
+ }
2028
+ removeAllListeners() {
2029
+ this.listeners.clear();
2030
+ }
2031
+ };
2032
+
2033
+ // src/utils/storage.ts
2034
+ var Storage = class {
2035
+ constructor(channelToken, email) {
2036
+ this.prefix = email ? `bp_chat_${channelToken}_${email}` : `bp_chat_${channelToken}`;
2037
+ }
2038
+ get() {
2039
+ try {
2040
+ const raw = localStorage.getItem(this.prefix);
2041
+ return raw ? JSON.parse(raw) : {};
2042
+ } catch {
2043
+ return {};
2044
+ }
2045
+ }
2046
+ set(data) {
2047
+ try {
2048
+ const current = this.get();
2049
+ localStorage.setItem(this.prefix, JSON.stringify({ ...current, ...data }));
2050
+ } catch {
2051
+ }
2052
+ }
2053
+ getConversationId() {
2054
+ return this.get().conversationId;
2055
+ }
2056
+ setConversationId(id) {
2057
+ this.set({ conversationId: id });
2058
+ }
2059
+ getVisitor() {
2060
+ return this.get().visitor;
2061
+ }
2062
+ setVisitor(visitor) {
2063
+ this.set({ visitor });
2064
+ }
2065
+ clear() {
2066
+ try {
2067
+ localStorage.removeItem(this.prefix);
2068
+ } catch {
2069
+ }
2070
+ }
2071
+ };
2072
+
2073
+ // src/widget.ts
2074
+ var DEFAULT_API_URL = "https://api.baseportal.io";
2075
+ var BaseportalChat = class {
2076
+ constructor(config) {
2077
+ this.events = new EventEmitter();
2078
+ this.channelInfo = null;
2079
+ this.visitor = null;
2080
+ this.isAuthenticated = false;
2081
+ this.isOpenRef = { current: false };
2082
+ this.mounted = false;
2083
+ this.config = {
2084
+ ...config,
2085
+ apiUrl: config.apiUrl || DEFAULT_API_URL,
2086
+ position: config.position || "bottom-right",
2087
+ locale: config.locale || "pt"
2088
+ };
2089
+ this.hidden = config.hideOnLoad || false;
2090
+ this.visitor = config.visitor || null;
2091
+ this.isAuthenticated = !!config.visitor?.email;
2092
+ this.apiClient = new ApiClient(
2093
+ this.config.channelToken,
2094
+ this.config.apiUrl
2095
+ );
2096
+ if (this.isAuthenticated && this.visitor?.email) {
2097
+ this.apiClient.setVisitorIdentity(this.visitor.email, this.visitor.hash);
2098
+ }
2099
+ this.storage = new Storage(
2100
+ this.config.channelToken,
2101
+ this.isAuthenticated ? this.visitor?.email : void 0
2102
+ );
2103
+ this.realtimeClient = new RealtimeClient(this.apiClient);
2104
+ if (!this.visitor) {
2105
+ this.visitor = this.storage.getVisitor() || null;
2106
+ }
2107
+ this.init();
2108
+ }
2109
+ async init() {
2110
+ try {
2111
+ this.channelInfo = await this.apiClient.getChannelInfo();
2112
+ const primaryColor = this.config.theme?.primaryColor || this.channelInfo.theme?.primaryColor || "#6366f1";
2113
+ const t = getTranslations(this.config.locale);
2114
+ mount({
2115
+ channelInfo: this.channelInfo,
2116
+ apiClient: this.apiClient,
2117
+ realtimeClient: this.realtimeClient,
2118
+ storage: this.storage,
2119
+ events: this.events,
2120
+ visitor: this.visitor,
2121
+ isAuthenticated: this.isAuthenticated,
2122
+ position: this.config.position,
2123
+ hidden: this.hidden,
2124
+ t,
2125
+ container: this.config.container,
2126
+ isOpenRef: this.isOpenRef,
2127
+ setIsOpen: (open) => {
2128
+ this.isOpenRef.current = open;
2129
+ }
2130
+ });
2131
+ if (primaryColor !== "#6366f1") {
2132
+ updateTheme(primaryColor);
2133
+ }
2134
+ this.mounted = true;
2135
+ this.events.emit("ready");
2136
+ } catch (e) {
2137
+ console.error("[BaseportalChat] Failed to initialize:", e);
2138
+ }
2139
+ }
2140
+ // --- Visibility ---
2141
+ open() {
2142
+ if (!this.mounted) return;
2143
+ this.events.emit("_open");
2144
+ this.events.emit("open");
2145
+ }
2146
+ close() {
2147
+ if (!this.mounted) return;
2148
+ this.events.emit("_close");
2149
+ this.events.emit("close");
2150
+ }
2151
+ toggle() {
2152
+ if (this.isOpenRef.current) {
2153
+ this.close();
2154
+ } else {
2155
+ this.open();
2156
+ }
2157
+ }
2158
+ show() {
2159
+ this.hidden = false;
2160
+ this.events.emit("show");
2161
+ }
2162
+ hide() {
2163
+ this.hidden = true;
2164
+ this.events.emit("hide");
2165
+ }
2166
+ isOpen() {
2167
+ return this.isOpenRef.current;
2168
+ }
2169
+ // --- Visitor ---
2170
+ identify(visitor) {
2171
+ this.visitor = visitor;
2172
+ this.isAuthenticated = true;
2173
+ this.apiClient.setVisitorIdentity(visitor.email, visitor.hash);
2174
+ this.storage = new Storage(this.config.channelToken, visitor.email);
2175
+ this.storage.setVisitor(visitor);
2176
+ this.events.emit("identified", visitor);
2177
+ if (this.mounted) {
2178
+ this.remount();
2179
+ }
2180
+ }
2181
+ updateVisitor(data) {
2182
+ if (this.visitor) {
2183
+ this.visitor = { ...this.visitor, ...data };
2184
+ this.storage.setVisitor(this.visitor);
2185
+ }
2186
+ }
2187
+ clearVisitor() {
2188
+ this.visitor = null;
2189
+ this.isAuthenticated = false;
2190
+ this.apiClient.clearVisitorIdentity();
2191
+ this.storage.clear();
2192
+ this.storage = new Storage(this.config.channelToken);
2193
+ this.realtimeClient.unsubscribe();
2194
+ if (this.mounted) {
2195
+ this.remount();
2196
+ }
2197
+ }
2198
+ // --- Actions ---
2199
+ sendMessage(content) {
2200
+ this.events.emit("_sendMessage", content);
2201
+ }
2202
+ setConversationId(id) {
2203
+ this.events.emit("_setConversationId", id);
2204
+ }
2205
+ newConversation() {
2206
+ this.events.emit("_newConversation");
2207
+ }
2208
+ // --- Config ---
2209
+ setTheme(theme) {
2210
+ if (theme.primaryColor) {
2211
+ updateTheme(theme.primaryColor);
2212
+ }
2213
+ }
2214
+ setPosition(position) {
2215
+ this.config.position = position;
2216
+ if (this.mounted) {
2217
+ this.remount();
2218
+ }
2219
+ }
2220
+ setLocale(locale) {
2221
+ this.config.locale = locale;
2222
+ if (this.mounted) {
2223
+ this.remount();
2224
+ }
2225
+ }
2226
+ // --- Events ---
2227
+ on(event, callback) {
2228
+ this.events.on(event, callback);
2229
+ }
2230
+ off(event, callback) {
2231
+ this.events.off(event, callback);
2232
+ }
2233
+ // --- Lifecycle ---
2234
+ destroy() {
2235
+ this.realtimeClient.unsubscribe();
2236
+ this.events.removeAllListeners();
2237
+ unmount();
2238
+ this.mounted = false;
2239
+ }
2240
+ remount() {
2241
+ unmount();
2242
+ this.mounted = false;
2243
+ this.init();
2244
+ }
2245
+ };
2246
+
2247
+ // src/index.ts
2248
+ var src_default = BaseportalChat;
2249
+ export {
2250
+ BaseportalChat,
2251
+ src_default as default
2252
+ };
2253
+ //# sourceMappingURL=index.esm.js.map